Skip to content

Checking access...

Canister Wiring After Reinstall

Last Updated: 2026-05-12 Audience: Engineers who run dfx canister install --mode reinstall on any backend canister, plus anyone deploying a fresh prod canister via Canister Production ActivationEstimated time: ~10 min per environment (all calls idempotent)

Overview

After any canister reinstall (staging reset, production go-live, emergency redeploy), the inter-canister dependency mesh must be reconfigured. Reinstall wipes stable memory — every Option<Principal> config field reverts to None, breaking gated methods silently until the wiring is re-applied.

This runbook lists every set_* call needed to restore the mesh, organised by purpose. Order doesn't matter (no circular deps). Each call is idempotent — safe to re-run.

Reinstall vs upgrade: --mode upgrade preserves stable memory; the wiring survives. You only need this runbook after --mode reinstall or after a fresh dfx canister install against a newly-created canister.

When to use this runbook

  • After running scripts/reset-staging-and-seed.sh (staging reset)
  • After fresh prod canister creation per Canister Production Activation Step 5
  • After an emergency redeploy where --mode upgrade failed and you had to --mode reinstall

Environment setup

Substitute these per environment before running the config calls. Production values are populated where prod canisters exist; staging-only or not-yet-created backends are marked TBD.

Staging

bash
OB_PRINCIPAL="ilm6d-l7jrq-aargc-ngpro-5hlx7-sjn5q-d6kjv-a3hen-iwayd-dgszc-uqe"
USER_SERVICE="j4rvr-3aaaa-aaaao-qkvfq-cai"
MEMBERSHIP="z2upb-xqaaa-aaaah-qqjta-cai"
DOM_TOKEN="njo7k-3qaaa-aaaau-aed4a-cai"
DAO_ADMIN="xe7g5-4aaaa-aaaao-a7byq-cai"
IDENTITY_GW="akngw-faaaa-aaaal-qsyyq-cai"
GOVERNANCE="ayio2-biaaa-aaaas-qeb7a-cai"
TREASURY="zri6y-dqaaa-aaaai-atrfa-cai"
BLOG="dsqvo-niaaa-aaaao-a6ynq-cai"
OTTER_CAMP="hjfzw-gyaaa-aaaao-a7fjq-cai"

Production

bash
# OB_PRINCIPAL is the prod oracle-bridge signer (PEM at ~/.config/oracle-bridge-prod/).
# Full principal pulled from memory project_oracle_bridge_prod_pem — verify
# with a gated-call error before relying on it (see "How to derive
# OB_PRINCIPAL" below).
OB_PRINCIPAL="<abhcq-...-2qe>"   # TBD — verify before use

USER_SERVICE="6iu47-dyaaa-aaaaf-qgeza-cai"
MEMBERSHIP="tx4wx-iqaaa-aaaah-avegq-cai"
DOM_TOKEN="ybuho-zyaaa-aaaal-qwsfq-cai"
IDENTITY_GW="mwutv-4iaaa-aaaac-behda-cai"
GOVERNANCE="powzt-dyaaa-aaaaj-qrqra-cai"
TREASURY="m7xyj-kaaaa-aaaac-behcq-cai"
BLOG="2ty2p-5yaaa-aaaap-qudcq-cai"

# Not yet created in production (staging-only as of 2026-05-12)
# DAO_ADMIN="TBD"
# AIRDROP="TBD"
# OTTER_CAMP="TBD"
# EDUCATION="TBD"
# MARKETPLACE="TBD"
# PROOF_NFTS="TBD"

Production canisters that are staging-only get created at OVH cutover (BL-519) or earlier per the prod cutover plan.

Oracle-bridge principal wiring (5 canisters)

These canisters gate update methods on require_oracle_bridge(caller). The oracle-bridge Node.js process signs IC calls as $OB_PRINCIPAL. Each canister stores it as Option<Principal> and must be re-set after reinstall.

