- Docs
- Data and storage
- Storage and media
Storage and media
Store files behind policy-gated presigned URLs, locally or on S3 and R2, with a media pipeline on top.
storage.Storage hands out presigned upload and download URLs for opaque objects such as receipts and photos, gated by one policy. media adds the transform and serve pipeline: Media processing.
Quick start
Section titled “Quick start”Pick local for a single instance: it writes under root and needs no infra. Pick s3 or r2 when instances must share files.
STORAGE_ADAPTER=localSTORAGE_ROOT=./data/storageSTORAGE_SECRET=dev-only-hmac-secretSTORAGE_URL_BASE=http://localhost:8080/filesSTORAGE_ADAPTER=s3STORAGE_REGION=us-east-1STORAGE_ENDPOINT=https://s3.us-east-1.amazonaws.comSTORAGE_ACCESS_KEY_ID=AKIAEXAMPLESTORAGE_SECRET_ACCESS_KEY=prod-secret-hereSTORAGE_POLICY_SYNC=manualSTORAGE_SYNC_FAIL=warnSTORAGE_URL_BASE=https://files.example.comEnv vars: STORAGE_ADAPTER, STORAGE_REGION, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY, STORAGE_URL_BASE. policy_sync is manual or auto; sync_fail is warn or require. endpoint needs a scheme and host, and a custom endpoint also requires url_base. r2 needs account_id. local defaults root to /tmp/storage; without secret it uses an ephemeral key (presigned URLs break on restart) and refuses to start under ZEVER_ENV=production or prod. See the storage options and media options.
Mint a presigned URL
Section titled “Mint a presigned URL”Storage addresses objects by bucket and key. Resolve with c.Storage() (lazy, cached, retried) or open the adapter directly:
package main
import ( "context" "fmt" "time"
storagelocal "github.com/zenta-dev/zever/adapters/storage/local" "github.com/zenta-dev/zever/core/storage")
func main() { ctx := context.Background() st, err := storagelocal.New(storage.Options{ LocalOptions: storage.LocalOptions{ Root: "./data/storage", Secret: "dev-only-hmac-secret", }, URLBase: "http://localhost:8080/files", }) if err != nil { panic(err) } defer st.Close(ctx)
up, err := st.PresignUpload(ctx, "receipts", "b_123.pdf", "application/pdf", time.Hour) if err != nil { panic(err) } fmt.Println(up.Method) // PUT or POST, per adapter down, err := st.PresignDownload(ctx, "receipts", "b_123.pdf", time.Hour) if err != nil { panic(err) } fmt.Println(down.Method) // GET}The interface also has Exists, Delete, Move (overwrites the destination; a missing source reports ErrNotFound), Name, and Close(ctx). Call the interface, not adapter internals, so the policy check runs.
Gate access with a policy
Section titled “Gate access with a policy”policy is a flat map: default covers unlisted buckets, every other key is a bucket. Each bucket sets read, write, update, and delete rules with public, allow, and deny (deny wins). * matches any authenticated subject, anonymous matches everyone.
STORAGE_ADAPTER=local# STORAGE_POLICY_DEFAULT_READ: no string form in env; set it in Go with container.WithOptions# STORAGE_POLICY_RECEIPTS_READ: no string form in env; set it in Go with container.WithOptions# STORAGE_POLICY_RECEIPTS_WRITE: no string form in env; set it in Go with container.WithOptionsStorage and media adapters are independent; keep them aligned to avoid cross-store copies. See the adapters matrix.
Production checklist
Section titled “Production checklist”- Cap media:
max_download_bytes(64 MiB),max_pixels(about 50 MP),max_duration,presign_ttl(1h default, 7-day ceiling). Details: Media processing. - Never log credentials:
access_key_idandsecret_access_keyride file or env; display goes throughconfig.RedactedServices. - Close order: both are snapshot services, closed ahead of leaf
cacheandqueue; only resolved services close. - Skip quietly at the job layer when no source file exists, rather than failing the booking flow.
Troubleshooting
Section titled “Troubleshooting”presign_ttl must be >= 0 and <= 7 days
Cause: the window exceeds the 7-day ceiling, often 720h copied from a sample. Fix: at most 604800000000000 ns in YAML or "168h" in env.
S3 works locally but fails in deploy with endpoint errors
Cause: endpoint lacks a scheme, or region is empty. Fix: set a full URL like https://s3.us-east-1.amazonaws.com and pin region.
A second instance returns 404 for a fresh upload
Cause: local with two hosts, each with a private filesystem. Fix: move both to s3 or r2 with a shared bucket and re-upload.