Application documents (upload / download / delete) #132

Open
opened 2026-06-19 13:26:27 +00:00 by james · 0 comments
Owner

Let a user attach documents to an application — résumés sent, cover letters, supporting material, offer letters, correspondence — and download or delete them later.

Builds on the Application core ticket and the lib/storage blob interface from ADR-0018 (the same primitive Profile picture upload uses today).

Scope

  • New application_documents table: (id, user_id, application_id, filename, content_type, size_bytes, storage_key, uploaded_at). user_id and application_id both FK + indexed. storage_key is the opaque path returned by lib/storage.
  • Storage path bakes user_id into the prefix per the ADR-0018 convention (e.g. applications/{user_id}/{application_id}/{uuid}-{filename}) so a leak of lib/storage can't cross users.
  • API routes under app/api/applications/[id]/documents/:
    • POST — multipart upload, returns the document record (no plaintext storage key in the response).
    • GET — list documents for the application.
    • GET /[docId] — stream the file with the original content_type and a sensible Content-Disposition.
    • DELETE /[docId] — remove the DB row and the blob.
  • Reasonable per-file size cap (e.g. 25 MB) and an allowlist of content types (PDF, common image types, plain text, common office types). Limits live in the API handler and are surfaced in the upload UI.
  • PWA: a Documents panel on the application detail page — drag-and-drop / file-picker upload, list with download buttons, delete confirmation. TanStack Query for the document list.
  • Cross-engine tests for upload → list → download → delete and for cross-user isolation (a document URL from user A returns 404 to user B).

Acceptance criteria

  • application_documents migration applies on startup, both engines.
  • A user can upload a file to an application, see it listed, download it with the original filename + content type, and delete it.
  • Files land in the per-user storage prefix; a deletion removes both the row and the blob.
  • User B cannot download or delete user A's documents — all attempts return 404.
  • Oversize or disallowed-content-type uploads are rejected at the API layer with a clear error.
  • Tests run on both DB engines.

Part of epic #129. Depends on the Application core ticket landing first.

Let a user attach documents to an application — résumés sent, cover letters, supporting material, offer letters, correspondence — and download or delete them later. Builds on the Application core ticket and the `lib/storage` blob interface from ADR-0018 (the same primitive Profile picture upload uses today). ## Scope - New `application_documents` table: `(id, user_id, application_id, filename, content_type, size_bytes, storage_key, uploaded_at)`. `user_id` and `application_id` both FK + indexed. `storage_key` is the opaque path returned by `lib/storage`. - Storage path bakes `user_id` into the prefix per the ADR-0018 convention (e.g. `applications/{user_id}/{application_id}/{uuid}-{filename}`) so a leak of `lib/storage` can't cross users. - API routes under `app/api/applications/[id]/documents/`: - `POST` — multipart upload, returns the document record (no plaintext storage key in the response). - `GET` — list documents for the application. - `GET /[docId]` — stream the file with the original `content_type` and a sensible `Content-Disposition`. - `DELETE /[docId]` — remove the DB row and the blob. - Reasonable per-file size cap (e.g. 25 MB) and an allowlist of content types (PDF, common image types, plain text, common office types). Limits live in the API handler and are surfaced in the upload UI. - PWA: a Documents panel on the application detail page — drag-and-drop / file-picker upload, list with download buttons, delete confirmation. TanStack Query for the document list. - Cross-engine tests for upload → list → download → delete and for cross-user isolation (a document URL from user A returns 404 to user B). ## Acceptance criteria - [ ] `application_documents` migration applies on startup, both engines. - [ ] A user can upload a file to an application, see it listed, download it with the original filename + content type, and delete it. - [ ] Files land in the per-user storage prefix; a deletion removes both the row and the blob. - [ ] User B cannot download or delete user A's documents — all attempts return 404. - [ ] Oversize or disallowed-content-type uploads are rejected at the API layer with a clear error. - [ ] Tests run on both DB engines. Part of epic #129. Depends on the Application core ticket landing first.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#132
No description provided.