bash
dfx canister call $IDENTITY_GW set_oracle_bridge          "(principal \"$OB_PRINCIPAL\")" --network ic
dfx canister call $GOVERNANCE   set_oracle_bridge          "(principal \"$OB_PRINCIPAL\")" --network ic
dfx canister call $TREASURY     set_oracle_bridge_principal "(principal \"$OB_PRINCIPAL\")" --network ic  # ⚠️ different method name
dfx canister call $MEMBERSHIP   set_oracle_bridge          "(principal \"$OB_PRINCIPAL\")" --network ic
dfx canister call $BLOG         set_oracle_bridge          "(principal \"$OB_PRINCIPAL\")" --network ic

Note treasury uses a different method name (set_oracle_bridge_principal) than the others. Easy to miss.

Cross-canister references (6 calls)

These canisters need to know other canisters' IDs to make inter-canister calls.

bash
dfx canister call $IDENTITY_GW  set_user_service           "(principal \"$USER_SERVICE\")" --network ic
dfx canister call $GOVERNANCE   set_membership_canister     "(principal \"$MEMBERSHIP\")"   --network ic
dfx canister call $TREASURY     set_dom_token_canister      "(principal \"$DOM_TOKEN\")"    --network ic
dfx canister call $TREASURY     set_membership_canister     "(principal \"$MEMBERSHIP\")"   --network ic
dfx canister call $USER_SERVICE set_membership_canister     "(principal \"$MEMBERSHIP\")"   --network ic
dfx canister call $USER_SERVICE set_dao_admin_canister      "(principal \"$DAO_ADMIN\")"    --network ic

Init-arg-only canisters (wired at install time, NOT post-install)

These canisters take their config as #[init] arguments — there's no post-install setter. The wiring happens during dfx canister install, not after.

bash
# user-service: controllers Vec<Principal> (application-level, separate from IC-level controllers)
# Staging: pass (vec {}) for dev-fallback "everyone = controller"
# Production: pass (vec { principal "$OB_PRINCIPAL"; principal "<coby>"; principal "<graydon>" })
dfx canister install $USER_SERVICE --mode reinstall \
  --wasm user_service.wasm \
  --argument '(vec { principal "..." })'

# airdrop: full InitConfig record
dfx canister install $AIRDROP --mode reinstall \
  --wasm airdrop.wasm \
  --argument '(record {
    pool_balance_e8s = 10_000_000_000_000_000 : nat64;
    amount_per_member_e8s = 100_000_000_000_000 : nat64;
    membership_canister = principal "...";
    dom_token_canister = principal "...";
    controllers = vec { principal "..." }
  })'

# otter-camp: admin = msg_caller at init; dom_token_canister as Option<Principal>
# Admin auto-set to whoever calls install. Pass dom-token for convenience.
dfx canister install $OTTER_CAMP --mode reinstall \
  --wasm otter_camp.wasm \
  --argument '(opt principal "...")'

Canisters with NO post-install config needed

These reinstall cleanly with default or no-arg init:

