Skip to content

Latest commit

 

History

History
628 lines (423 loc) · 40.9 KB

File metadata and controls

628 lines (423 loc) · 40.9 KB

Deployment

This document covers testnet and mainnet deployment. For local development, see local-development.md; the full local stack runs in Docker and needs no secrets.

If an LLM is doing deployment prep, it should execute the scriptable steps here, inspect outputs, and stop for human help when a step needs secret custody, wallet funding, ENS/DNS/account access, dashboard clicks, or product judgment. The companion testnet-prep.md is only the human/operator blocker list.

Mental model

Commonality deploys to three independent targets:

Target What goes there How it's deployed
Ethereum chain Smart contracts ./scripts/deploy-contracts.sh <net>
Render AI services, platform API, and indexer (see Indexer on Render) render.yaml blueprint + git push
IPFS + IPNS + ENS + DNS UI (eight branded SPAs) scripts/deploy-testnet.sh (one-shot per release)

Each target has its own cadence and its own blast radius. Don't try to unify them behind one megascript — the separation is the feature.

Infrastructure-as-code

  • render.yaml is the Render blueprint — but it is generated. The source of truth is render.yaml.template (service structure) plus deployments/<network>.env (non-secret values). A gitignored .env.render file may contain the Render API key for script/API operations; treat it as a secret. After editing either file, regenerate and commit:

    node scripts/generate-render-yaml.mjs          # defaults to deployments/base-sepolia.env
    node scripts/generate-render-yaml.mjs deployments/mainnet.env   # for mainnet

    Do not edit render.yaml directly — your changes will be overwritten. Configure only secrets through the Render dashboard (the sync: false entries that remain after generation).

  • docker-compose.yml is the source of truth for local.

  • deployments/<network>.env is the source of truth for deployed contract addresses and other non-secret deployment values. The deploy script writes it and you commit it; generate-render-yaml.mjs reads it to fill in render.yaml.

Docker and lockfile invariants

Local Docker builds and Render service builds should use the same install inputs:

  • Service Dockerfiles use the pinned node:24.14.1-alpine image and pin npm to 11.16.0 inside the image. Do not switch back to a floating Node/npm toolchain unless local Docker and Render are changed together.
  • Workspace service Dockerfiles copy every workspace package.json before npm ci. This makes Docker installs validate the normal full-workspace lockfile instead of depending on hand-shaped nested per-workspace lockfile entries.
  • Direct workspace viem dependencies are intentionally aligned to exact 2.53.1. If a workspace needs a different viem, treat that as a deliberate dependency split and verify Docker npm ci behavior before committing.
  • It is OK to run a normal root npm install; the resulting package-lock.json should be committed and should not require manual lockfile edits.

No Terraform, no Kubernetes, no Pulumi. Resist upgrading until you have a concrete reason.


One-time setup

1. Create service and operator secret files

Generate deployment wallets first:

node scripts/generate-wallets.mjs

This creates/updates two gitignored files:

  • .env.secrets — generated service hot keys plus finder trust secrets. These are the secrets consumed by local/deployed services.
  • deployments/operator-addresses.env — public wallet addresses, x402 payment recipient addresses, CHANNEL_VERIFIER_TRUSTED_SIGNER_ADDRESS, RECURRING_PLEDGE_SCHEDULER_ADDRESS, and UI default-trust env vars.

Move any operator-only secrets to ~/.secrets/commonality/operator.env (or set COMMONALITY_OPERATOR_SECRETS_FILE to another path). Use .env.operator-secrets.example as the reference. This file holds maintenance/setup secrets only: DEPLOYER_PRIVATE_KEY, ENS_OWNER_PRIVATE_KEY, PINATA_JWT, IPNS_PRIVATE_KEY_*, Cloudflare DNS tokens, and Render API keys.

Then fill the remaining non-generated service values in .env.secrets (use .env.secrets.example as the reference):

  • OPENROUTER_API_KEY — LLM access

  • VITE_WALLETCONNECT_PROJECT_ID — from cloud.walletconnect.com

  • RPC provider URLs, especially BASE_SEPOLIA_RPC_URL

  • noninflammatory attestation policy. For the first testnet, upload seed statements with npm exec --workspace=fake-data-generation tsx prepareSeedStatements.ts -- --upload, then run ./scripts/setup-testnet-ai-policy.mjs. The setup script reads the approved noninflammatory-civility-topic seed statement's uploaded CID from fake-data-generation/output/seed-statements.uploads.json; pass --alignment-topic-statement-cid=<CID> only to override it. This configures both the stateless content-attester fallback and the us-politics beat-memory/beat-agent rehearsal; add --x-api-bearer-token=<token> or set X_API_BEARER_TOKEN for Twitter/X ingestion. Review the generated BEAT_MEMORY_BEAT_DEFINITION_JSON before public use.

  • deployed service/UI URLs once chosen Put operator-only values in ~/.secrets/commonality/operator.env:

  • DEPLOYER_PRIVATE_KEY — used by Hardhat deploy/funding scripts; keep small gas balance only

  • ENS_OWNER_PRIVATE_KEY — ENS setup only; move fully cold when possible

  • PINATA_JWT — for IPFS uploads

  • IPNS_PRIVATE_KEY_TESTNET_* (one per UI subdomain) — generated all at once with ./scripts/setup-testnet-naming.sh (or one by one with ./scripts/setup-ipns-key.sh)

  • Optional Cloudflare DNS automation: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID

  • Optional Render automation: RENDER_API_KEY

