Docs / Batteries
File storage
Governed file uploads from Postgres annotations — direct-to-store uploads with mint/confirm, signed downloads, image variants, quotas, lifecycle GC and S3 / R2 / GCS adapters.
Tag a column @gqlith.file(…) and gqlith generates a complete, governed file pipeline: upload mutations, signed downloads, per-tenant quotas, lifecycle garbage collection and webhooks — under the same RBAC, tenancy and audit model as everything else.
Enabled by choosing a storage adapter. Files live in your bucket; metadata lives in a managed gqlith_files table in your Postgres.
COMMENT ON COLUMN recipes.hero_image IS
'@gqlith.file(profile: "images", maxSizeBytes: 10485760,
allowedContentTypes: ["image/png", "image/jpeg", "image/webp"],
variants: { thumb: "256x256" })';
Direct-to-store uploads, mint → confirm
File bytes never pass through your API. The client asks for an upload ticket, PUTs directly to the store, then confirms:
requestUploadmints an opaque, signed ticket after checking RBAC, content-type / size policy and the tenant’s quota.- The client uploads straight to S3 / R2 / GCS with that ticket.
confirmUploadverifies the object server-side and flips itpending → confirmed.
Attaching a file to a row is validated on every FK write — confirmed status, tenant, owner and mint binding all checked — so a caller can’t attach someone else’s upload or shop for a laxer policy.
With the client SDK, the whole dance is one call: client.files.uploadFile(blob, opts). Large files switch to resumable multipart automatically (uploadFileResumable / resumeUpload); single-request uploads are supported up to 5 GiB.
Signed downloads, private by default
File.url and downloadUrl(fileId) mint short-lived signed URLs through a fail-closed authorization gate. Visibility is configured per profile (public | private, default private), and untrusted content types are served neutralized with Content-Disposition: attachment.
Image variants
Declare presets in the annotation (variants: { thumb: "256x256" }) and request them with File.variantUrl(preset) — served via CDN transform for public profiles, or derived by a sharp worker you run on a Node/Bun host (PNG / JPEG / GIF / WEBP).
Lifecycle: GC, webhooks, quotas
- Orphaned uploads are reaped on a schedule you configure (
orphanPolicy: reap-after-30d, orretainto keep everything); deletes and restores flow through a transactional outbox with a two-phase reconciler, so the bucket and the database can’t drift. - HMAC-signed lifecycle webhooks (
onUploadConfirmed,onFileDeleted) deliver at-least-once with backoff and a dead-letter state. - Per-tenant storage quotas (
quota.maxBytesPerTenant,maxFilesPerTenant) are reserved at mint time, transactionally.
Two environment variables are load-bearing: GQLITH_REAPER_SERVICE_TOKEN authenticates the reaper endpoint on any delete-capable deployment, and GQLITH_WEBHOOK_SIGNING_KEY signs lifecycle webhooks when storagePolicy.webhooks is declared.
Adapters
s3-compatible— AWS S3, Cloudflare R2, and any S3-API store (MinIO, B2, Spaces, Ceph), with SigV4 signing that runs on the edge.gcs— Google Cloud Storage (V4 signing, resumable sessions).local— in-process object server for dev and tests. Not for production.