-
Notifications
You must be signed in to change notification settings - Fork 0
Restore Runbook
If the current maintainer walked away tomorrow, this is the page a stranger reads to bring Proton Pulse back up on their own account, keys, and infrastructure. Filed as #265.
Read this top to bottom before touching anything. The steps are ordered because Phase 3 (data import) depends on Phase 2 (fresh Supabase project) which depends on Phase 1 (inventory + accounts).
Accounts:
- GitHub account with
ghCLI installed and authenticated. - Supabase account (free tier is enough to start).
- Cloudflare account with the
proton-pulse.comdomain transferred to it (or a new domain if the old one is gone). - Discord account with admin access to a server for webhooks + announcements.
- Optional: Steam API key, SteamGridDB API key, OpenAI API key, VirusTotal API key. All can be obtained fresh for free.
Tools:
-
gh,git,jq,curl,psql,python3,uv,pnpm,supabaseCLI.
| Repo | Contains | Public |
|---|---|---|
mdeguzis/proton-pulse-web |
Main app source, edge fn source, pipeline scripts, CI workflows | Yes |
mdeguzis/proton-pulse-web-staging |
Staging preview target for gh-pages | Yes |
mdeguzis/proton-pulse-web.wiki |
Public wiki (this page lives here) | Yes |
mdeguzis/proton-pulse-data-backup |
Weekly + build-triggered tarball backups of Supabase + gh-pages | Private |
mdeguzis/decky-proton-pulse |
Companion Decky plugin source | Yes |
mdeguzis/decky-proton-pulse.wiki |
Plugin wiki | Yes |
mdeguzis/condenser-proton-pulse |
Condenser port of the plugin | Yes |
mdeguzis/claude-configs |
Private repo of Claude memory + rule backups | Private |
- Domain:
proton-pulse.com(through Cloudflare). - Site:
https://www.proton-pulse.comserved directly from Cloudflare Pages projectproton-pulse-web(post-#362). - Staging:
https://staging.proton-pulse.comserved from Cloudflare Pages projectproton-pulse-web-staging. - Per-game data: R2 bucket
proton-pulse-databound tohttps://data.proton-pulse.com(public GET, CORS open). - Edge worker:
pp-edge-statuson Cloudflare Workers, powers the status page probe. - Supabase project:
ilsgdshkaocrmibwdezk(URLhttps://ilsgdshkaocrmibwdezk.supabase.co). - Discord server: check pinned invite
https://discord.gg/UdPaEsMtd. See Site Status Page for the incident + announcement webhooks. - GH Pages plumbing (
gh-pagesbranch +mdeguzis/proton-pulse-web-stagingrepo) is preserved as the rollback path only. Nothing points to it in the live config.
Non-obvious pieces that live in .github/workflows/ and need to keep working after a restore:
| File | Purpose | Where it fails visibly |
|---|---|---|
.github/workflows/semgrep.yml |
Static analysis (TypeScript, OWASP, security-audit rulepacks) on every push + PR | PR checks show semgrep/ci red |
.github/workflows/sbom.yml |
Syft SBOM + Grype vulnerability scan, uploads CycloneDX artifact | PR checks show sbom + grype red |
.grype.yaml |
Ignore rules: drop go-module + github-action findings (vendored inside the supabase npm CLI, not our runtime) |
Removing this file re-floods Grype with 300+ false positives |
.github/workflows/restore-drill-reminder.yml |
Quarterly cron opens the drill checklist issue | No drill issue on 1st of Jan/Apr/Jul/Oct |
.github/workflows/security-monitor.yml + .github/scripts/security-monitor.mjs
|
Hourly Supabase log check: auth spikes, 5xx rates, RLS denials, aggregate + per-IP 429s | New issues stop appearing when a real event lands |
Content moderation lives in a sibling repo:
| File | Purpose |
|---|---|
proton-pulse-data/.github/workflows/content-moderation.yml |
Every 4h + daily deep scan, calls the script below |
proton-pulse-data/.github/scripts/moderate-content.mjs |
Two-layer scan (wordlist + OpenAI) on user_configs, plus aux scans on user_proton_configs.app_name, user_systems.label, user_systems.sysinfo_text, flagged_reports.reason_text. Aux hits open GH issues labeled content-moderation-review + security (rate-limited to 10 issues per run) |
The content-moderation-review label must exist in the target repo (mdeguzis/proton-pulse-web) or issue creation will 422. Recreate with:
gh label create content-moderation-review --repo mdeguzis/proton-pulse-web \
--color d73a4a --description "Aux moderation scan flagged a row -- admin triage needed"image-refetch-
plugin-link-complete,plugin-link-remove,plugin-link-start,plugin-link-status,plugin-link-unlink,plugin-links-list protondb-summary-
public-steam-profile(anonymous, powers/lookup-- reads Steam profile URL / SteamID + owned games via Steam Web API usingSTEAM_API_KEY) -
steam-appdetails,steam-callback,steam-depot-info,steam-explore,steam-library-lookup,steam-news sync-steam-librarysync-steam-wishlistuser-system-upload
| Secret | Purpose | Where to get a new value |
|---|---|---|
SUPABASE_URL |
REST base URL for the project | Supabase console -> Project Settings -> API |
SUPABASE_ANON_KEY |
Public anon key | Supabase console -> Project Settings -> API |
SUPABASE_SERVICE_ROLE_KEY |
Server-only key, RLS bypass | Supabase console -> Project Settings -> API |
SUPABASE_TOKEN |
Personal Access Token for supabase CLI + Management API |
Supabase console -> Account -> Access Tokens |
BACKUP_HMAC_SECRET |
Signs the backup tarballs | Generate: openssl rand -hex 32
|
BACKUP_REPO_TOKEN |
Push access to proton-pulse-data-backup
|
GitHub fine-grained PAT, Contents: R/W on the backup repo |
STAGING_DEPLOY_TOKEN |
Push access to proton-pulse-web-staging (rollback path only, GH Pages) |
GitHub fine-grained PAT, Contents: R/W on the staging repo |
CLOUDFLARE_API_TOKEN |
CF Pages Edit + Workers Scripts Write + R2 Bucket Item Write on proton-pulse-data
|
Cloudflare dashboard -> My Profile -> API Tokens |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account ID (not the token) | Cloudflare dashboard -> right sidebar on any Pages or Workers page |
R2_ACCESS_KEY_ID |
R2 access key for the pipeline data upload | Cloudflare dashboard -> R2 -> Manage R2 API Tokens |
R2_SECRET_ACCESS_KEY |
Paired secret for the above | Same |
DISCORD_WEBHOOK_ANNOUNCE |
#announcements channel |
Discord -> Server -> Integrations -> Webhooks |
DISCORD_WEBHOOK_BACKUPS |
Backup channel | Same |
DISCORD_WEBHOOK_BUILDS |
CI failures channel | Same |
DISCORD_WEBHOOK_ISSUES |
Issue events channel | Same |
DISCORD_WEBHOOK_PULL_REQUESTS |
PR events channel | Same |
DISCORD_WEBHOOK_RELEASES |
Releases channel | Same |
STEAM_API_KEY |
Steamworks Web API | https://steamcommunity.com/dev/apikey |
SGDB_API_KEY |
SteamGridDB fallback for missing cover art | https://www.steamgriddb.com/profile/preferences/api |
OPENAI_API_KEY |
Content moderation classifier (lives in mdeguzis/proton-pulse-data too -- same key, both repos) |
https://platform.openai.com/api-keys |
VT_API_KEY |
VirusTotal attachment scanner | https://www.virustotal.com/gui/my-apikey |
CODECOV_TOKEN |
Coverage uploads | codecov.io repo settings |
GITHUB_TOKEN |
Injected by Actions itself | Never manually set |
Assume you have the code repo cloned and the backup repo cloned side by side.
- In the Supabase console, create a new project. Region should match latency needs (US East is what we used).
- Note the project ref (from the URL). It replaces
ilsgdshkaocrmibwdezkeverywhere below. - Copy the anon key, service role key, project URL. These become the new secrets.
- Run every migration under
supabase/migrations/in order via the Management API:
source ~/.supabase
export SUPABASE_PROJECT_REF=<new-ref>
for f in supabase/migrations/*.sql; do
echo "applying $f"
SQL=$(cat "$f")
curl -s -X POST "https://api.supabase.com/v1/projects/${SUPABASE_PROJECT_REF}/database/query" \
-H "Authorization: Bearer $SUPABASE_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"query\": $(echo "$SQL" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}"
done- Deploy every edge function:
export SUPABASE_ACCESS_TOKEN=$SUPABASE_TOKEN
for fn in supabase/functions/*/; do
slug=$(basename "$fn")
[ "$slug" = "_shared" ] && continue
supabase functions deploy "$slug" --project-ref "$SUPABASE_PROJECT_REF"
done- In Supabase console -> Edge Functions -> Secrets, add:
STEAM_API_KEY,SGDB_API_KEY,OPENAI_API_KEY. See Secrets and Environment for the current names each fn expects.
Populate every secret from the Phase 1 table into mdeguzis/proton-pulse-web -> Settings -> Secrets and variables -> Actions. Also copy SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and OPENAI_API_KEY into mdeguzis/proton-pulse-data -> Settings -> Secrets so the content moderation workflow can run.
Recreate the content-moderation-review label in mdeguzis/proton-pulse-web (see the Security scanner + moderation infrastructure section above for the gh label create command). Without the label, the aux moderation scanner cannot open issues.
- Ensure
proton-pulse.comis on Cloudflare. - Create two Cloudflare Pages projects in the CF dashboard:
-
proton-pulse-web-staging(custom domain:staging.proton-pulse.com) -
proton-pulse-web(custom domain:www.proton-pulse.com)
-
- Create the R2 bucket
proton-pulse-dataand bind the custom domaindata.proton-pulse.comto it (CORS: allow GET from any origin). - In the Cloudflare API Tokens dashboard, create a token with these permissions:
- Cloudflare Pages: Edit (both accounts)
- Workers Scripts: Write + Read (entire account)
- R2 Storage Bucket Item: Write (scoped to proton-pulse-data)
- Add the token as GitHub Actions secret
CLOUDFLARE_API_TOKEN. Also addCLOUDFLARE_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY(R2 credentials come from the CF R2 dashboard, NOT the API token). - Add the Supabase edge fn secret
STAGING_URL=https://staging.proton-pulse.comso Steam OAuth callbacks from staging land back on staging instead of prod.SITE_URLstays ashttps://www.proton-pulse.com(see theproject_supabase_site_url.mdmemory). - Update the CF Worker
pp-edge-status'sALLOWED_ORIGINSarray inworkers/edge-status/index.jsif any origin has changed; deploy viamake deploy-worker. - First push to
maintriggerspublish-shell.yml, which deploys the shell to CF Pages.
The GitHub Pages plumbing is preserved as rollback. To activate:
- Repoint
wwwDNS to the GitHub Pages IPs (185.199.108-111.153). - In
mdeguzis/proton-pulse-web-> Settings -> Pages -> Custom domain: setwww.proton-pulse.com. - Wait for GitHub's cert to be issued, then flip Cloudflare proxy back on with SSL mode = Full (non-strict).
- Dispatch
update-data.ymlwithdeploy_target=ghpagesto populate thegh-pagesbranch.
The initial CF Pages deploy needs data. Dispatch the full pipeline (deploys to CF Pages by default post-#362):
gh workflow run update-data.yml --repo mdeguzis/proton-pulse-web
gh run watch --repo mdeguzis/proton-pulse-webFor the R2 bucket, the first pipeline run syncs all ~187k per-game data files. Wall time ~30 min for the sync alone. If the pipeline succeeds but R2 is empty at the end, check the CLOUDFLARE_API_TOKEN has R2 Bucket Item: Write on proton-pulse-data specifically.
The backup repo has three artifact families under latest/:
| File | Contents | Restore path |
|---|---|---|
latest-schema.tar.gz |
pg_dump of the schema (tables, RLS, functions, triggers) | Not needed if Phase 2.1 migrations were applied cleanly. Use for drift check only |
latest-user_configs.tar.gz |
JSON dump of user_configs (Pulse reports) |
psql \copy back into Supabase |
latest-author_avatars.tar.gz |
Storage bucket dump | Upload via supabase storage upload
|
latest-site.tar.gz |
Full gh-pages snapshot (data JSONs, search-index, etc.) | git checkout --orphan gh-pages && tar xf latest-site.tar.gz && git add -A && git commit && git push --force |
tar xzf latest-user_configs.tar.gz
# yields user_configs.jsonl
export PGPASSWORD=<db-password>
psql -h db.<PROJECT_REF>.supabase.co -U postgres -d postgres \
-c "\copy user_configs FROM 'user_configs.jsonl' WITH (FORMAT csv)"Adjust FORMAT + column list based on what the dump actually contains -- inspect the first row before running.
tar xzf latest-author_avatars.tar.gz
supabase storage cp --project-ref "$SUPABASE_PROJECT_REF" \
--recursive ./author_avatars/ supabase://author-avatars/The latest-site.tar.gz tarball is a snapshot of the pre-#362 gh-pages layout. In the CF Pages era it is not what the live site reads (that's the R2 bucket + the CF Pages shell). Use it for:
-
Rollback: repush the tarball to
gh-pagesand follow section 2.3.1 to activate GitHub Pages. -
Cold-start R2: dispatch
update-data.yml(see 2.4). The pipeline probes fresh and uploads to R2, so the tarball is not strictly required. Use it only if you need the historical exact snapshot.
Rollback restore:
git clone --depth 1 git@github.com:<yourorg>/proton-pulse-web.git ghp
cd ghp
git checkout --orphan gh-pages
git rm -rf .
tar xzf ../latest-site.tar.gz
git add -A
git commit -m "Restore from backup"
git push origin gh-pages --forceEach wiki repo is its own git remote. To restore:
git clone git@github.com:<yourorg>/proton-pulse-web.wiki.git
# copy .md files from an old backup or from a fork
cd proton-pulse-web.wiki
git add -A
git commit -m "Restore wiki"
git pushSame pattern for decky-proton-pulse.wiki.git.
Before declaring recovery done, verify each of these:
-
/about.htmlshows the version and short SHA you expected. -
/data/570/index.jsonreturns a real ProtonDB report list. - Steam sign-in from
/profile.htmlcompletes and lands on the profile page. -
/admin.htmlrecognises the seed admin uuid you inserted into theadminstable. - File a test issue with the
incidentlabel -> Discord#announcementsreceives a message. -
/status.htmlloads and shows every edge function green. - Submit a dummy report through the plugin or
/submit.html, approve it in admin, confirm it shows up on the game page. - Open any PR -- CI shows
semgrep/ciandsbom + grypegreen. - SBOM artifact appears on the PR run: Actions -> the sbom+grype job -> Artifacts panel has
sbom-cyclonedx+grype-sarif. - Content moderation workflow (
proton-pulse-data) runs on schedule and its summary shows all four aux tables were scanned (0 hits acceptable, the count row is what matters). -
content-moderation-reviewlabel exists inmdeguzis/proton-pulse-web(gh label list --repo mdeguzis/proton-pulse-web | grep content-moderation-review).
If the domain, Supabase project, Cloudflare zone, and Discord server are being transferred (not recreated), do all of these:
- Discord: Server Settings -> Members -> transfer ownership. Requires 2FA on the new account.
- Supabase: console -> Organization -> add new owner -> demote current owner. Free tier only allows one owner per org, so may require moving the project to a new org.
- Cloudflare: DNS zone -> Members -> add new member as Administrator, verify DNS still resolves, then remove the old member.
- Domain: registrar transfer (auth code + 5-day window).
- GitHub: repo settings -> Manage access -> add new maintainer, wait for accept, then transfer ownership under Settings -> Danger Zone.
- Cycle every PAT that the previous maintainer's account issued:
SUPABASE_TOKEN,BACKUP_REPO_TOKEN,STAGING_DEPLOY_TOKEN,GITHUB_TOKENshows up asGITHUB_TOKENin Actions so that's free.
Rotating any long-lived secret should be a two-write:
- Add the new value under a temporary secret name (e.g.
SUPABASE_TOKEN_NEW). - Deploy code that reads both
SUPABASE_TOKENandSUPABASE_TOKEN_NEW, preferring the new one. - Trigger every workflow that uses the secret and confirm.
- Rename
SUPABASE_TOKEN_NEW->SUPABASE_TOKEN, remove the compat code, redeploy.
- Web Deploy Workflow - normal staging + prod flow
- Secrets and Environment - detailed secret-by-secret notes
- Data Pipeline - what the CI pipeline actually does
- Site Status Page - live health of every edge fn
- Security Guardrails - what every scanner is for + how to add a new one
- Content Moderation - primary + aux scan tables and the review flow