# Storage (/product/infrastructure/storage)



## One backend per tenant

Every tenant is on Neon Object Storage. Its buckets live in its own Neon project and fork copy-on-write with its database branch, so a preview or restore branch sees a consistent snapshot of the rows and the files they reference. Storage is provisioned inside project creation and only in a region that carries it; a project in a region without it fails provisioning rather than activating, since a serving store without storage would fail on its own storefront.

A tenant has two buckets, `private` and `public`. Their credentials are minted once at provision time and stored in the tenant's own database, the non-secret half in settings and the keys in a server-only secrets row, sealed under a key the database does not hold. No storage credential lives in an environment variable.

## Keys

Every object lives at `<logical>/<path>`. The logical segment is a soft directory, such as branding, logos, visit photos or agent uploads, and a closed set maps each name to the public or the private bucket. An unknown name is refused rather than defaulted, because a default would land the object in the public bucket. The path is built server-side from an entity id and a filename.

Keys are tenant-relative. Nothing in a key names the tenant, because the credential the client was built from already does. That makes cloning a tenant's objects a same-key mirror.

## Uploads and reads

Uploads go direct to the bucket. The browser asks a server action for a presigned PUT URL bound to the file's type and size, then PUTs the bytes itself, bypassing the server's body limit. The browser never fetches object bytes to preview: media renders through element loads, text through a capped server-side read.

A private object is served through a short-lived presigned GET URL. A download URL forces an attachment disposition, so a cross-origin file saves rather than opens.

## Public URLs

A stored public URL is usually a bare key, independent of environment and branch, resolved to a full URL at the render boundary against the tenant's public bucket URL. The server resolves it in the query layer; the client reads the tenant's public storage URL from the tenant provider, since a per-tenant value cannot be a build-time constant. One wildcard image host covers every tenant, so a new tenant needs no configuration.

## Isolation

Across tenants the boundary is hard: each tenant's buckets live in its own project with its own credential, and a client cannot forge a credential it never holds. Within a tenant, a private upload is app-layer only today; per-user ownership of uploads is outstanding.

## Mirroring a captured catalog

Products captured from another store carry absolute URLs to that vendor's CDN. An operations task downloads each distinct image, re-encodes it to WebP, uploads it under the products prefix and repoints the product at the key, keeping the origin URL so the mirror is traceable. Re-encoding is the point: vendor CDNs publish photography masters, and one catalog went from hundreds of megabytes to tens.
