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 by | Lives | Reversible | |
|---|---|---|---|
| Saved | the protocol, on every render | the landing store | yes - a re-render replaces it |
| Saved As | the edition owner, from their own session | a destination | no - 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.
Providers
| Provider | Role |
|---|---|
| Vercel Blob | The landing store. Every render, every serving URL. Not permanent |
| Arweave | The one destination available today. Owner-signed, permanent |
| In-memory | A 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 yetGET /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.
planSaveAsreturns 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.postSaveAsReceiptsreports 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.postSaveAsManifesttakes 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.
| State | Means | reason, when present |
|---|---|---|
live | no render has landed yet, or the chain does not point at a copy that exists | manual-detach-required - a wallet account's verified copy, waiting for the owner to detach |
rendering | a render is in flight | |
saved | the landing store holds it; nothing permanent yet | |
archiving | a detach is in flight | |
archived_propagating | the detach broadcast; the chain has not confirmed it | |
archived | the chain points at a permanent copy | refresh-failed - the copy stands and is pointed at; a later refresh of it failed |
archive_failed | no permanent copy exists | render-failed or detach-failed - which leg |
locked | frozen 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.
Payment Currency
The currencies a payment can settle in on Stacks - sBTC for collecting, STX for creating, the CurrencyCode values and their decimals.
Arweave
The Arweave destination - the owner's key, the free allowance and what it is measured in, buying more room, gateways, and how detach pins an edition to a permanent URL.