Large file bytes belong in object storage; PostgreSQL stores ownership, metadata, status, hashes, and lineage. Uploads are untrusted: limit size and type, use random object keys, scan content, parse in isolation, and publish only after verification. Signed URLs grant temporary access and must be scoped, short-lived, and authorized when issued.
Use a server-generated object key such as tenant/<uuid>/documents/<uuid>/source; never concatenate a user filename into a filesystem path. Preserve the display filename separately after normalization.
For small files, FastAPI can stream to storage while enforcing a byte limit. For large files, return a short-lived signed upload request restricted to one key, content size/type conditions where supported, and a completion callback or explicit finalize operation.
Store document ID, tenant, owner, object key, declared/detected media type, byte size, cryptographic hash, scan status, parser/chunker/embedding versions, retention class, timestamps, and error category. Do not make a signed URL the durable identifier.
Parsers process adversarial input. Set CPU, memory, page, decompression, and time limits; disable macros/external references; isolate risky formats; patch dependencies; and reject encrypted or unsupported content according to policy. Chunking starts only after the document is authorized and clean.
RAG inherits every ingestion mistake. Hidden instructions inside documents are data, not trusted policy. Preserve source locations and access metadata per chunk, and never let a retrieved document change tool permissions. Deletion must remove or tombstone derived chunks, embeddings, caches, and evaluation copies.
Specify a PDF upload and ingestion API with direct upload, finalize, status, download, and delete operations.
Acceptance criteria: wrong-tenant access fails at metadata and object layers, oversize and mismatched content are rejected, duplicate finalize is harmless, and delete traces every derived artifact.