03 — User Journeys (Files Module)
- 1. Journey: Upload a document (teacher, office)
- 2. Journey: Download and open (student, parent)
- 3. Journey: Delete a misplaced file (admin)
- 4. Journey: Verify a batch of uploads (office manager)
- 5. Journey: Signed-URL download (planned)
- Journey map (mermaid)
Journeys map to the real endpoint surface (
files.controller.ts:29-71). Multi-file, sharing, and signed-URL steps are(planned)/(forward-looking).
1. Journey: Upload a document (teacher, office)
Context screen (fees/circular/record)
→ tap "Attach file" → POST /api/v1/files/upload (multipart, field `file`, files.controller.ts:29-41)
→ multipart with `file` field → FilesService.upload: tenant scoped (files.service.ts:28),
storage key `<uuid>--<originalName>` (files.service.ts:31),
provider upload → FileRecord persisted (files.service.ts:36-45)
→ 201 → record JSON {id, originalName, mimeType, size, ...}
→ client shows file tile in context list (GET /files refreshed)
- Error paths: 401/403 (guard layer,
(planned)— guards not yet implemented), 500 provider outage; offline → client-side retry queue ((forward-looking)). - Large-file reality: no size cap is enforced server-side today — the entire buffer
round-trips through memory (
FileInterceptormemory storage →upload(input.buffer)→res.send(buffer)). QA and client must assume ~tens of MB max for safety.
2. Journey: Download and open (student, parent)
File tile → tap download
→ GET /api/v1/files/:id/download (files.controller.ts:55-64)
→ service: findById → storage.download(storageFileId) → Buffer (files.service.ts:58-64)
→ headers: Content-Type = stored mimeType, Content-Disposition: attachment; filename="<originalName>"
→ body: whole file → OS open/save
- Not a redirect, not a signed URL — a proxied buffered stream. Fine for <25 MB; for large
exports the blueprint wants streaming (
STORAGE_ARCHITECTURE.md:67)(planned). - No
Rangesupport → no resume for interrupted downloads; client should retry from scratch.
3. Journey: Delete a misplaced file (admin)
File tile → overflow menu → Delete → confirm dialog
→ DELETE /api/v1/files/:id (files.controller.ts:66-71)
→ service: findById (404 if gone) → storage.delete(storageFileId) → repo.softDelete (files.service.ts:66-70)
→ { message: 'File deleted' } → tile removed optimistically; audit note (proposed)
- Semantics: object destroyed at provider, metadata soft-deleted (still visible to
admin-only future audit queries; excluded from normal lists via
BaseRepository).
4. Journey: Verify a batch of uploads (office manager)
GET /api/v1/files → array sorted createdAt desc (files.service.ts:48-50)
→ client groups/filters by its own context metadata (no server filter exists)
→ tap each → detail sheet (GET /files/:id) → download spot-check
(proposed)enhancement: pagination?page&limit,?mimeType=filter — no server support.
5. Journey: Signed-URL download (planned)
POST (planned) or GET /files/:id/signed-url?expiresIn=3600
→ provider.getSignedUrl(storageFileId, ttl) (storage-provider.ts:25)
→ R2 presigned GET (r2.provider.ts:77-86); Appwrite view URL (appwrite-storage.provider.ts:67-74,
NOTE: ignores expiresInSeconds); local returns fake path `/api/v1/files/<id>`
(local-storage.provider.ts:47-51, ponytail shortcut)
→ client downloads direct from provider; expiry → 403 → re-request
Journey map (mermaid)
flowchart LR
A[Context screen] --> B[Upload sheet]
B -->|POST /files/upload| C[(Storage provider)]
C -->|StoredFile| D[FileRecord in Mongo]
D --> E[File tile in list]
E -->|GET /files/:id/download| F[OS open/save]
E -->|DELETE /files/:id| G[(object deleted + soft-delete)]