.env.secrets, ~/.secrets/commonality/operator.env, and deployments/operator-addresses.env are gitignored/untracked secret material. Never commit secrets.

2. Fund Base Sepolia operational wallets

The human/operator only needs to use a Base Sepolia faucet for DEPLOYER_ADDRESS in deployments/operator-addresses.env. The deployer needs ETH for contract deployment anyway, and the distribution script can use DEPLOYER_PRIVATE_KEY from the operator secrets file to fund the other transaction-sending wallets, including RECURRING_PLEDGE_SCHEDULER_ADDRESS for permissionless standing-pledge execution pokes and ALIGNMENT_TRUST_BOOTSTRAP_ADDRESS for CauseStarter trust writes.

After the faucet transfer lands, inspect the distribution plan:

node scripts/fund-base-sepolia-wallets.mjs --amount 0.005 --dry-run

If the script reports that the deployer balance is sufficient, send the transfers:

node scripts/fund-base-sepolia-wallets.mjs --amount 0.005 --yes

The script refuses to run if the funder cannot cover the per-wallet transfers, estimated transfer gas, and its reserve balance. By default it sends 0.005 ETH to each operational wallet and leaves 0.02 ETH in the deployer; adjust with --amount or --reserve if needed. If you prefer a separate faucet-funded distribution wallet, set FUNDER_PRIVATE_KEY and pass/use that instead; otherwise the deployer is the intended funder.

