Flag reconciler backfill runbook
Script: console/scripts/flag_reconciler_backfill.py
Issue: #2012
Design: [PR #2005](https://github.com/raxx-app/TradeMasterAPI/pull/2005) — docs/architecture/flag-reconciler-bidirectional-sync-2026-05-13.md section 9.2
Epic: #798
Last updated: 2026-05-21 UTC
Purpose
Before the flag reconciler (Card 2) enforces drift detection, every existing
FLAG_* Heroku config var must have a corresponding row in
console_flag_promotions with synced=true.
Without this backfill, the reconciler's kill-switch (Card 4) would see all
64 pre-existing rows as synced=false and interpret them as drift — disabling
the entire promotion UI.
This script reads all 4 Heroku apps, diffs against console_flag_promotions,
and creates or updates rows for any untracked FLAG_* vars.
CRITICAL ordering constraint: Card 4 (kill-switch enforcement) MUST NOT be deployed until this script has been run and confirmed zero unexpected pending-state rows.
Prerequisites
- Migration
0078has been applied on the target DB (console_flag_promotionshas thesynced,last_heroku_value,last_synced_at,drift_detected_at,drift_reason,sync_sourcecolumns). DATABASE_URLis set to the target console DB (staging or prod).HEROKU_API_TOKENis set to a read-scoped Heroku token. Read from vault:
HEROKU_API_TOKEN=<read from vault at HEROKU_API_TOKEN path>
- Python dependencies are installed:
pip install requests sqlalchemy
Procedure
Step 1 — dry run (always first)
cd console/
DATABASE_URL="<target>" HEROKU_API_TOKEN="<token>" \
python scripts/flag_reconciler_backfill.py --dry-run
Review the JSON output. For each app, verify:
- flags_found matches your expectation (based on the comment in issue #2012:
~5 on raxx-api-prod, ~53 on raxx-console-prod, ~38 on raxx-api-staging,
~76 on raxx-console-staging).
- planned_inserts covers flags that you know are live on Heroku but absent
from console_flag_promotions.
- planned_updates covers rows where synced=false (all pre-migration-0078
rows default to false).
- skipped_existing is the count of flags already confirmed synced.
- No error fields in any app block.
Step 2 — apply
Once the dry-run output looks correct:
cd console/
DATABASE_URL="<target>" HEROKU_API_TOKEN="<token>" \
python scripts/flag_reconciler_backfill.py --apply --confirm
The --confirm flag is required as a defense-in-depth guard against
accidental writes.
Step 3 — verify
After --apply, run --dry-run again to confirm idempotency:
DATABASE_URL="<target>" HEROKU_API_TOKEN="<token>" \
python scripts/flag_reconciler_backfill.py --dry-run
Expected output: planned_inserts=0, planned_updates=0 for all apps.
All previously-untracked flags are now skipped_existing.
Optionally query the DB directly:
SELECT count(*) FROM console_flag_promotions
WHERE synced = false AND state = 'deployed';
Expected: 0 rows.
Expected output format
The script emits JSON to stdout and log lines to stderr.
{
"mode": "DRY-RUN",
"run_at_utc": "2026-05-21T12:00:00+00:00",
"apps": [
{
"app": "raxx-console-staging",
"flags_found": 76,
"planned_inserts": 24,
"planned_updates": 52,
"skipped_existing": 0,
"applied_inserts": null,
"applied_updates": null,
"inserts": [...],
"updates": [...],
"skips": [],
"error": null
}
],
"totals": {
"flags_found": 172,
"planned_inserts": 50,
"planned_updates": 122,
"skipped_existing": 0
}
}
After --apply, the mode field is "APPLY" and applied_inserts /
applied_updates replace the null values.
What to do if the script exits non-zero
Exit code 1 means one or more Heroku API fetch calls failed. The script logs the failing app name and error before exiting.
Steps:
1. Check the log line for fetch failed for <app>: ....
2. Verify HEROKU_API_TOKEN is valid and has read scope on that app.
3. Re-run the script; it is idempotent — any apps that succeeded in the prior
run will have skipped_existing > 0 and will produce no new writes.
4. If the error persists, check Heroku status at
https://status.heroku.com and retry.
Rollback procedure
The script only creates rows in console_flag_promotions with
state='deployed' and created_by='system_backfill'. To undo:
DELETE FROM console_flag_promotions
WHERE created_by = 'system_backfill'
AND sync_source = 'heroku_cli_backfill'
AND state = 'deployed';
This is destructive — only do this if the backfill produced incorrect rows and you have reviewed the impact on the reconciler.
Non-goals
- This script does NOT write to Heroku config-vars (read-only Heroku API).
- This script does NOT run automatically during
alembic upgrade head. - This script does NOT enforce the drift kill-switch (that is Card 4).
- This script does NOT create new feature flags — it backfills tracking rows for flags already live on Heroku.