Per-tenant object storage, the key shape, presigned uploads, and how a public URL is resolved.
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.
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 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.
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.
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.
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.