zazzy
Features

File uploads

S3-compatible storage with presigned uploads and per-plan limits.

File uploads store user content in an S3-compatible bucket. The Next server never proxies bytes: it issues short-lived presigned URLs, the browser PUTs straight to the bucket, and a download route redirects to a presigned GET. Every file is org-scoped, capped by a per-purpose size/type policy, and metered against an optional per-plan storage quota.

Optional feature

Files is optional (core: false, requires: ["orgs"]). Files are org-owned; the storage quota and the deletion audit are soft seams that light up only when flags and audit-log are present.

What it does

  • Presigned upload flow — the files router (createFilesRouter) on orgProcedure exposes createUpload (validate, quota-check, write a pending row, return { fileId, uploadUrl }), confirmUpload (pendingready), delete (soft-delete → status = "deleted"), and list. The client PUTs via putObject (lib/put-object.ts, XMLHttpRequest for progress).
  • Storage primitivesserver/storage.ts is the only importer of the AWS SDK: presignUpload / presignDownload / deleteObject, both TTLs 300s, forcePathStyle: true for MinIO, with content-type bound into the SigV4 signature so the stored type can't be swapped.
  • Per-purpose constraintslib/config.ts defines FilePurpose (avatar | logo | attachment): avatars and logos are 2 MiB raster images (SVG excluded to avoid stored-XSS); attachments are 25 MiB (images + common docs). These caps are fixed product policy, not entitlements.
  • Download routeGET /api/files/:id authorizes via session membership in the file's org, then 302-redirects to a presigned GET. Missing / non-ready / other-org files all collapse to 404 (no existence oracle); attachments get a sanitized Content-Disposition: attachment.
  • ReaperreapFiles(db, deleteObj, asOf) reclaims orphaned pending rows (older than 24h) and soft-deleted rows, object first then row. It runs from a durable Inngest cron and, when jobs is pruned, from an opportunistic on-write sweep inside createUpload.
  • UI componentsFileUpload, AvatarPicker, LogoPicker, and AttachmentsManager; /settings/attachments is the worked attachment example.

Models it adds

ModelPurpose
FileAn org-scoped uploaded file: key (unique, {orgId}/{purpose}/{uuid}), contentType, size, purpose, and a status that walks pending → ready → deleted. uploadedById is a plain audit column, not a User relation, so deleting a user never cascades files away.

There is no separate Attachment model — "attachment" is a purpose value on File. Adds the marker-wrapped Organization.files relation.

Env it needs

S3_ENDPOINT=            # S3-compatible endpoint (MinIO in dev)
S3_REGION=
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=

All are .optional() in env.ts (D-043), so a generated app builds with uploads dormant; getStorage() throws loudly listing any missing var when a route actually needs storage. Adds the minio compose service and the two @aws-sdk/* dependencies.

Composition

  • requires: ["orgs"] — every procedure runs on orgProcedure scoped to the active org, File.organizationId cascades from Organization, and object keys are org-partitioned.
  • Soft consumer seams (injected, files never imports them): flags supplies the storageBytes quota check (omitted ⇒ unlimited); audit-log records file.deleted; jobs runs the reaper cron (the on-write sweep is the jobs-pruned fallback).
  • Seams files exposesAvatarPicker (consumed by the profile form behind @feature:files markers, saving through core user.updateAvatar) and LogoPicker (consumed by org settings, saving through core org.updateLogo). User.image and Organization.logo are core fields; only the upload path prunes, falling back to a plain URL/initials display.
  • Dependents — none; no feature requires files.

What pruning removes

Leaving files out drops features/files, the /api/files/:id route, the /settings/attachments page, the File model and the Organization.files relation, and the createFilesRouter wiring — plus the files-reaper cron, the five S3_* env vars, the minio compose service, and the two AWS SDK dependencies. No feature requires files, so nothing else is forced out; the avatar and logo pickers fall back to plain image/URL fields at their marked call sites.

On this page