What to do when a deploy of ourdao-frontend is wrong, in the order you should
try things, with an explicit account of what cannot be rolled back.
Nobody should be inventing this procedure while the app is broken. Read the decision table first, pick a lane, and follow it.
| What went wrong | Lane | Time to safe |
|---|---|---|
| A change shipped behind a feature flag and the flagged behaviour is bad | A — flag off | One rebuild + redeploy |
An unflagged commit reached main and the last known-good build is still deployable |
B — redeploy the last good build | One redeploy (2–5 min on Vercel) |
| You know the cause, and a fix is smaller or safer than a revert | C — roll forward | As long as the fix takes |
| Signed writes are misbehaving for everyone, and the cause isn't a frontend bug | D — pause the contract | Seconds, one signature |
| Anything is wrong on-chain (wrong state written, funds moved, a bad contract deploy) | Not reversible from this repo | See What can't be rolled back |
Rule of thumb: A, then B, then C. Prefer the smallest reversible action. Use D only when the contract itself is the problem, because pausing stops every member, not just the affected flow.
- Identify the bad build. The version and commit are shown at the foot of
the app sidebar (
buildLabel()insrc/lib/build-info.ts). Get that SHA — it's what you revert to, and what you name in the incident issue. - Check whether the bad behaviour is already behind a flag. If it is, Lane A is strictly faster and doesn't require a revert commit.
- Decide if writes are affected. If members can sign a transaction that does damage, go to Lane D first, investigate afterwards. Reads can wait; funds and state can't.
- Open an issue. Even for a 5-minute revert. The cause, the SHA, and the fix belong somewhere durable.
The right move when the bad change shipped dark behind a flag. Nothing to revert; the code stays, the behaviour goes.
- Remove the flag name from
NEXT_PUBLIC_FEATURE_FLAGSin the hosting dashboard (or the.envfile for a self-hosted deployment). - Redeploy. This step is not optional —
NEXT_PUBLIC_*values are inlined at build time, so changing the env var alone changes nothing for members. - Confirm: load the affected route and check the old behaviour is back.
Because the flag is off by default and the flagged branch is still fully reviewable in the diff, this is why a risky change ships behind one at all — see FEATURE_FLAGS.md.
The right move when an unflagged commit broke something and the previous deploy
is still good. This is a build-level rollback, not a git revert — you're
redeploying a known-good artefact, which is faster and doesn't touch main.
On Vercel (or any host with deployment history):
- Open the project's Deployments list.
- Find the last deployment whose build you trust (cross-check the SHA against
CHANGELOG.mdand any incident issue). - Promote / redeploy that build to production. This does not run a new build; it re-serves the existing artefact, so it can't pick up new breakage.
- Confirm the build label in the sidebar matches the SHA you intended.
Self-hosted (Docker):
# Redeploy the previous image tag, not a rebuilt one.
docker pull <registry>/ourdao-frontend:<last-good-tag>
docker run -p 3000:3000 --env-file .env.production <registry>/ourdao-frontend:<last-good-tag>Then fix forward properly. A redeployed old build is a mitigation, not a
resolution: the bad commit is still on main and will redeploy on the next push.
Follow up with a Lane C fix — or a git revert commit if that's genuinely the
right answer — so the next deploy isn't the bad build again.
If there is no known-good build to go back to (bad deploy was the first release, or every build since is suspect), go straight to Lane C.
The right move when the cause is understood and the fix is smaller and safer
than a revert — which is most of the time, because a git revert re-applies
every other change that happened to ship in the bad build.
- Branch from
main, fix the cause, add a regression test that fails without the fix. - Add a
## [Unreleased]entry toCHANGELOG.mdif a member could notice. - Ship it as a normal PR — the four CI checks apply, and it goes through review like any other change.
- Link the fix to the incident issue in the PR description.
The frontend is not the right lever for a contract-level incident. The contract
has a pause entrypoint that blocks every state-changing operation for all
members, and the admin page at /admin calls it (useAdminActions().pause() in
src/hooks/dao/writes.ts).
- An admin opens
/adminand clicks Pause, signs, and confirms. - Members see a clear "the DAO is currently paused" error on write actions
(error code 6, mapped in
src/lib/contract-errors.ts); reads keep working. - Fix the root cause in the contract or the frontend.
- Unpause from the same page once it's safe. Confirm the "Contract is Active" badge in the admin panel flips back.
Pause is a real hammer: it stops every member from registering, voting, staking, requesting, and repaying. Don't reach for it because a single flow is misbehaving — use Lane A or B for that.
Being clear about this up front prevents the worst outcome: reaching for a frontend rollback to "undo" something that happened on-chain.
- On-chain state. A transaction that was signed and confirmed is final. If the bad deploy caused a member to sign something wrong, redeploying the app doesn't undo it. You can only add a corrective transaction (which usually means an admin action) or pause until a human has looked.
- A deployed contract. Soroban contracts are immutable — there is no "revert". A bad contract means deploying a new one, and then the frontend has real work to do. See CONTRACT-REDPLOYMENT.md.
- The backend indexer.
ourdao-backendkeeps its own state and history. A frontend rollback doesn't reindex anything, and the UI will happily render stale backend-derived data (loan history, notifications, activity feed) — see the "distinguishable vs. indistinguishable" cases in degradation-matrix.md. - A user's cached bundle. A member with the PWA installed may keep a
service-worker-cached shell. The service worker (
public/sw.js) is network-first, so a member who reloads gets the new build; one who is fully offline keeps the old one until they reconnect. There's no forced cache-busting step here — that's a known gap, not something to improvise during an incident.
- Land the Lane C fix (or a
git revert) somainis not still broken. - Update the incident issue with: the bad SHA, the lane you took, the fix PR, and
the
CHANGELOG.mdentry if a member noticed. - If the runbook was wrong or incomplete at any point, fix this file in the same PR. A runbook that lied under pressure is worse than no runbook.