3. Create accounts

  • Render — one account is fine for both testnet and prod (we'll use separate blueprints per network).
  • Pinata — free tier covers a few CIDs.
  • ENS — use commonality.eth on mainnet L1. We use only mainnet ENS: even for testnet UI deploys, the ENS name lives on L1, because eth.limo (and most gateways) only resolve mainnet ENS. The dapp's contract network is independent.
  • DNS provider for commonality.works — prefer Cloudflare so setup can be API-driven; otherwise any provider that supports TXT and CNAME records works.
  • RPC provider (Alchemy or Infura) — the public endpoints in hardhat.config.cjs work for light use, but paid endpoints are worth it for the indexer.

Deploying a testnet release (happy path)

Step 0: Run release-candidate validation

The verifier CLI is installed by this repository's normal npm install as the @adamspitz/verifier dev dependency. Run verifier commands through the npm scripts or with npx verifier-run; see verifier/README.md if the CLI is unavailable.

Before changing public testnet state, refresh the verifier checks you intend to claim for this milestone, then refresh the root dashboard:

verifier-run automated.test-full
COMMONALITY_VERIFIER_ALLOW_E2E_STACK=1 verifier-run artifact.ipfs-domain-smoke
COMMONALITY_VERIFIER_ALLOW_DESTRUCTIVE=1 verifier-run stack.fresh-seeded
COMMONALITY_VERIFIER_ALLOW_RESTART=1 verifier-run stack.restart-consistency
npm run verifier:root

The old verifier:release-candidate supervisor has been retired; release-candidate is now a readiness label over the specific checks above plus any relevant manual/LLM reports.

The guarded checks may wipe local dev data, restart local services, or run the E2E stack; omit only with an explicit note in the release-candidate report. Missing/stale manual QA synthesis reports should surface as uncertain, not be treated as a pass.

Step 1: Deploy contracts to Base Sepolia

./scripts/deploy-contracts.sh base-sepolia

Keys are read automatically from the operator secrets file — no env pasting needed. The deploy script uses CHANNEL_VERIFIER_TRUSTED_SIGNER_ADDRESS from deployments/operator-addresses.env for the ChannelVerifier trusted signer on non-local networks.

Contract deployment is incremental. ./scripts/deploy-contracts.sh <net> runs hardhat/scripts/deploy-incremental.js, which writes deployments/<net>.contracts-manifest.json alongside deployments/<net>.env. On later runs it reuses a contract when all of these are still true:

  • the manifest fingerprint for that contract matches the current bytecode/ABI/constructor arguments;
  • the address in deployments/<net>.env still has code on-chain;
  • any constructor dependency address is unchanged.

If a dependency changes, downstream contracts whose constructor arguments contain that address are redeployed too. This is conservative rather than magical: contracts are not upgradeable, and the script will not mutate an already-deployed contract into a new implementation. Commit both deployments/<net>.env and deployments/<net>.contracts-manifest.json after a non-local deployment so the next operator/machine can make the same incremental decision.

For non-local deployments, CONTRACT_ADMIN_ADDRESS must be set in deployments/operator-addresses.env before running deploy (it must be distinct from DEPLOYER_ADDRESS; the deployer holds gas money only). The deploy script initiates admin transfer for ChannelVerifier and ChannelRegistry using Ownable2Step, transfers DelegatableNotes ownership directly, then automatically runs the admin-acceptance helper only when a deployment left pending admin transfers.

That helper reads CONTRACT_ADMIN_PRIVATE_KEY from the operator secrets file, calls ChannelVerifier.acceptOwnership() and ChannelRegistry.acceptOwnership() after verifying the key matches CONTRACT_ADMIN_ADDRESS, and verifies DelegatableNotes.owner() already equals the admin address. If you ever need to repair or re-run just the acceptance step, it is idempotent:

./scripts/accept-admin-ownership.sh base-sepolia

This writes contract addresses (including CONTRACT_ADMIN_ADDRESS) to deployments/base-sepolia.env and detailed metadata to hardhat/deployments/base-sepolia-<timestamp>.json.

After deploying content-funding contracts, explicitly configure economics before inviting users:

  • Base Sepolia testnet intentionally uses the project-owned faucetable dev token (USDZZZ) so testers can be funded freely. This is a deliberate testnet-only exception, not a production precedent.
  • Mainnet/production MVP uses USDC. Use only a vetted standard ERC-20 settlement token (no fee-on-transfer/rebasing/callback tokens). Mainnet must not use a project-owned mintable test token.
  • Ensure admin ownership is on Adam's cold admin key or Safe multisig before inviting funds: ChannelVerifier.owner(), ChannelRegistry.owner(), and DelegatableNotes.owner() should all equal CONTRACT_ADMIN_ADDRESS.
  • Set CreatorAssuranceContractFactory.thirdPartyMinPurchase to a meaningful minimum in settlement-token units.
  • Keep thirdPartyMaxDuration bounded (default 7 days, matching the default channel veto window) unless there is a deliberate anti-squatting reason to change it.

Commit deployments/base-sepolia.env to git. Services read addresses from it.

Optionally verify on Basescan:

npx hardhat verify --network base-sepolia <address> <constructor-args>

Step 2: Deploy services to Render

First time only:

  1. Make sure render.yaml is up to date: node scripts/generate-render-yaml.mjs and commit if it changed.

  2. In Render, New → Blueprint, connect to this GitHub repo.

  3. Render reads render.yaml and creates the declared web, private, and worker services plus the indexer Postgres database. This includes the persistent-disk commonality-alignment-trust-bootstrap worker.

  4. For each service, open its dashboard and set the sync: false env vars. Use the helper script to generate a per-service block you can paste into Environment → Add from .env:

    node scripts/generate-render-secrets.mjs

    It reads .env.secrets, deployments/operator-addresses.env, and deployments/base-sepolia.env and prints one block per service. ALIGNMENT_TOPIC_STATEMENT_CID will be missing until you run scripts/setup-testnet-ai-policy.mjs — add it to the attesters service afterward.

For the alignment-trust worker, also open its Render Shell and put ALIGNMENT_TRUST_DENYLISTED_ADDRESS from deployments/operator-addresses.env in /data/denylist.txt. Funding, pause/resume, denylist editing, and verification procedures are in alignment-trust-bootstrap/README.md.

Subsequent deploys: just git push. Render rebuilds automatically (autoDeploy: true).

Do not add Render custom domains for each service. Render is compute; Cloudflare is the public edge/naming layer. Deploy the Cloudflare Worker gateway in cloudflare-service-gateway/ so one hostname routes to the Render *.onrender.com service origins:

  • https://services.testnet.commonality.works/indexer/*
  • https://services.testnet.commonality.works/platform-api/*
  • https://services.testnet.commonality.works/attesters/*
  • https://services.testnet.commonality.works/workers/*

Verify after the Worker is deployed:

curl https://services.testnet.commonality.works/attesters/health
curl https://services.testnet.commonality.works/workers/health
curl https://services.testnet.commonality.works/indexer/graphql
curl https://services.testnet.commonality.works/platform-api/health

The deployed testnet configuration should use the Cloudflare gateway URLs above. Direct Render *.onrender.com URLs are only a debugging fallback for isolating gateway problems; do not bake them into deployments/base-sepolia.env or release UI bundles when the gateway is healthy.

Step 3: Deploy UI to IPFS (+ IPNS + ENS + DNS)

Before building the UI, set VITE_EVENT_CACHE_URL in .env.secrets to the public base URL of the deployed indexer, for example:

VITE_EVENT_CACHE_URL=https://services.testnet.commonality.works/indexer

Run ./scripts/setup-env.sh base-sepolia after wallet generation. It publishes the chain-scoped VITE_DEFAULT_ALIGNMENT_TRUST_ROOT derived from the dedicated bootstrap key into both the domain UI and CauseStarter Vite configuration.

The IPFS UI cannot use the local Vite proxy, so this URL is written into ui/.env by scripts/setup-env.sh and emitted into each domain's runtime config.json by the Vite build. scripts/deploy-ui.sh will stop early if VITE_EVENT_CACHE_URL is missing.

How the naming layer works (testnet)

We pin each UI build to IPFS (Pinata) and point to it through a stable IPNS name (one per UI subdomain). The ENS contenthash and the Cloudflare UI gateway configuration both reference that IPNS name — and stay unchanged forever. Per-deploy work is a single w3name publish (free, no transaction, no DNS change).

This is testnet-only. On mainnet we pin the ENS contenthash directly to immutable IPFS CIDs (see "Mainnet differences" below), so every UI deploy is an on-chain transaction. That gas friction is intentional — it acts as a deploy-control gate, and there's no IPNS private key whose loss could let someone swap the live UI.

              Pinata pin                w3name publish                  Cloudflare UI gateway
   build/   ──────────────▶   CID   ──────────────────▶   IPNS   ─────▶  resolve IPNS → CID
                                                                         fetch/cache /ipfs/{cid}/...
                                                                         serve *.testnet.commonality.works
                                                            ▲
                                                            │ unchanged after one-time setup:
                                                            │   • Worker env var → IPNS name
                                                            │   • ENS contenthash → ipns://<name> (kept for ENS-native resolvers,
                                                            │     but nested eth.limo HTTPS is currently not operational; see note below)

