Storage
GiftWrapt stores user avatars and item photos in an S3-compatible bucket. The app doesn’t care which backend you use - any service that speaks the S3 API works. The bundled compose files include Garage and RustFS as sidecar options; for hosted deploys, AWS S3, Cloudflare R2, and Supabase Storage all work fine.
How Uploads Flow
Section titled “How Uploads Flow”- The browser uploads to a server function.
- Sharp transcodes the image to webp (256x256 cover for avatars, 1200px long edge for item photos) before writing to storage.
- The object is written to your bucket via S3
PutObject. - Public URLs are either direct (when
STORAGE_PUBLIC_URLis set) or proxied through/api/files/<key>(the default). - Each upload uses a fresh nanoid suffix in the key, so URLs are immutable and safe to cache aggressively.
Required Env Vars
Section titled “Required Env Vars”Five vars do the work. See Environment variables for the full reference.
STORAGE_ENDPOINT=STORAGE_REGION=STORAGE_BUCKET=STORAGE_ACCESS_KEY_ID=STORAGE_SECRET_ACCESS_KEY=Plus two important toggles:
| Var | When to set it |
|---|---|
STORAGE_FORCE_PATH_STYLE | true for Garage, RustFS, MinIO. false (default) for AWS S3, Cloudflare R2. |
STORAGE_PUBLIC_URL | Set to a CDN base URL to hand clients direct URLs and skip the per-image proxy. Recommended on Vercel to avoid function invocations. |
Backend Recipes
Section titled “Backend Recipes”Garage (Self-Host)
Section titled “Garage (Self-Host)”The default for the bundled compose stack. Garage is a small Rust S3 server that runs alongside the app and Postgres in the same compose file.
STORAGE_ENDPOINT=http://garage:3900STORAGE_REGION=garageSTORAGE_BUCKET=giftwraptSTORAGE_ACCESS_KEY_ID=GK<24 hex chars>STORAGE_SECRET_ACCESS_KEY=<64 hex chars>STORAGE_FORCE_PATH_STYLE=true
# Bootstrap Garage on first bootINIT_GARAGE=trueGARAGE_RPC_SECRET=<64 hex chars>GARAGE_ADMIN_TOKEN=<64 hex chars>Garage requires specific key formats:
- Access key ID:
GK+ 24 hex chars →printf 'GK%s' "$(openssl rand -hex 12)" - Secret key: 64 hex chars →
openssl rand -hex 32
When INIT_GARAGE=true, the app’s entrypoint runs Garage’s admin HTTP API on first boot to assign cluster layout, create the bucket, import the key, and grant permissions. Idempotent.
RustFS (Self-Host)
Section titled “RustFS (Self-Host)”Alternative to Garage. Pick one. RustFS is simpler in that it reads its root user directly from the standard STORAGE_* vars, no admin token needed.
STORAGE_ENDPOINT=http://rustfs:9000STORAGE_REGION=us-east-1STORAGE_BUCKET=giftwraptSTORAGE_ACCESS_KEY_ID=<any string>STORAGE_SECRET_ACCESS_KEY=<any string>STORAGE_FORCE_PATH_STYLE=trueINIT_RUSTFS=trueAWS S3
Section titled “AWS S3”STORAGE_ENDPOINT=https://s3.us-east-1.amazonaws.comSTORAGE_REGION=us-east-1STORAGE_BUCKET=your-bucketSTORAGE_ACCESS_KEY_ID=AKIA...STORAGE_SECRET_ACCESS_KEY=...STORAGE_FORCE_PATH_STYLE=falseSTORAGE_PUBLIC_URL=https://your-bucket.s3.us-east-1.amazonaws.com # optionalCloudflare R2
Section titled “Cloudflare R2”STORAGE_ENDPOINT=https://<account-id>.r2.cloudflarestorage.comSTORAGE_REGION=autoSTORAGE_BUCKET=giftwraptSTORAGE_ACCESS_KEY_ID=<R2 access key id>STORAGE_SECRET_ACCESS_KEY=<R2 secret>STORAGE_FORCE_PATH_STYLE=falseSTORAGE_PUBLIC_URL=https://cdn.example.com # your R2 public bucket / custom domainSupabase Storage
Section titled “Supabase Storage”Supabase exposes an S3-compatible endpoint at <project>.supabase.co/storage/v1/s3. The Vercel + Supabase Marketplace integration auto-derives this from SUPABASE_URL if you don’t set STORAGE_ENDPOINT yourself.
STORAGE_ENDPOINT=https://<project>.supabase.co/storage/v1/s3STORAGE_REGION=us-east-1STORAGE_BUCKET=giftwraptSTORAGE_ACCESS_KEY_ID=<Supabase storage access key>STORAGE_SECRET_ACCESS_KEY=<Supabase storage secret>STORAGE_FORCE_PATH_STYLE=trueAdmin > Storage
Section titled “Admin > Storage”The admin panel has a Storage page that exposes a few read-only diagnostics:
- Bucket connectivity check.
- Object count and approximate disk usage.
- Recent uploads with previews.
- A test-upload button to verify writes end-to-end without leaving a screenshot in someone’s wishlist.
There’s no admin UI for switching backends - storage is environment configuration only. To change provider, update the env vars and restart.
Mirroring External Images
Section titled “Mirroring External Images”mirrorExternalImagesOnSave is an admin toggle (off by default). When on, every image URL the scraper hands back gets re-fetched and copied into your bucket on item save. This decouples your lists from the source URL’s lifetime - useful if you’ve been burned by Amazon swapping product images out from under you.
Trade-off: each save costs a fetch and an upload, and your bucket grows faster. Leave it off for small deployments.