⚠️ Alpha · testnets only. ChainRender is in active alpha and deployed on testnets only — Ethereum Sepolia, the Robinhood Chain Testnet, and the Circle Arc Testnet — not on any mainnet. Contracts, addresses, and APIs may change. Do not use with real funds or production assets.
Guides

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