The same IPNS name backs the ENS contenthash and the *.testnet.commonality.works Worker route, but only the .works URL is currently expected to work in browsers.

One-time setup (per environment)

Do this once for testnet, again for mainnet. It costs a few mainnet-ENS transactions then never costs gas again.

  1. Generate local naming material:

    ./scripts/setup-testnet-naming.sh

    This is safe/idempotent and does not touch external services. It creates or reuses one IPNS key per UI, appends missing IPNS_PRIVATE_KEY_TESTNET_* values to the operator secrets file, and writes the public IPNS names to deployments/testnet-ipns.env.

  2. ENS prerequisite — create subdomains/resolvers (mainnet L1). The script detects whether the parent is wrapped and uses the ENS Name Wrapper when required:

    ./scripts/create-ens-subdomains.sh --inspect
    ./scripts/create-ens-subdomains.sh --yes

    This creates/updates testnet.commonality.eth plus commonality, lazygiving, alignment, tally, content-funding, civility, common-sense-majority, and conceptspace under it, each with the ENS public resolver.

    If commonality.works has first been imported into ENS via DNSSEC, the same script can target that ENS name instead:

    ./scripts/create-ens-subdomains.sh --root commonality.works --inspect
    ./scripts/create-ens-subdomains.sh --root commonality.works --yes

    DNSSEC-importing commonality.works itself is still a one-time ENS/DNSSEC setup step outside this script; after import, subdomain creation is ordinary ENS automation. If we decide to use commonality.works as the ENS root for testnet contenthashes, update ensRoot in deployments/testnet-names.json from commonality.eth to commonality.works before the next step.

  3. Set ENS contenthashes automatically after the ENS names/resolvers exist:

    ./scripts/setup-testnet-naming.sh --ens --yes

    This calls scripts/update-ens.sh for each UI and submits one mainnet transaction per UI name, pointing the ENS contenthash at that UI's ipns://<name>.

  4. Deploy the Cloudflare UI gateway. The current production path is Worker proxying, not DNSLink CNAMEs. See cloudflare-ui-gateway/:

    source ~/.secrets/commonality/operator.env
    echo "$PINATA_GATEWAY_KEY" | npx wrangler secret put PINATA_GATEWAY_KEY \
      -c cloudflare-ui-gateway/wrangler.testnet.toml
    npx wrangler deploy -c cloudflare-ui-gateway/wrangler.testnet.toml

    The Worker resolves each configured w3name IPNS value to a CID, caches IPNS→CID in KV, fetches immutable CID paths through IPFS gateways, and caches successful CID responses at Cloudflare.

  5. DNS records: each UI hostname needs a proxied Cloudflare DNS record so the Worker route can receive traffic. The current records are proxied CNAMEs; their target is not semantically important because the Worker intercepts before origin. The old DNSLink TXT records are harmless but no longer the browser-serving mechanism.

