Objects work the way SQL does: the guest holds no bucket, no path, and no credential, and every operation crosses to the control plane’s storage service over the private link under the deployment’s workload token. The token decides which namespaces the app may address, so a namespace another app declared is unreachable even by name.

Declare a namespace

Up to five per app.
Namespaces need no operator configuration. Each is a directory under $TINY_DATA_DIR/storage/<environment>/<name> on the control-plane host.

Use it

The name is the one from resources.storage[].name, and may be omitted when the manifest declares exactly one namespace. Asking for a namespace the manifest never declared throws before any request is made.
→ ObjectMetadata
Accepts a string or Uint8Array and returns the stored object’s metadata.
→ Uint8Array | null
null means that key is absent. A missing namespace or a superseded deployment raises, so an unreachable namespace never reads as an empty one.
→ boolean
Whether the key existed.
→ ObjectSummary[]
Objects sorted by key with key, size, lastModified, and etag, filtered by an optional prefix.

Keys

Keys must be relative and may not contain ... A key that would escape its namespace is refused before it reaches a disk. Maximum key length is 1024 bytes.

Bytes are bytes

Bytes cross the link raw rather than base64-encoded in JSON, so binary is stored exactly as given — the round trip is byte-for-byte, not UTF-8-normalized.

ETags are change validators, not digests

etag is derived from an object’s size and modification time, not from a content digest. Two identical objects under different keys have different ETags.It is safe to compare an ETag with an earlier one for the same key to detect a change. It is not safe to use it for deduplication.

Quotas and limits

On top of those, maxSize is the per-namespace quota. It is measured against the namespace as it will be after the write, so replacing an object does not charge for the copy it replaces.
Filling a quota is a RESOURCE_LIMIT_EXCEEDED refusal, never a truncated write.
Bytes moved are metered as storage_bytes, split into reads and writes by the operation field on each usage record.

Writes are atomic

Writes are staged under $TINY_DATA_DIR/storage/.staging and renamed into place, so a reader sees either the previous object or the new one — never a partial file. That directory sits beside the environment directories rather than inside a namespace, which keeps a half-written object out of listings and out of quota accounting.

Serving files to a browser

The 32 MiB object limit bounds what your app can write over the private link. What a browser can upload through an app is bounded separately by the reverse proxy: the single-host nginx configuration sets client_max_body_size 20m.
examples/file-vault is a working version of this.

Durability

Objects live on the control-plane host’s disk. They are not replicated, and snapshots are sibling directories on the same filesystem — so a lost host disk loses them along with any SQLite database beside them.Back up $TINY_DATA_DIR as one unit. See Backups.
An S3/R2-backed ObjectStore is the intended replacement, and it is one interface implementation away — nothing an app sees would change. It is not implemented in this repository. retention describes how long deleted objects are kept for recovery. It is recorded on the claim and is not yet enforced by a reaper.