CanisterInit signatureNotes
dao-admininit(controllers: Option<Vec<Principal>>)(null) → None, dev-fallback on is_controller
bloginit()No args
proof-nfts(no #[init])No args
education(no #[init])No args
marketplace(no #[init])No args
treasuryinit()No args (wired via set_* post-install)
membershipinit()No args (wired via set_* post-install)
governanceinit(membership_canister: Option<Principal>)(null) → None, wired via set_membership_canister post-install
identity-gatewayinit(controllers: Option<Vec<Principal>>)(null) → None, wired via set_* post-install

DOM token — NEVER reinstall on staging or production

Canister IDs njo7k-3qaaa-aaaau-aed4a-cai (staging) / ybuho-zyaaa-aaaal-qwsfq-cai (production). Token supply + holder balances are irreplaceable. --mode upgrade only, never --mode reinstall.

If a reinstall is ever necessary (e.g., catastrophic stable-memory corruption), the supply + ledger state must be export-imported via icrc1_* calls before the reinstall — out of scope for this runbook.

Dependency graph

oracle-bridge (off-chain, Node.js)
  signs as → $OB_PRINCIPAL
  calls → identity-gateway (link-ii, verify-self-custody)
         → user-service (get_password_hash, register_username_for_user, ...)
         → governance (create_proposal, cast_vote)
         → treasury (propose_payout, execute_payout)
         → membership (get_membership, mint_membership)
         → blog (list_posts, create_post, publish, ...)

identity-gateway
  calls → user-service (link_ii_principal, get_user_by_principal)

governance
  calls → membership (is_member, get_membership)

treasury
  calls → dom-token (icrc1_transfer)
  calls → membership (is_member)

user-service
  calls → membership (get_membership)
  calls → dao-admin (contact_exists_by_user_id)

airdrop
  calls → membership (get_membership, get_activated_at)
  calls → dom-token (icrc1_transfer)

otter-camp
  calls → dom-token (icrc1_burn)

How to derive OB_PRINCIPAL (when memory drifts)

Do NOT use dfx identity import on the oracle-bridge PEM. The same secp256k1 PEM yields different principals from dfx vs Node agent-js (encoding difference, discovered 2026-04-15 staging-reset).

Canonical method (works for any environment):

  1. Trigger any oracle-bridge API endpoint that hits a require_oracle_bridge-gated canister method (e.g., POST /api/identity/link-ii against identity-gateway).
  2. The canister will reject with: Unauthorized: caller <PRINCIPAL> is not the configured oracle-bridge principal
  3. That <PRINCIPAL> is the live oracle-bridge signing identity. Use it verbatim.

For production: trigger the gated call once after the first deploy, record the principal, then use it for all subsequent config calls.

Automation

The staging reset script at scripts/reset-staging-and-seed.sh (Phase 2b) automates all of the above for staging. For production go-live there are two options:

  1. Adapt Phase 2b — swap canister IDs to production values and run once after initial deploy.
  2. BL-174 pattern — integrate set_oracle_bridge and friends into each canister's deploy-staging.yml / deploy-production.yml via the reusable workflow's post_deploy_command input (CLAUDE.md "New Repo Checklist" §5). Reference impls: user-service/.github/workflows/deploy.yml, blog/.github/workflows/deploy-staging.yml.

Option 2 is the long-term solution; option 1 is the day-one expedient. Most canisters that take set_oracle_bridge have already migrated to option 2 for staging deploys — the prod side is still option 1 until each canister's deploy-production.yml follows suit.

Failure symptoms if wiring is missing

Missing configSymptomUser-visible
identity-gateway.oracle_bridge = NonePOST /api/identity/link-ii → 400 "oracle-bridge principal not configured""Failed to link Internet Identity" on UI
identity-gateway.user_service = NonePOST /api/identity/link-ii → 400 "User service not configured"Same UI error
governance.oracle_bridge = NonePOST /api/governance/proposals → 500 "Unauthorized""Failed to create proposal"
governance.membership_canister = NoneVoting + proposal creation fails (no membership check possible)"Could not verify membership"
treasury.oracle_bridge_principal = NonePayout calls fail"Treasury service unavailable"
user-service.controllers = [] + not empty-fallbackAll oracle-bridge → user-service calls rejectedLogin fails with "Invalid email or password"
otter-camp.admin = Noneset_dom_token_canister and campaign management fail"Admin not configured"

Changelog

  • 2026-05-12 — Promoted from hello-world-workspace/bmad-artifacts/runbooks/canister-wiring-manifest.md to operator-facing docs per BL-528. Production env vars populated from current canister_ids.json + memory; DAO_ADMIN/AIRDROP/OTTER_CAMP/EDUCATION/MARKETPLACE/PROOF_NFTS prod still TBD (staging-only).
  • 2026-04-15 — Initial manifest created during staging reset. Every config call verified end-to-end. Oracle-bridge principal discrepancy (dfx vs agent-js) discovered and documented.

Hello World Co-Op DAO