After this, alignment.testnet.commonality.works serves whatever the IPNS record currently points at, subject to the Worker's short CID cache TTL.

Known eth.limo limitation for nested testnet subdomains: despite eth.limo's documentation saying nested ENS gateway names should work when the exact subdomain has a resolver and contenthash, https://alignment.testnet.commonality.eth.limo/ currently fails during the TLS handshake with SSL_ERROR_INTERNAL_ERROR_ALERT. We verified on 2026-06-01 that:

  • alignment.testnet.commonality.eth exists, has a resolver, and has a valid contenthash.
  • Its IPNS name resolves to a pinned IPFS CID.
  • Temporarily changing the ENS contenthash from ipns://k51qzi5uqu5dgvr67vrtjpjg7x54uqmfgf8zkv83zyju33eqnff6hrledwxhz1 to direct ipfs://QmZc2MNaAAu4ZFFDGRNj5Z3qArxfi6Wipvb36EoWwEAbPj did not fix the TLS failure after waiting for eth.limo cache/certificate issuance.
  • The contenthash was restored to IPNS afterward.

Do not repeat that mainnet ENS transaction experiment unless there is new evidence that eth.limo changed its nested-subdomain behavior. Treat *.testnet.commonality.works as the public testnet URL for now; the nested *.testnet.commonality.eth.limo URLs are not operational.

Per-deploy (every release)

./scripts/deploy-testnet.sh

This builds each UI for base-sepolia, pins it to Pinata, and publishes a new IPNS revision under the matching key. No on-chain transaction, no DNS change. Total wall time: a couple of minutes for all eight UIs.

To deploy a subset:

DOMAINS="alignment lazygiving" ./scripts/deploy-testnet.sh

To deploy a single UI by hand (for debugging):

./scripts/deploy-ui.sh base-sepolia alignment    # → prints CID
./scripts/publish-ipns.sh IPNS_PRIVATE_KEY_TESTNET_ALIGNMENT <cid>

Visit https://alignment.testnet.commonality.works to verify. Do not use https://alignment.testnet.commonality.eth.limo as the testnet browser smoke target unless the known nested-subdomain TLS limitation above has been resolved.

After the deployed services/UI are reachable, run the configured testnet verifier smoke and refresh the dashboard:

COMMONALITY_VERIFIER_ENABLE_TESTNET_SMOKE=1 \
  COMMONALITY_TESTNET_RPC_URL=https://... \
  COMMONALITY_TESTNET_GRAPHQL_URL=https://... \
  COMMONALITY_TESTNET_APP_URL=https://alignment.testnet.commonality.works \
  verifier-run env.testnet-smoke
npm run verifier:root

