Rising Tide Developers
Storage

Storage

Where a smart product's rendered artifacts live, how an edition owner writes a permanent copy under their own key, and what the protocol will and will not sign.

Rising Tide stores a smart product's rendered artifacts in two places with two different jobs, and the difference is who signs.

  • The landing store holds every render. The protocol writes it, it is fast, it is what apps and wallets read day to day, and it is not permanent.
  • A destination holds a permanent copy. The protocol cannot write there. Only the edition owner can, signing with a key that is theirs.

That split is deliberate. The protocol used to hold one storage key and spend it on everybody's behalf; a permanent write is now the owner's act, made with the owner's key, and the protocol holds no storage key at all.

Saved, and Saved As

Two words, and they are not synonyms.

Written byLivesReversible
Savedthe protocol, on every renderthe landing storeyes - a re-render replaces it
Saved Asthe edition owner, from their own sessiona destinationno - permanent, by construction

Every render is saved. A create never fails because a destination was full, out of credits or refusing, because a create never touches a destination.

creator publishes
product enters PUBLISHED
edition is collected
visible immediately
render lands
saved, served from the landing store
owner runs Save As
signed by the owner's key
detach
the chain points at the permanent copy

Providers

ProviderRole
Vercel BlobThe landing store. Every render, every serving URL. Not permanent
ArweaveThe one destination available today. Owner-signed, permanent
In-memoryA no-op for credential-free local development. Never a landing store outside tests

DEFAULT_STORAGE_PROVIDER names the landing provider. Naming a destination there is refused by name at startup rather than reported as a missing provider, because it is not a misconfiguration: it is a caller asking the protocol to sign, and the answer is no.

Destinations are plural by design even though there is one today. Xtrata (Stacks inscriptions) and Bitcoin Ordinals are the candidates, and each prices differently - a byte allowance is an Arweave fact, not a storage fact. The test for whether something belongs under a provider is unchanged: if a fact only holds for one provider, it lives under that provider.

Archival images

Every render also leaves PNG images in the landing store: a portrait, and for the products that carry them a square or landscape, plus a 2400 x 1260 social-card image (IMAGE_OG). They are what a grid or a link preview should draw, because the live SVG thumbnail renders on demand and carries its fonts inline, while a PNG has no font to load, is about a third of the size and never changes once written. Read them in one batch with GET /api/product-images: it answers the portrait render, else square, else landscape, and prefer=og asks for the social card alone. An id with no render yet is simply absent, which is how you tell still rendering from broken. These are saved, not permanent: the landing store is ours, and only a Save As writes a permanent copy.

Reading how much room an owner has

Before offering Save As, ask what the owner's key can hold.

const { data } = await rtClient.users.getStorage();

data.enabled;                        // has a storage wallet
data.storageAddress;                 // the key that will sign
data.allowance.remainingFreeBytes;   // null means unreadable, NOT zero
data.allowance.aboutPages;           // the same number, in something a person can read
data.allowance.perItemCapBytes;      // one item may not exceed this
data.credits.winc;                   // bought room, in the destination's own unit
data.savedUnwritten.count;           // owned editions with no permanent copy yet

GET /api/users/me/storage is the endpoint; see Users. Null is not zero. Every field degrades to null when the destination cannot be reached, and rendering that as "0 bytes left" tells an owner their room is gone when the truth is that nobody asked successfully. Say unknown.

Running a Save As

Three legs, all owner-gated. The protocol plans and verifies; the bytes are signed and uploaded by the owner's wallet in their own session.

  1. planSaveAs returns the items to write, each with the landing URL to read the bytes from, its size and its tags, plus the key that must sign and the per-item cap. Items an earlier run already wrote under the same key come back with their id, which is how a resumed run skips them.
  2. postSaveAsReceipts reports what was uploaded. Each receipt is fetched back from a named cache, checked for byte length and hash against the saved copy, and checked for its signer once the gateway has indexed it. Nothing is recorded on the client's word for it. A 422 says whether the refusal is transient.
  3. postSaveAsManifest takes the manifest, verifies it the way a detach would, records it as the edition's permanent manifest, and enqueues the detach.

@rising-tide/ui/storage ships this flow as SaveAsButton and useAutoSaveAs, so an app that wants the standard behaviour mounts a component rather than calling three routes.

Resolving the manifest URL

A manifest is the JSON document an edition's on-chain token-uri resolves to after detach. It carries the edition's metadata plus an image URL and the destination's own content id, so recovery never depends on a hostname.

Before detach, token-uri points at the protocol's metadata route, which generates a SIP-016 document live and serves the saved artifact's URL as its image. A saved edition is a complete, readable edition; it simply depends on us.

The manifest is verified complete and readable before anything is written on chain: a permanent document that would be broken forever is refused rather than pinned.

Detachability - owner-controlled permanent storage

Permanent storage answers where does the data live? Detachability answers who gets the final say, and can the protocol be cut out of the loop entirely? The detachable capability gives that authority to the edition owner, not the author.

A detachable contract carries a per-edition token-URI override, and get-token-uri short-circuits to it when set. The capability adds two owner-gated functions:

  • set-detached-uri(edition-id, uri) - the current owner pins that edition's token URI to any permanent URL, so the product resolves its metadata without the protocol API and survives the protocol disappearing. Authority is the per-edition owner, kept current across transfers - not the contract owner. No edition is special: the author has no say over #1 or any other edition they do not own.
  • lock-detached(edition-id) - the owner permanently freezes that edition's override. Once locked it can never change again. Only the current owner gets the final, immutable say.

The lock is per edition, so one owner freezing theirs never freezes anyone else's.

What triggers a detach

A completed Save As, and nothing else. Detach means leaving custody, so there is nothing to detach to until a permanent copy exists. A render does not trigger it: the protocol has nowhere to point.

For a custodial account the detach follows the Save As automatically, signed by the protocol on the owner's behalf.

A bring-your-own-wallet owner gets no automatic detach. The protocol will not sign for a key it does not hold, so the job is recorded SKIPPED and the edition reads live with reason manual-detach-required: a verified permanent copy the chain does not point at. The on-chain function is public and owner-gated, and the SDK builds the call - buildSetDetachedUriTransaction (and buildLockDetachedTransaction to freeze it) in @rising-tide/sdk - for the owner's wallet to sign. No shipped surface mounts either one, so if your app serves wallet accounts, the detach is a button you build: read the reason, build the transaction, broadcast from the owner's wallet.

Which URL gets pinned, and why the hostname in it is the fragile part, is per-destination - see Arweave.

The states an edition moves through

archival is derived server-side and carried on the instance payloads, so a client renders the truth rather than guessing from what it just did.

StateMeansreason, when present
liveno render has landed yet, or the chain does not point at a copy that existsmanual-detach-required - a wallet account's verified copy, waiting for the owner to detach
renderinga render is in flight
savedthe landing store holds it; nothing permanent yet
archivinga detach is in flight
archived_propagatingthe detach broadcast; the chain has not confirmed it
archivedthe chain points at a permanent copyrefresh-failed - the copy stands and is pointed at; a later refresh of it failed
archive_failedno permanent copy existsrender-failed or detach-failed - which leg
lockedfrozen forever

saved is a healthy resting state, not a queue position. Most editions live there. reason is categorical and public-safe on purpose; the operator-grade error text stays in rt-admin.