File attachments
Attachment definitions never add URLs, object keys, or storage-provider columns to resource tables. Drizzle owns metadata in framework schemas:
storage_blobsdescribes file objects.storage_attachmentsconnects blobs to records usingrecordType, text-normalizedrecordId, and attachment name. Text normalization supports both UUID and numeric application primary keys.
The storage adapter manages object bytes only.
Generate attachments
bunway g scaffold Product name:string image:image:optional manual:file:optional photos:files:optional
image accepts image MIME types, file is singular, and files supports multiple objects. Generated
multipart routes convert browser File values into UploadedFile before attaching them.
Model API
Generated hydrators add handles to ordinary Drizzle rows:
const product = hydrateProduct(row)
await product.image.attach(file)
await product.image.url()
await product.image.detach()
await product.image.purge()
Multiple attachments additionally expose collection operations and per-blob removal. detach removes
the database relationship while retaining the blob; purge removes the relationship, unreferenced blob
metadata, and physical object.
Local development
Local storage writes beneath storage/. Configure an absolute public URL when the frontend and API use
different origins:
STORAGE_SERVICE=local
STORAGE_PUBLIC_URL=http://localhost:3000/storage
S3 and R2
STORAGE_SERVICE=s3
STORAGE_BUCKET=my-bucket
STORAGE_REGION=auto
STORAGE_ENDPOINT=https://ACCOUNT.r2.cloudflarestorage.com
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_PUBLIC_URL=https://files.example.com
The S3-compatible adapter uses Bun's native S3 support. The attachment API is independent of the adapter, leaving room for future presigned direct uploads without changing model-facing code.
Scaffold tables show attachment counts in a dedicated badge. Clicking opens previews and removal controls; a steady filled badge uses fading outward rings to indicate attached files.