Buckets
A bucket is a logical container for objects. Each bucket carries a public/private flag, optional per-file size cap, optional total bucket size cap, an allow-list of MIME types, and an owner. Buckets are :storage.Bucket nodes - visible to Cypher, GraphQL, and RLS like any other graph entity.
Public vs Private
A public: true bucket serves objects via GET /storage/v1/object/public/{bucket}/{path} with no authentication. Flipping a bucket to private (or revoking its signing version) is the kill-switch for sharing.
# Toggle private at any time:
ALTER BUCKET avatars SET PUBLIC = false;
# Or via REST:
curl -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"public": false}' \
http://localhost:7474/storage/v1/bucket/avatarsSize limits
| Field | Scope | Notes |
|---|---|---|
| file_size_limit | Per object | Rejects upload with 413 if Content-Length exceeds |
| bucket_size_limit | Sum across bucket | Pre-flight + atomic check at write time |
| max_file_size | Global (config) | Server-wide ceiling (anvil.toml) |
| max_total_storage | Global (config) | Storage cap across every bucket |
| max_user_storage | Per user | Tracked via :auth.User.storage_used |
Allowed MIME types
If allowed_mime_types is non-empty, uploads with a Content-Type outside the list are rejected with 415.
CREATE BUCKET avatars PUBLIC = true, FILE_SIZE_LIMIT = 5242880, ALLOWED_MIME_TYPES = ["image/png", "image/jpeg", "image/webp"];
App Buckets
A bucket can belong to an app: pass "app": "<slug or id>" when creating it over REST (the DDL has no APP clause) and the slug is stored and returned as app on the bucket. It is fixed at creation. An app-scoped service-account key of that app - one whose privilege in the app is editor or admin - can then upload, download, list, delete and sign objects in the bucket, and nothing else under /storage: it cannot create, list or change buckets, its own included. A bucket it may not use answers 403 whether it is missing, has no app or belongs to another app. Give an app bucket a plain id; one that is also a route word (copy, move, list, sign, upload, public, signed) is never reachable by an app key.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "crm-files", "public": false, "app": "crm"}' \
http://localhost:7474/storage/v1/bucket
# => { "id": "crm-files", "app": "crm", ... }
# With an app key of crm (editor or admin in the app):
curl -X POST -H "Authorization: Bearer anvil_sk_..." \
--data-binary @quote.pdf \
http://localhost:7474/storage/v1/object/crm-files/quotes/q1.pdf| Route | An app key may |
|---|---|
| POST / PUT / GET / HEAD / DELETE /storage/v1/object/{bucket}/{path} | Upload, upsert, download (Range included), read metadata, delete |
| POST /storage/v1/object/list/{bucket} | List |
| POST /storage/v1/object/sign/{bucket}/{path} | Mint a signed download URL |
| POST /storage/v1/object/upload/sign/{bucket}/{path} | Mint a signed upload URL |
REST surface
| Method | Path | Use |
|---|---|---|
| POST | /storage/v1/bucket | Create (optional app: the bucket belongs to that app) |
| GET | /storage/v1/bucket | List visible buckets |
| GET | /storage/v1/bucket/{id} | Fetch one |
| PUT | /storage/v1/bucket/{id} | Update settings |
| DELETE | /storage/v1/bucket/{id} | Drop (must be empty) |
| POST | /storage/v1/bucket/{id}/empty | Delete every object inside |
| POST | /storage/v1/bucket/{id}/sign-revoke | Invalidate signed URLs |