fullstack app has an object store alongside its database: buckets you name, holding objects by key. Storage is app-scoped and org-level — it belongs to the app, outlives any session, and is reachable by every agent that can reach the app (allowed_apps on the apps tool). A frontend_only app has no storage and answers 501 feature_not_configured; a backend still provisioning answers a retryable job_not_ready.
This is not the files API. Files are what your agents produce; storage is what your app serves.
{id} in every route below is what the database routes take: an app_ id, the app’s name, or the word default, which resolves the organization’s only fullstack app. With two or more fullstack apps default is 409 ambiguous_app; with none, 404 not_found.
Shapes
object
{ object: "bucket", name, public, created_at }. A name is ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$. public is recorded on the bucket; every read through Vetta is authorized by your key or a signed URL either way.object
{ object: "storage_object", bucket, key, size_bytes, content_type, etag, updated_at }. etag is the store’s hash without quotes, or null when it reports none.object
{ object: "signed_url", url, method, expires_at }. method is PUT or GET. The url points at the app’s own backend host and is the only place that host is visible; it carries its grant in the URL and is dead after expires_at.Buckets
GET /v1/apps/{id}/storage/buckets— scopeagents:read.{ object: "list", data: bucket[] }— the buckets the app’s store holds, including ones your own code created there. A bucket whose name falls outside the grammar above is the store’s alone: it is neither listed here nor tracked by Vetta.POST /v1/apps/{id}/storage/buckets— scopeagents:write. Body{ name, public? }(default private) →201 bucket. An existing name is409 name_conflict.DELETE /v1/apps/{id}/storage/buckets/{bucket}— scopeagents:write. A non-empty bucket is refused with400 validation_failed(paramforce);?force=trueempties it first.
Objects
GET /v1/apps/{id}/storage/buckets/{bucket}/objects?prefix=&limit=&after=— scopeagents:read. A page ofstorage_object, keys underprefix.PUT /v1/apps/{id}/storage/buckets/{bucket}/objects/{key}— scopeagents:write. The raw body is the object, up to 50 MiB;Content-Typeis required and stored →storage_object. Anything larger goes through an upload URL.GET /v1/apps/{id}/storage/buckets/{bucket}/objects/{key}— scopeagents:read. The bytes, with the storedContent-Type— authorized by your key like every other read.?signed=truereturns a one-hoursigned_url(GET) instead of the bytes.DELETE /v1/apps/{id}/storage/buckets/{bucket}/objects/{key}— scopeagents:write. Idempotent204.
/ — users/alice.json is one object with a two-segment key. Encode each segment as you would any path segment; an empty segment or .. is 400 validation_failed (param key).
Signed URLs
POST /v1/apps/{id}/storage/buckets/{bucket}/objects/{key}/upload-url — scope agents:write
string
required
The type the upload will carry.
integer
60–3600, default 900.
signed_url with method: "PUT". PUT the bytes to url with that Content-Type before expires_at; the object then appears in the bucket like one uploaded inline. The download counterpart is GET .../objects/{key}?signed=true, above.
From an agent
An agent with theapps tool reaches the same store through five actions, gated by the tool’s allowed_apps. When the organization has exactly one fullstack app, app may be omitted; several is 409 ambiguous_app, none is 404 not_found.
Bytes never sit in a tool result:
storage_put reads a fil_ the agent already has and storage_get returns one it did not, so the files API is the way in and out.
Auditing and billing
Bucket creation and deletion, object uploads, downloads and deletions are audit rows (app.bucket_created, app.bucket_deleted, app.storage_put, app.storage_get, app.storage_deleted) carrying the bucket and key — never the bytes. Creating or deleting a bucket also reaches webhook subscribers as app.storage.bucket.created / app.storage.bucket.deleted with { app_id, bucket }, once the bucket and its audit row exist; object reads and writes are audit rows only, never events. Storage bills on the storage line item per bucket per day once a rate is published; no rate is published today, so storage carries no charge — and no line appears on the ledger.