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 upgradepreserves stable memory; the wiring survives. You only need this runbook after--mode reinstallor after a freshdfx canister installagainst 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 upgradefailed 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
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
# 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.
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 icNote 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.
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 icInit-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.
# 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:
| Canister | Init signature | Notes |
|---|---|---|
| dao-admin | init(controllers: Option<Vec<Principal>>) | (null) → None, dev-fallback on is_controller |
| blog | init() | No args |
| proof-nfts | (no #[init]) | No args |
| education | (no #[init]) | No args |
| marketplace | (no #[init]) | No args |
| treasury | init() | No args (wired via set_* post-install) |
| membership | init() | No args (wired via set_* post-install) |
| governance | init(membership_canister: Option<Principal>) | (null) → None, wired via set_membership_canister post-install |
| identity-gateway | init(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):
- Trigger any oracle-bridge API endpoint that hits a
require_oracle_bridge-gated canister method (e.g.,POST /api/identity/link-iiagainstidentity-gateway). - The canister will reject with:
Unauthorized: caller <PRINCIPAL> is not the configured oracle-bridge principal - 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:
- Adapt Phase 2b — swap canister IDs to production values and run once after initial deploy.
- BL-174 pattern — integrate
set_oracle_bridgeand friends into each canister'sdeploy-staging.yml/deploy-production.ymlvia the reusable workflow'spost_deploy_commandinput (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 config | Symptom | User-visible |
|---|---|---|
| identity-gateway.oracle_bridge = None | POST /api/identity/link-ii → 400 "oracle-bridge principal not configured" | "Failed to link Internet Identity" on UI |
| identity-gateway.user_service = None | POST /api/identity/link-ii → 400 "User service not configured" | Same UI error |
| governance.oracle_bridge = None | POST /api/governance/proposals → 500 "Unauthorized" | "Failed to create proposal" |
| governance.membership_canister = None | Voting + proposal creation fails (no membership check possible) | "Could not verify membership" |
| treasury.oracle_bridge_principal = None | Payout calls fail | "Treasury service unavailable" |
user-service.controllers = [] + not empty-fallback | All oracle-bridge → user-service calls rejected | Login fails with "Invalid email or password" |
| otter-camp.admin = None | set_dom_token_canister and campaign management fail | "Admin not configured" |
Related Documentation
- Canister Production Activation — references this runbook as Step 5 (init hooks) for fresh prod canister deploys
- Cycles Top-Up — pair with this when a fresh prod canister is also low on cycles
- Canister Unresponsive — diagnose missing-wiring symptoms
Changelog
- 2026-05-12 — Promoted from
hello-world-workspace/bmad-artifacts/runbooks/canister-wiring-manifest.mdto operator-facing docs per BL-528. Production env vars populated from current canister_ids.json + memory;DAO_ADMIN/AIRDROP/OTTER_CAMP/EDUCATION/MARKETPLACE/PROOF_NFTSprod 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.