Restore a client config that was overwritten¶
Use this procedure when a client reports that a setting "changed by itself" or that values disappeared (starter questions, page-specific overrides, CTAs…). It finds who wrote the config and when, then puts back the last good version from the daily snapshots.
config.client_configs holds the client's override only, one row per
(domain, config_slug, config_scope). A backoffice save replaces the whole row, so a
form saved with the wrong values loses everything that was not in it. Every night,
config.client_configs_daily_snapshots copies each row; see
Daily snapshot history.
Prerequisites¶
- Read access to the production Supabase project (Supabase MCP or SQL editor). Its
role is read-only on
config.client_configs: the write goes throughrose-config. backend/.env.productionfor therose-configwrite. Supabase is shared across environments, so every write in this procedure is a production write: get the operator's approval before step 4.- The slug is CLI-editable (
x-cli-editable: truein its schema). If it is not, stop and hand the diff to the operator.
Steps¶
1. Find the write¶
Read the current row and when it last changed:
SELECT enabled, config, updated_at
FROM config.client_configs
WHERE domain = 'acme.com' AND config_slug = 'engagement' AND config_scope = 'website';
Find who saved it in the Supabase logs (edge_logs, a window of a few minutes around
updated_at). Backoffice saves are POST upserts on client_configs?on_conflict=…;
the JWT subject is the user's auth.users.id:
SELECT timestamp, event_message,
log_attributes['request.sb.jwt.authorization.payload.subject'] AS user_id
FROM logs
WHERE source = 'edge_logs'
AND event_message ILIKE 'POST%client_configs?on_conflict%'
ORDER BY timestamp
SELECT u.email, b.display_name
FROM auth.users u LEFT JOIN public.backoffice_users b ON b.user_id = u.id
WHERE u.id = '<user_id>';
2. Pick the snapshot¶
Take the latest snapshot whose source_updated_at is before the bad write, and compare
it with the current row:
SELECT snapshot_date, source_updated_at, enabled, config
FROM config.client_configs_daily_snapshots
WHERE domain = 'acme.com' AND config_slug = 'engagement' AND config_scope = 'website'
ORDER BY snapshot_date DESC
LIMIT 3;
Restoring the snapshot undoes every save since it, not only the bad one. Run the step 1
log query from the snapshot's source_updated_at to now: if any other save touched the
row, or the bad save also held a legitimate change, list them for the operator and
re-apply the ones they keep after the restore.
3. Export the snapshot to a file¶
From backend/, with .env.production loaded (set -a; . ./.env.production; set +a):
.venv/bin/python - <<'EOF' > /tmp/restore.json
import json, os
from supabase import create_client
c = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_ROLE_KEY"])
rows = (c.schema("config").table("client_configs_daily_snapshots").select("config")
.eq("domain", "acme.com").eq("config_slug", "engagement")
.eq("config_scope", "website").eq("snapshot_date", "2026-10-08").execute().data)
assert len(rows) == 1, rows
print(json.dumps(rows[0]["config"], ensure_ascii=False, indent=2))
EOF
4. Write it back¶
Check that updated_at has not moved since step 1, then replace the whole override right
away. rose-config set is an unconditional upsert, so a save landing between the check
and the write would be lost: the verification below rules it out.
rose-config set engagement --site acme.com --env production -f /tmp/restore.json --dry-run
rose-config set engagement --site acme.com --env production -f /tmp/restore.json --yes
The snapshot is the override layer only, so writing it back does not bake in defaults.
If the snapshot's enabled is a boolean that differs from the row's, set it with
rose-config enable|disable <slug> --site <domain> --env production --yes. If the
snapshot's enabled is NULL (inherit the feature default) and the row's is not, stop
and ask the operator: the CLI only writes true or false.
Verification¶
The row matches the snapshot:
SELECT c.updated_at,
c.config = s.config AS config_matches,
c.enabled IS NOT DISTINCT FROM s.enabled AS enabled_matches
FROM config.client_configs c
JOIN config.client_configs_daily_snapshots s
ON s.domain = c.domain AND s.config_slug = c.config_slug
AND s.config_scope = c.config_scope AND s.snapshot_date = '2026-10-08'
WHERE c.domain = 'acme.com' AND c.config_slug = 'engagement';
Rerun the step 1 log query from the check in step 4 to now: your write is the only save.
Open the screen in the backoffice and wait about 15 seconds without touching anything: it shows the restored values and offers no Save. If Save appears on its own, the screen is rewriting the config; do not save, and file a bug.