Pilot pre-release checklist¶
Run this before every production deploy while the ~15-user pilot is live. It
covers what no automated test covers today: dashboard toggles, manual
env vars, and operational risks a green CI run does not catch. See
docs/superpowers/plans/2026-09-02-pilot-safety-gate.md for why each item
exists.
One-time setup (do before the first pilot user is invited)¶
These are one-time, not per-deploy, but the pilot is not safe to open until all three are done.
- BLOCKING — Disable public sign-up in the Supabase dashboard.
Authentication → Sign In / Providers → turn off "Allow new users to
sign up". This is the single most important item on this page. The
invite-gated
/auth/invites/redeemendpoint (backend/app/api/invites.py) only protects the app's own sign-up form. The Supabase anon key is public by design, so until this toggle is off, anyone can open a browser console and callsupabase.auth.signUp()directly — skipping the invite gate, the invite rate limits, and the form entirely. Do not treat the invite gate as sufficient on its own; confirm this toggle is off, in the dashboard, before inviting anyone. - BLOCKING — Confirm PITR / backups are enabled on the Supabase
project (Settings → Database → Backups). There is no
pg_dumpautomation anywhere in this repo, and the project's own readiness checklist has this unchecked:docs/reference/supabase-complete-reference.md:1184—- [ ] PITR enabled for backups. This is the only unrecoverable-data-loss risk in the pilot — a bad migration or a bug that deletes rows has no fallback without it. - Generate invite codes with real entropy, not a memorable string:
INVITE_REDEEM_RATE_LIMITandINVITE_REDEEM_EMAIL_RATE_LIMITbound how much noise a guesser can make, not whether a guess succeeds — nothing stops someone from tryingPILOT-2026a handful of times across a few hours and getting in. Entropy in the code itself is the actual control.
Before deploying¶
-
git log --first-parent main— confirm what is actually shipping. - Confirm no extraction job is running. On the host: A deploy restarts the workers, and an interrupted extraction job restarts from document zero — billing OpenAI a second time for work already paid for.
-
.deploy-historyhas at least two lines, otherwise./scripts/deploy_prod.sh --rollbackhas nothing to roll back to. (.deploy-historyis created at the repo root bydeploy_prod.shitself on first deploy; if it doesn't exist yet, the next deploy will be the first line and rollback still won't have a target.)
Environment variables to set (and verify, not just set)¶
Set these in the production .env on the host, then rebuild/redeploy as
noted — do not mark any of these done just because the variable is present
in .env.
-
LLM_RATE_LIMIT(default20/hour) — budget on/qa,/chat,/enhance_query, the only OpenAI-billed endpoints. Verify it fires:Expect the run to end in afor i in $(seq 1 21); do curl -s -o /dev/null -w "%{http_code} " \ -X POST https://juddges.augustyniak.ai/api/enhance_query \ -H 'Content-Type: application/json' -d '{"query":"test"}' done; echo429. (Signed-in requests get their own per-user bucket — seefrontend/app/api/enhance_query/route.ts— so run this loop signed out, or expect it to take one signed-in user's whole hourly budget rather than the shared container bucket.) -
INVITE_REDEEM_RATE_LIMIT(default60/hour) andINVITE_REDEEM_EMAIL_RATE_LIMIT(default5/hour) — the first is a SHARED ceiling on/auth/invites/redeemkeyed on the proxy address, not per-client protection; the second is the real per-invitee limit, keyed on the normalized email. Both ship with safe defaults — only override with a reason. -
LLM_NAME— pin to a dated OpenAI snapshot before the pilot. Currently unset, so extraction and chat run on the floating alias"gpt-5", which OpenAI can repoint without notice — an extracted dataset from before and after that happens is not reproducible.OPENAI_MODELin.env.exampleis read by no code at all —LLM_NAME(backend/packages/juddges_search/juddges_search/llms.py) is the live variable. Do not editOPENAI_MODELexpecting an effect. -
SENTRY_DSNandNEXT_PUBLIC_SENTRY_DSN— Sentry is fully wired (backend/app/sentry.py,frontend/sentry.*.config.ts, including the Celery integration and a PII scrubber) but is a silent no-op without a DSN.NEXT_PUBLIC_SENTRY_DSNis baked into the JS bundle at build time — setting it on a running container does nothing; the frontend image must be rebuilt. Verify the backend side: then confirm a "JUDDGES SENTRY SMOKE TEST" event lands in the Sentry project within a minute. For the frontend, trigger a client error in the browser after rebuilding and confirm it appears too. - Langfuse tracing — set
LANGFUSE_PUBLIC_KEYandLANGFUSE_SECRET_KEY.ENABLE_LANGFUSEin.env.exampleis not read by any code path — tracing turns on automatically the moment both keys above are non-empty (seebackend/packages/juddges_search/juddges_search/chains/callbacks.pyandbackend/app/search_telemetry.py). Verify by running one real chat query, then confirming a trace with a cost figure appears in the Langfuse project — no trace means the keys did not take.
After deploying¶
-
curl -s -o /dev/null -w '%{http_code}' https://juddges.augustyniak.ai/returns200. -
/extractredirects to/auth/loginwhen signed out. - Sign in as a test user, run one search, open one judgment.
- Launch a five-document extraction and confirm it reaches COMPLETED.
- Export the result as CSV and open the file.
- Sign-up with a bogus/unknown invite code is refused on the deployed site (not just in tests).
- Sentry shows no new unresolved issues from the deploy window.
- Langfuse shows today's spend below the monthly ceiling.
Note: post_deploy_validation() in scripts/deploy_prod.sh only warns
on failure — it never aborts the deploy and never rolls back automatically.
A green "=== Deployment complete ===" message is not evidence the deploy
worked. This checklist is the evidence.
Known gaps at pilot launch¶
Named so nobody mistakes this checklist for full coverage:
- The global rate-limit middleware does not cover routes mounted via
include_router— tracked as #574 (see the comment aboveapp.include_router(...)inbackend/app/server.py). The LangServe LLM routes are covered separately byLLM_RATE_LIMIT(backend/app/llm_rate_limit.py), but otherinclude_routerroutes are not. - There is no per-user ceiling on extraction spend — the most expensive
path in the app.
EXTRACTION_SUBMIT_RATE_LIMITandMAX_DOCUMENTS_PER_JOBbound submission rate and job size per request, not total spend per user over time. - There is no Grafana, no metrics endpoint, and no log aggregation (see
infra/grafana/README.md— every dashboard exceptsearchis a placeholder).docker compose logsplus Sentry are the only error visibility until that lands.