Documents
Review snapshots contain document metadata and a storage key. PDF bytes live on local disk in development or private Neon Object Storage when configured.
Adapter selection
DOCUMENT_STORAGE selects local or neon. Without an
override, development uses local and NODE_ENV=production uses Neon. Local
files live in ignored .demo-documents/{UUID}. Hosted mode uses S3-compatible
credentials, an HTTPS endpoint, region, and bucket. The exact environment and CORS setup
is in Document storage setup. Playwright
explicitly starts with local storage.
Upload lifecycle
createDocumentStorage().put(file)
validate extension, MIME, nonempty size ≤ 10 MiB, %PDF- header
POST /api/documents JSON { name, size }
local response:
POST multipart file → server validation → disk + owner sidecar → { key }
neon response:
signed POST (5 minutes), pending/<uploadId>, owner + exact size constraints
browser uploads directly to bucket
POST /api/documents JSON { uploadId }
verify owner, bounded read, PDF validation
write final object under a new UUID with owner metadata; attempt pending deletion
return { key }
workspace / mail desk:
document command attaches key and metadata → save review
route checks the key exists and has this uploader; stored size replaces the client size
Upload roles are examiner, supervisor, manager, and admin. Attaching or updating metadata has its own command permissions. The server validates bytes again; client checks alone are not trusted. A valid header does not prove a PDF is structurally valid or harmless.
Download authorization
GET /api/documents/[key] validates UUID syntax, loads the actor, and checks
that a current in-scope review references that storageKey. Local mode returns the PDF;
Neon mode redirects to a signed GET valid for 60 seconds. Both responses use
private/no-store caching. Guessing a stored UUID alone does not authorize the route.
Failure boundary
Object upload and review save are separate transactions. A failed attach/save can leave
bytes without reachable metadata. bun run documents:prune lists objects that
no snapshot version references and that are older than a day;
--delete removes them. There is no records-retention workflow. Test both
adapters when changing upload responses; the browser validates their shapes.