File storage
What is an object store?
An object store is a bucket you own — on Amazon S3 or Cloudflare R2 — that nuzur can write files to on your behalf. You register it once at the team level, give it an identifier, and from then on you refer to it by name: a File field points at it, and nuzur-cli deploy hands its credentials to a deployed app.
Registering the store in nuzur rather than pasting keys into a config file is the point. The bucket stays yours, the credentials stay encrypted, and every project in the team can use the same store without anyone copying a secret around.
Registering a store
In the nuzur web app, open team settings → Object Stores and click New object store.
Every store has:
- Identifier — a short name you'll recognize later (e.g.
media-prod,uploads-eu). - Type —
S3for Amazon S3,R2for Cloudflare R2. - Status —
activeordisabled. Disabling a store leaves it registered but takes it out of circulation, so you can retire a bucket without deleting its configuration.
The rest of the form depends on the type.
S3 and R2 side by side
| Field | Amazon S3 | Cloudflare R2 |
|---|---|---|
| Bucket | bucket name | bucket name |
| Region | e.g. us-east-1 |
not asked — R2 is always auto |
| Account id | — | your Cloudflare account id |
| Access key | access key id | access key id |
| Secret | secret access key | secret access key |
| Endpoint | derived from the region | derived from the account id, overridable |
Amazon S3
Create (or reuse) an IAM user or role with read and write access to the bucket, then paste its access key id, secret access key, region, and bucket name.
Cloudflare R2
R2 speaks the S3 API, so nuzur talks to it the same way — it just addresses it differently. Two values are specific to R2:
Your account id. Open the Cloudflare dashboard and go to R2. The account id is shown on the R2 overview page, next to the S3 API endpoint. It's the same 32-character hex id that appears in your dashboard URL. nuzur derives the endpoint from it:
https://<account_id>.r2.cloudflarestorage.com
An R2 API token. Still in R2, open Manage R2 API Tokens → Create API token. Give it the Object Read & Write permission and scope it to the bucket you're registering. Cloudflare then shows you an Access Key ID and a Secret Access Key — those are the two values nuzur wants. Copy them immediately; Cloudflare shows the secret once.
Region: R2 has no regions in the S3 sense. Its region is always
auto, and nuzur fills that in for you — there is no region field on an R2 store.
The endpoint override
The derived endpoint is right for a standard R2 bucket. If your bucket lives in one of R2's jurisdictional locations, its host is different — the EU jurisdiction, for example, is served from:
https://<account_id>.eu.r2.cloudflarestorage.com
That's what the optional endpoint field is for. Leave it empty and nuzur derives the standard host; fill it in and nuzur uses exactly what you typed. It also works for any other S3-compatible service that speaks the same API.
Where your keys are stored
The access key and secret never touch your project schema, and they are never written into a project version's JSON. When you save a store, nuzur pushes the pair into AWS Secrets Manager, encrypted with KMS and keyed by team. What lives in the project — and in anything you export — is the store's UUID and its non-secret settings.
That means:
- Sharing a project, exporting a version, or handing a model to a teammate never leaks a credential.
- Rotating a key is a change in one place: edit the store, and every field and every future deploy that references it picks up the new value.
- A deployed app gets the credentials at deploy time (see below), not by reading them back out of nuzur at runtime.
Pointing a field at a store
Four field types hold files: File, Image, Audio, and Video. Each one has a storage type in its type config:
| Storage type | Where the bytes go |
|---|---|
binary |
nuzur's own storage — nothing to configure |
object_store |
your bucket, via a registered object store |
Choose object_store and the field asks for two more things:
- Object store — pick one of the team's registered stores.
- Path prefix — the key prefix every file for this field is written under, e.g.
avatars/orinvoices/2026/. It keeps one field's files from colliding with another's inside a shared bucket.
That's the whole field config: a store UUID and a prefix. The credentials aren't copied into the field — nuzur resolves them from the team on each request, which is why rotating a key needs no change to the model.
See Field types for the full list of types and their validations.
Generated /upload and /sign endpoints
The field-level setting above is about files nuzur stores for you. Go Code Gen has a separate, optional file storage switch that gives your generated backend its own upload path. Turn it on and the generated app exposes two endpoints:
POST /upload
A multipart/form-data request with the file in a part named file. It returns the stored object's URL and key:
{ "url": "https://…/uploads/9f2c….png", "key": "uploads/9f2c….png" }
POST /sign
Takes a key or a url and returns a time-limited signed URL for it. Request:
{ "key": "uploads/9f2c….png", "expiry_seconds": 900 }
Response:
{ "url": "https://…/uploads/9f2c….png?X-Amz-Signature=…" }
expiry_seconds is optional.
How your model uses them
The endpoints are generic — they are not bound to any entity. File fields in the generated app stay plain string/URL columns, so the flow is two steps:
POST /uploadthe bytes, take theurlfrom the response.- Put that
urlinto a normal create or update payload, like any other string field.
If the app's
aws:config block is missing,/uploadand/signreturn503. The app still boots and every other endpoint keeps working — a missing storage config degrades those two routes instead of taking the service down.
Getting credentials to a deployed app
nuzur-cli deploy wires this up for you. The recommended path is to reference a registered store:
nuzur-cli deploy --host 203.0.113.10 --project my-project \
--storage-enabled --storage 8f1c2b7e-....
--storage takes the object store UUID. Its credentials are resolved server-side from your team at deploy time — the secret is never typed on your command line and never stored in a deploy-config file.
If you'd rather pass a bucket by hand — including an R2 or any other S3-compatible bucket that isn't registered in nuzur — use the manual flags:
| Flag | Description |
|---|---|
--storage-enabled |
Generate the file storage layer (/upload and /sign) |
--storage |
UUID of a registered object store; credentials resolved server-side from your team |
--s3-bucket |
Bucket name |
--s3-region |
Bucket region (use auto for R2) |
--s3-access-key |
Access key id |
--s3-secret |
Secret access key — CLI-only, never written to a deploy-config file |
--s3-endpoint |
Endpoint override for R2 or any S3-compatible service |
For a manual R2 bucket, --s3-endpoint https://<account_id>.r2.cloudflarestorage.com with --s3-region auto is the equivalent of a registered R2 store.
Whichever way the credentials are resolved, deploy writes them into the project's prod.yaml on the server with chmod 600 — readable only by the account that runs the app. See Deploying a project for the rest of the deploy flags.