Guide: Update a Deployed App
ChainRender apps are immutable — you don't change a deployed app, you publish a new version and point your
name at it, through the on-chain SubnameVersionRegistry. Updates are safe by design: timelocked, observable, and
append-only.
These are contract calls. Versioning lives in the on-chain
SubnameVersionRegistry(there's no SDK wrapper yet — you call it directly with viem/ethers). The examples below show the contract functions; wire them with your wallet library of choice.
Publish a new version
// via viem (illustrative) — call the versioned registry directly
await wallet.writeContract({
address: subnameVersionRegistry,
abi: versionRegistryAbi,
functionName: "queueVersion",
args: [
"myapp", // your subname label
newAppHash, // the new version's component hash (already registered)
timelockSeconds, // your name's delay (>= the registry minimum; can only grow)
releaseNote, // an optional bytes32 content-addressed release note (0x0 for none)
],
});
The new version is queued, not instant. It goes live only after your name's timelock elapses — a public,
observable delay. Until then, the current version keeps resolving (no silent swap), and anyone can see the pending
version via pending(labelHash).
How a viewer resolves the current version
Your app's consumers resolve the current version — a free view call:
const currentHash = await client.readContract({
address: subnameVersionRegistry, abi: versionRegistryAbi,
functionName: "resolveCurrent", args: [labelHash], // labelHash = keccak256(label)
});
// then render currentHash the usual way
Or resolve through the name: myapp.chainrender.eth (the ENSIP-10 wildcard resolver reads the registry's current
version for you).
Pinning (for cautious consumers)
A consumer who doesn't want to auto-follow updates pins to a specific version and ignores the pointer entirely:
const pinned = await client.readContract({
address: subnameVersionRegistry, abi: versionRegistryAbi,
functionName: "resolveAt", args: [labelHash, versionIndex], // any historical version, forever
});
Every version ever published stays resolvable at its index — append-only, immutable lineage.
Sealing a name (freeze / renounce)
When your app is stable and you want to guarantee it never changes again, freeze the name — no more versions can be queued, and the current version becomes permanent (Grade C → A for consumers following the name):
await wallet.writeContract({
address: subnameVersionRegistry, abi: versionRegistryAbi,
functionName: "freeze", args: ["myapp"],
});
This is the "upgrade-then-renounce" pattern, standardized: mutable while you stabilize, then immutable forever.
The rules that keep updates safe
- Only the name's controller can queue versions (a stranger can't hijack your name).
- Timelocks can only grow, never shrink — a name can't weaken its own safety after the fact.
- The registry is ownerless — no admin can touch your versions, change the rules, or force an update.
- The version's hash must exist (be registered + enforcer-graded) — you can't point at unregistered bytes.
Cosmetic vs. substantive — don't over-version
Only publish a new version for a substantive change (features, text, actions). A cosmetic change (color, spacing, font) is a render-time theme change — free, no version, no redeploy. See Theme Without Redeploying.
Related
- Versioning & Updates — the model + reasoning.
- Registering a Subname · Content Addressing