Supported testnet host slugs: commonality (default), lazygiving, alignment, tally, content-funding, civility, common-sense-majority, conceptspace. (deploy-testnet.sh still maps lazygiving to the UI build's legacy lazyGiving domain internally.)

Optional eth.limo workaround: one path-hosted uber bundle

If nested ENS subdomains remain unusable on eth.limo, deploy one root IPFS directory containing all eight independently built apps:

./scripts/deploy-testnet-uber-ui.sh

The script:

  1. runs setup-env.sh base-sepolia,
  2. builds all eight IPFS/hash-router UI domains with path-hosted cross-app links,
  3. pins each standalone app bundle and records its CID,
  4. assembles those exact artifacts under /testnet/<slug>/,
  5. pins the root bundle, and
  6. writes deployments/testnet-ui-uber-release.json.

Default cross-app links are relative, so the bundle works when loaded from any root gateway path. To bake absolute links for the intended eth.limo name, run:

UBER_PUBLIC_BASE_URL=https://commonality.eth.limo ./scripts/deploy-testnet-uber-ui.sh

The script also publishes the root CID to IPNS_PRIVATE_KEY_TESTNET_UBER_UI when that key exists. Create the key with the normal naming setup script:

./scripts/setup-testnet-naming.sh

Then set commonality.eth to that uber-bundle IPNS name once:

./scripts/setup-testnet-naming.sh --ens --uber --uber-only --yes

After that, each ./scripts/deploy-testnet-uber-ui.sh run updates the IPNS record without another ENS transaction. If eth.limo or other public gateways fail to resolve the w3name-backed IPNS record, use the printed direct-CID fallback instead:

./scripts/update-ens.sh commonality.eth <uber-root-cid> --network mainnet

That costs one ENS transaction per uber-bundle release but keeps the same path-hosted URL shape. The resulting URLs are hash-routed paths such as:

https://commonality.eth.limo/testnet/alignment/#/
https://commonality.eth.limo/testnet/tally/#/statements

This does not remove the per-app split-off path: each app CID is pinned and recorded separately, so a future alignment.commonality.works or alignment.commonality.eth.limo name can point directly at the same standalone alignment CID/IPNS record. deployments/testnet-ui-uber-release.json records the uber root CID, root IPNS name (when published), and all standalone app CIDs.

Mainnet differences

For mainnet we skip the IPNS layer and pin the ENS contenthash directly to immutable IPFS CIDs. Per-deploy flow per UI:

  1. ./scripts/deploy-ui.sh mainnet <domain> — build, pin, print CID.
  2. ./scripts/update-ens.sh <domain>.commonality.eth <cid> --network mainnet — submit one on-chain transaction setting the contenthash to ipfs://<cid>.

So a full mainnet release is eight on-chain transactions. At typical gas this is roughly $5–20 per release. We accept that cost because:

  • Whoever holds ENS_OWNER_PRIVATE_KEY (which can be a multi-sig) is the only party who can change a live UI — there's no IPNS key file lying around.
  • A contenthash is immutable; no concept of "the IPNS service returned stale data."
  • The transaction record is an auditable deploy log.

The *.commonality.works mainnet subdomains can either (a) keep DNSLink TXT records updated per release to the same CID, or (b) be configured as Cloudflare-proxied CNAMEs to <name>.commonality.eth.limo so they track the ENS contenthash automatically. Option (b) avoids per-release DNS work but requires Cloudflare's CNAME-flattening / TLS termination so the cert is for *.commonality.works. Decide before mainnet.

A dedicated scripts/deploy-mainnet.sh wrapper doesn't exist yet — write it as part of pre-mainnet prep, modeled on deploy-testnet.sh but calling update-ens.sh instead of publish-ipns.sh.

Gateway cache lag

IPNS resolution is cached by gateways (eth.limo, cloudflare-ipfs.com, etc.) for a few minutes after a publish. Users may see the previous CID for that window. This matches how IPFS deploys cached today and is not specific to IPNS.


Indexer on Render

The Render blueprint now includes both the commonality-indexer web service and a commonality-indexer-db Postgres database.

Set these indexer env vars in the Render dashboard:

  • PONDER_CHAIN: base-sepolia for testnet or mainnet for production
  • PONDER_RPC_URL_84532 or PONDER_RPC_URL_1: RPC URL for the selected chain
  • START_BLOCK: block where the deployed contracts start emitting relevant events
  • All contract addresses from deployments/<network>.env

The blueprint already wires:

  • PONDER_SCRIPT=start so hosted deployments use ponder start
  • DATABASE_URL from the managed Postgres database
  • DATABASE_SCHEMA=commonality_base_sepolia_v4 for the current Base Sepolia deployment. Use a fresh schema only when intentionally abandoning stale indexed data; otherwise keep the schema stable.
  • PONDER_EXPERIMENTAL_DB=platform so normal Render redeploys of a changed Ponder build can reuse the same production schema instead of failing with "previously used by a different Ponder app".
  • PONDER_ETH_GET_LOGS_BLOCK_RANGE=10 for Base Sepolia because the current Alchemy free-tier RPC rejects wider eth_getLogs ranges. If the RPC plan is upgraded, raising this value can make historical backfill faster.
  • For the first Render rehearsal, START_BLOCK is intentionally near the current chain head to avoid free-tier RPC rate limits during backfill. If you need older testnet events, lower START_BLOCK and switch to a fresh Ponder schema (or drop the existing schema) after upgrading RPC capacity.
  • The indexer declares a small persistent disk even though it does not store application data there. This is an intentional Render workaround, not indexer storage: Render disables zero-downtime/rolling deploys for services with disks, which gives Ponder the stop-before-start deployment behavior it needs for the exclusive DATABASE_SCHEMA lock. Do not remove this disk just because /data appears unused unless the indexer has moved to a cleaner singleton-writer deployment model.

Known Render indexer deployment trap: Ponder schema lock

If commonality-indexer is healthy but a manual or automatic Render deploy fails, check the deploy logs for:

MigrationError: Failed to acquire lock on schema "...". A different Ponder app is actively using this schema.

This happens because Render web services deploy with rolling/zero-downtime semantics: the new container starts before the old live container stops. ponder start needs an exclusive Postgres schema lock, so the new indexer cannot initialize while the old indexer is still running against the same DATABASE_SCHEMA.

The intended Render fix is the persistent disk declared on commonality-indexer in render.yaml.template: Render services with disks deploy stop-before-start instead of zero-downtime, avoiding two simultaneous Ponder writers on the same schema. This is admittedly a platform-specific workaround rather than a beautiful architecture knob. The disk is operationally significant even though the indexer does not use /data for app state; do not delete it during cleanup/refactoring unless you replace it with another guaranteed singleton deploy strategy.

If the disk is not attached yet, or if a rehearsal needs an emergency dashboard-green workaround, you can still use a fresh schema:

  1. Pick a fresh schema name in render.yaml.template (DATABASE_SCHEMA=commonality_base_sepolia_v<N+1>).

  2. If abandoning old testnet history is acceptable, bump START_BLOCK / CONTENT_FUNDING_START_BLOCK in deployments/base-sepolia.env near the current chain head so the free-tier RPC does not have to backfill much history.

  3. Regenerate and commit render.yaml:

    node scripts/generate-render-yaml.mjs
    npm run smoke-check
  4. Push and let Render auto-deploy.

Do not treat fresh schemas as the long-term production answer. Before a real production deployment, either configure the indexer so the old process is stopped before the new one starts (if Render supports disabling rolling deploys for this service), or split indexing from serving so the singleton writer can deploy separately from read-only HTTP serving.

For local Docker development, the same image still defaults to PONDER_SCRIPT=dev:no-ui and PONDER_CHAIN=hardhat.


Environment variables

All service-specific env vars are documented in each service's README. Quick pointers:

The bottom of render.yaml lists which vars are secrets (set in Render dashboard) per service.

How env vars flow

.env.secrets              ← you fill in once (gitignored)
deployments/<net>.env     ← deploy script writes (committed)
         │
         ▼ for local dev: scripts/setup-env.sh <network>
         ▼ for Render:    copy-paste into dashboard once per service
         │
service-local .env files  ← each service reads its own

setup-env.sh is only used for local development. On Render, env vars live in the dashboard; use scripts/generate-render-secrets.mjs to generate the per-service blocks for bulk-pasting.


Updating a running deployment

Service code change

  1. Push to master.
  2. Render auto-builds and deploys (per-service, independently).
  3. Check /health endpoint afterward.

For mainnet, consider setting autoDeploy: false per service and triggering manual deploys from tagged commits.

Contract upgrade

Contracts are not upgradeable in this codebase. Redeploying changed contracts means:

  1. ./scripts/deploy-contracts.sh <net> incrementally deploys only contracts whose bytecode/ABI/constructor inputs changed, plus downstream contracts whose constructor inputs now point at new addresses. It writes updated addresses and the deployment manifest.
  2. Commit the updated deployments/<net>.env and deployments/<net>.contracts-manifest.json.
  3. Regenerate and commit render.yaml: node scripts/generate-render-yaml.mjs. This fills in the new contract addresses automatically — no Render dashboard edits needed for addresses.
  4. Redeploy UI (addresses are baked into the bundle): ./scripts/deploy-testnet.sh. The IPNS pointer updates automatically; ENS contenthash does not need a new transaction.
  5. If any indexed contract address changed, old indexer data for that contract set is wrong — wipe the indexer Postgres or use a fresh schema and resync from the new START_BLOCK/contract start block.

This is intentionally high-friction. For testnet it's tolerable; for mainnet, consider adding an audit pass before each redeploy.

UI-only change

./scripts/deploy-testnet.sh

Builds, pins, and publishes new IPNS revisions for all UIs. No ENS or DNS changes needed. Gateway caches may serve the previous CID for a few minutes.


Pre-mainnet checklist

Do not deploy to mainnet until all of these are checked. As a final workflow gate, refresh the relevant release-candidate/testnet checks, write the final QA synthesis report, then run:

npm run verifier:root
npm run verifier:state

The old verifier:full-launch supervisor has been retired; do not interpret an uncertain root result as approval. It means the verifier is missing fresh evidence or a human/LLM sign-off.

Security

  • Professional smart-contract audit passed
  • Generative / invariant testing complete
  • Emergency pause procedures documented
  • Separate wallets for deployer, attester(s), nudger(s), verifier, ENS owner — never reuse keys
  • Private keys stored in a password manager or hardware wallet, not plaintext
  • Mainnet DEPLOYER_PRIVATE_KEY only used for deployment, then retired

Infrastructure

  • Testnet indexer has been running stably for at least a week
  • Paid RPC endpoint (Alchemy/Infura) configured — public endpoints will rate-limit the indexer
  • Render services moved off plan: standard only if load requires it (standard is fine to start)
  • autoDeploy: false on mainnet services; deploy from tagged releases
  • Postgres add-on has automated backups enabled
  • Monitoring: at least Sentry (or similar) on the AI services

Contracts

  • Contracts verified on Basescan
  • deployments/mainnet.env committed
  • ChannelVerifier.trustedVerifier() matches CHANNEL_VERIFIER_TRUSTED_SIGNER_ADDRESS from deployments/operator-addresses.env, and Render platform-api-service uses the matching VERIFIER_PRIVATE_KEY
  • Tenderly or similar alerts set up for unusual contract activity

UI

  • UI builds against mainnet addresses
  • Mainnet ENS subdomain tree exists under commonality.eth, each with the public resolver
  • CIDs pinned to Pinata
  • ENS contenthashes set directly to ipfs://<cid> (immutable; no IPNS on mainnet) — one tx per subdomain per release
  • commonality.works mainnet subdomains configured (either DNSLink TXT updated per release, or Cloudflare-proxied CNAMEs to *.commonality.eth.limo — decide which)
  • scripts/deploy-mainnet.sh written (modeled on deploy-testnet.sh)
  • ENS_OWNER_PRIVATE_KEY for mainnet is in a hardware wallet or multi-sig, not plaintext on disk
  • All eight UIs load via both <name>.commonality.eth.limo and <name>.commonality.works, wallet connection works on each

Sign-off

  • Manual smoke test: create belief, add implication, receive nudge, fund a creator
  • Announcement prepared

Operations

Health checks

  • Attester and nudger services: GET /health
  • Indexer (once deployed): POST /graphql { "query": "{ _meta { block { number } } }" }
  • IPFS (Pinata): visit https://gateway.pinata.cloud/ipfs/<cid>
  • ENS: visit https://<name>.eth.limo

Logs

Render streams logs per service. For anything beyond casual debugging, add Sentry (or similar). Render log retention is limited.

Rollback

  • Services: Render keeps previous Docker images. In the dashboard: Deploys → pick a previous successful deploy → "Rollback to this deploy."
  • UI: Publish a new IPNS revision pointing at the previous CID: ./scripts/publish-ipns.sh IPNS_PRIVATE_KEY_TESTNET_<DOMAIN> <previous-cid>. CIDs are immutable; as long as Pinata still has the old one pinned, rollback is fast (modulo gateway cache lag). No ENS or DNS change needed.
  • Contracts: no rollback. Deploy a new version and update addresses everywhere.

Costs (approximate, mid-2026)

  • Render: 4 services × ~$25/mo standard = $100/mo, plus the Postgres add-on. Can probably drop several to starter ($7/mo) once load is known.
  • Render Postgres for indexer: ~$7–20/mo depending on plan.
  • Pinata: free tier covers a few CIDs; paid starts at ~$20/mo.
  • RPC (Alchemy/Infura): free tier covers light testnet use; production indexer probably needs ~$50/mo.
  • Total rough ballpark: $250–400/mo before mainnet gas.

Known gaps / future work

See TODO.md for the prioritized list. Deployment-relevant items:

  • Indexer operations hardening — the indexer is already in the Render blueprint; keep monitoring normal redeploys for Ponder schema-lock issues and use the Indexer on Render recovery notes if they recur.
  • Second smart-contract audit pass.
  • Monitoring / alerting — only Render's built-in logs today.
  • Staging environment — Render supports preview environments per PR; worth enabling before the project has real users.

Troubleshooting

Symptom Likely cause Fix
"Insufficient funds for gas" Deployer wallet empty Send ETH; for Base Sepolia use the Coinbase faucet or bridge from Ethereum Sepolia
Service builds but crashes on boot Missing env var Check Render logs; compare to .env.example in the service dir
Attester never attests Attester wallet has no ETH, or wrong IMPLICATIONS_CONTRACT_ADDRESS Check wallet balance; compare addresses to deployments/<net>.env
Indexer not syncing Wrong START_BLOCK, wrong RPC URL, schema-lock/deploy issue, or stale deployment values See Indexer on Render and the Ponder schema-lock trap in that section
UI loads but can't read contracts Stale bundle with old addresses Rebuild UI with ./scripts/deploy-ui.sh <network> and update ENS
ENS update transaction reverts ENS_OWNER_PRIVATE_KEY doesn't own the name Verify ownership at app.ens.domains
eth.limo returns 404 / "name not found" ENS subdomain missing or has no resolver set In app.ens.domains, ensure the subdomain exists and has the public resolver set; setContenthash on a name with no resolver silently no-ops
*.commonality.works loads but shows stale content Gateway is caching the old IPNS resolution Wait a few minutes; or force-refresh through cloudflare-ipfs.com/ipns/<name> to confirm IPNS is updated
publish-ipns.sh fails with "sequence too low" Multiple machines published to the same IPNS key out of order Always publish from a single machine; w3name's resolve then increment flow handles ordering automatically as long as you don't race

For anything not here, check Render logs first, then the service's own README.