Skip to content

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 through rose-config.
  • backend/.env.production for the rose-config write. 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: true in 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.