Appearance
Environment Strategy
THE PRODUCTION PROJECT CHANGED ON 2026-08-16 — CHECK THE REF, NOT THE NAME
Every ygmqxyrbnemhwkiyoboc on this page is the retired project, now named qr-setu-legacy-bkp. Production is ikkwqowfnbhdasfejojg (qr-setu-prod), on a separate Supabase account. ⚠ Both projects have held the name qr-setu-prod, so the NAME is ambiguous and only the REF identifies a project — anything written before 2026-08-16 that says "qr-setu-prod" means the other one. Following a stale instruction here points an OAuth redirect, a migration or a build at the wrong live project.
Env-var names are stale (2026-08-08)
Read VITE_SUPABASE_* below as EXPO_PUBLIC_SUPABASE_* (mobile) and SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY (apps/web, server-side). The branch→environment matrix itself is current. One thing to know before trusting any Dev result: qr-setu-dev's public schema is empty as of 2026-08-08 — the v2 baseline is authored and not yet applied.
Two Supabase projects, three Cloudflare Pages projects, three branches.
Branch → environment matrix
| Branch | Cloudflare project | Supabase project | Trigger |
|---|---|---|---|
develop | DEV | qr-setu-dev (dyhjofjjuazhyqcvlrkx) | auto on green CI |
uat | UAT | qr-setu-dev (shared with DEV) | manual workflow_dispatch |
main | PROD | qr-setu-prod (ygmqxyrbnemhwkiyoboc) | manual workflow_dispatch |
DEV and UAT share the Dev Supabase project. Only PROD is isolated (real user data). Each build injects its own
VITE_SUPABASE_*.
Secrets & env
.env.test— committed, dummy values only, for the test runner..env.local— real DEVVITE_SUPABASE_*, never committed..env.example— documents every server secret the functions read:SUPABASE_SERVICE_ROLE_KEY,SUPABASE_DB_URL,CLOUDFLARE_API_TOKEN,CLOUDFLARE_ZONE_ID,ENVIRONMENT,ENABLE_DEBUG_LOGS.- No hardcoded production-fallback URL in the client — an E2E/cold-stack retry must never reach a real project.
CI/CD pipelines (target)
- Separate pipelines:
frontend-ci.yml(build-and-test +sonar-scan+e2e-smoke+ autodeploy-dev), reusabledeploy.yml+deploy-uat.yml/deploy-prod.yml(workflow_dispatch+verify-ci), andbackend-ci.yml(path-filteredsupabase/**) +deploy-backend-prod.yml. - Gates: SonarQube Community (GHA service container, baseline-gated) + CodeQL + Dependabot + secret scanning + DAST; bundle-size + Web-Vitals + load-test.
- Branch protection: PR + CODEOWNERS + required checks.
Cloudflare gotchas
- Each Pages project's "Production branch" must be renamed
dev/uat/production. - Bot Fight Mode can 403 smoke checks — allowlist the checker.
See Deployment & Promotion for the manual promotion runbook.
Environments, CI/CD and promotion (operating-manual text)
Provenance — moved from CLAUDE.md on 2026-09-23 (QRS-1288)
This is the verbatim text of CLAUDE.md § "Environments, CI/CD & promotion [target]" as of commit 00c1eca, relocated here under the context-architecture programme. Sentences of the form "this said X until [date]" are corrections recorded at the time they were made; the live rule is the corrected one. Retired vocabulary inside those corrections names what was retired and is not a live claim.
Environments, CI/CD & promotion [target]
- Two Supabase projects: DEV+UAT share the Dev project; PROD is isolated. Three Cloudflare environments (DEV/UAT/PROD) — ⚠ these were Pages projects and
apps/webnow deploys as a Worker (wrangler.jsonc+deploy-web.yml); the Pages projects predate the 2026-08-20 conversion. Branch → env:develop→DEV (auto on green CI),uat→UAT andmain→PROD (manualworkflow_dispatch). ⚠ Each build injects its ownSUPABASE_URL/SUPABASE_PUBLISHABLE_KEY(web) orEXPO_PUBLIC_SUPABASE_*(mobile) —VITE_SUPABASE_*has ZERO hits in the repo and belonged to the retired Vite SPA. - ⚠ CONFIRM AN EF ACTUALLY EXISTS ON DEV BEFORE BELIEVING ANY TEST RESULT [found 2026-08-01]. Call
list_edge_functionson Dev and confirm the EF you depend on is deployed there. A missing EF returns 404, which the app surfaces as a generic failure indistinguishable from a bug in your own code — that is the whole hazard, and it is a Dev-completeness check. Do NOT compare Dev against Prod's inventory to decide what Dev should have (ADR-0020 / "Dev is the source of truth"): what Dev needs is what the repo'ssupabase/functions/declares, not what a pre-vision Prod happens to run. Dev's deployed set is the thing to bring up to the repo. - Separate pipelines. The workflow SET and its COUNT are in the measured inventory at the top of this file — do not restate either here; this list says what each one does:
ci.yml— jobsworkspace·sonar(QRS-246) ·e2e-web·card-ui-gate·docsbackend-ci.yml— path-filteredsupabase/**:edge-functions·sql-grants·db-pgtapparity-native.yml—android·ios, manualworkflow_dispatchonly with aplatforminput (android · ios · both), owner decision 2026-09-23. No schedule, no push trigger. Its nightly was 57% of all Actions minutes in Aug + Sep, and every run of it failed.- No CI runs on Dependabot PRs (owner decision 2026-09-23): every job in
ci,security,backend-ciandrelease-gatecarriesif: github.event.pull_request.user.login != 'dependabot[bot]'. A bump is tested once, by thedeveloppush after merge. Dependabot-PR CI was 29% of Aug + Sep minutes. ⚠nefoxxshares the same 2,000-minute quota and still runs CI on its Dependabot PRs. Evidence: portalguides/github-actions-usage-forensics.md. release-gate.yml— therelease.jsongate, path-filtered tosupabase/**+releases/**security.yml—secret-scan·sast; push, PR and manual. Its weekly cron was removed 2026-09-23.- ⚠ NO WORKFLOW IN ANY
digious-platformsREPO RUNS ON A SCHEDULE (owner decision 2026-09-23: "all runs should be explicitly invoked to save on quota"). Every workflow carriesworkflow_dispatch. Do not add aschedule:without asking. The same holds fornefoxx, which shares the quota: its nightlye2e-regression.ymlis manual-only. env-drift.yml— push-triggered ondevelop(migrations/functions/config) + manualworkflow_dispatch. Its daily cron was removed 2026-09-23 (owner: minutes are spent when a developer asks). ⚠ A secret changed only in the Supabase dashboard is now caught only by a manual run, so run it after any console change and before any promotion.- Every job in every workflow carries
timeout-minutes(2026-09-23; the default is 360), andci.yml'se2e-web,card-ui-gateanddocsnowneeds: workspace, so a lint/type-check failure skips them instead of paying for them. payments-watchdog.yml— the OUT-OF-BAND half of payment reconciliation. Manualworkflow_dispatchonly since 2026-09-23 (the hourly cron is kept commented out for a future launch window, and the workflow is also disabled in the UI), asserting four things the system cannot assert about itself: nothing has swept · the last sweep examined zero candidates (a green no-op on the one component whose job is to notice that something did not happen — QRS-013 applied to money) · a critical exception is open · the webhook is dropping events. It is date-bound by explicit owner decision and FAILS rather than skips when the bound passes, because a forever-cron pointed at a money endpoint that nobody watches is worse than none.deploy-prod.yml—workflow_dispatchonly, jobspreflight+deploy. BACKEND only (Supabase migrations + Edge Functions). It does not touchapps/web.deploy-web.yml—apps/webto Cloudflare WORKERS, branch-driven [2026-08-20].develop→devv.qrsetu.comanduat→uatt.qrsetu.comdeploy automatically on push;main→qrsetu.comisworkflow_dispatchonly, gated on typing the hostname by hand. Jobsresolve(branch→environment mapping) +deploy. ⚠ WORKERS, NOT PAGES, andapps/webCOULD NOT RUN ON CLOUDFLARE AT ALL BEFORE THIS. It shipped with@react-router/serve, so the SSR entry usedrenderToPipeableStreamand the bundle importednode:stream— Node APIs that workerd does not have. It built green, ran fine undernpm start, and would have 500'd on the first production request; the conversion needed a newentry.server.tsx(renderToReadableStream,await body.allReadyfor bots so a crawler gets a complete document) plusworkers/app.ts. A green build is not evidence that it targets the right runtime, which is this repo's own most-repeated defect shape. ⚠ NEITHER REQUIRED REVIEWERS NOR BRANCH PROTECTION EXISTS ON THIS PLAN — measured against the repo's own API, not read in docs: environment protection returns 422 "billing plan does not support" and amainruleset returns 403 "Upgrade to GitHub Pro". What does work is a custom deployment-branch policy, now set (web-dev→develop,web-uat→uat,web-production→main), so a dispatch from the wrong branch is refused by GitHub before a step runs. ⚠ And the dashboard's "Protected branches only" is a NO-OP here — with no protection rules possible, GitHub itself annotates it "all branches are still allowed to deploy", a setting that reads as a control and is not one. Production's typed-hostname confirmation is a guarded self-dispatch, never peer review. ⚠ This bullet asserted "SEVEN workflows exist" until 2026-08-17 and there were EIGHT — it omittedpayments-watchdog.ymlentirely, which until that date appeared exactly once in this whole file: inside the generated table. So the repo's only money-safety alarm was, in prose, invisible. It also nestedrelease-gate/security/env-driftinside theparity-nativebullet, which is how three workflows came to look like one. The generated block had the right number the whole time — the defect is that prose restated a count the block already owns, which is the exact thing the block's own header forbids. ⚠ This file claimed "No deploy workflow exists yet" until 2026-08-12;deploy-prod.ymldoes exist. But read the narrower statement it was protecting, which is still true and is the one that matters: nothing deploys on a merge. Prod is manualworkflow_dispatchbehind therelease.jsongate, and there is still nodeploy-dev.ymland nodeploy-uat.ymlfor the BACKEND — Supabase still has no push-triggered path to Dev or UAT. ⚠ BUT "nothing deploys on a merge" IS NO LONGER TRUE OF THE FRONTEND [corrected 2026-08-20]:deploy-web.ymlshipsapps/webtodevv.qrsetu.comon every push todevelopand touatt.qrsetu.comon every push touat. So a merge now does ship the web surface, and does not ship anything backend. Keep the two halves distinct — they are on different mechanisms with different gates. Target still adds:e2e-smoke, and a backenddeploy-dev/deploy-uatpath.
- Static analysis is TWO LAYERS, both live [QRS-246 / QRS-247]. This bullet used to promise SonarQube while the repo contained zero Sonar tooling — the standard was documented and never implemented, which is how one random file in the IDE extension turned up ten findings. What is true now:
- Layer 2 — CI, LIVE:
ci.yml'ssonarjob runs SonarQube CE in an ephemeral container and gates on a ratchet baseline (sonar-baseline.json, checked by.github/scripts/sonar-baseline-check.cjs) — it fails only when a count goes UP, because an ephemeral server has no previous analysis and so Sonar's own "New Code" Quality Gate cannot work. Do not convert this to aservices:block: a service container starts before any step, so the mandatoryvm.max_map_countfix could never apply and embedded Elasticsearch would refuse to boot. ⚠ The baseline IS bootstrapped —sonar-baseline.jsoncarries"bootstrapped": truewith real metrics, and this said "still un-bootstrapped" until 2026-08-28. The first two attempts never reached the gate at all — see QRS-256. The design (one deliberate red run that prints the measured table) is unchanged; what is corrected is the assumption that a redsonarjob means the gate ran. - Layer 2 does NOT run on this 16 GB Windows box (QRS-252). Attempted locally via Docker: SonarQube CE booted (~5 min) but the scanner container died
unexpected EOFand took Docker Desktop down with it — embedded Elasticsearch + a 3 GB scanner heap + a bind mount overnode_modules. Same family as QRS-245 (the full Playwright matrix). CI is the authority; locally, use the VS Code SonarLint extension, which is what surfaced this whole problem in the first place. - Layer 1 — local, LIVE and TYPE-AWARE:
eslint-plugin-sonarjs(~217 rules on) +eslint-plugin-securityrun in the whole-treeeslint .gate. Type-aware analysis is the load-bearing part, so do not "simplify" it away: without a program, sonarjs SILENTLY SKIPS ~15 rules that are alreadyerrorin the recommended set — includingprefer-read-only-props(S6759) — so the gate was green over 92 S6759 findings and a real always-true comparison in the reminders write path. Turning onprojectServicewas the entire fix; no rule was added. - Lint has TWO SPEEDS, on purpose. Measured: whole tree untyped 40s → typed 53s (CI pays it); a 3-file staged set untyped 10s → typed 16s (every commit pays it). So
lint-stagedpasses--config eslint.fast.mjs(typing off) and CI runs the typed gate. Accepted consequence: a type-aware finding can pass pre-commit and fail CI — it cannot reachdevelop. - Never assume ESLint == Sonar. It is a subset even now: SonarLint reported
S7781(preferreplaceAll) andS4036(PATH) during this work and neither rule exists ineslint-plugin-sonarjs. A greennpm run lintis much stronger evidence than it was, but only thesonarjob is authoritative. - The burn-down is FINISHED: 158 → 0, every sonarjs rule ON (QRS-247, closed 2026-07-30). The last 17 were
no-nested-conditional(8) andcognitive-complexity(9) — one piece of work: the async-screenloading/error/empty/contentladder in 5 screens, pluspackages/domain/src/reminders/recurrence.tsat 38 andschedule.tsat 19. Three things from it are worth carrying:- Re-measure the backlog; never read it off a comment. The config said 8 cognitive-complexity findings and there were 9 — the extra one was
.github/scripts/sonar-baseline-check.cjs, which the QRS-257 fix pushed over the threshold the day before. A gate's own scripts drift like app code and must be measured with it. This is the second time a recorded count was lower than reality (the first was 73 vs 158). - Extract a SHELL, don't thread props. Each screen's ladder became a shell component holding the identical wrappers plus top-level guard returns, so the rendered element tree is unchanged — which is the only reason touching 7 files in the QRS-203/206/207 blast radius was safe in one pass. Hooks all run before the guards, so hook order is fixed.
- Tests are what make a scary refactor mechanical.
recurrence.tswas the worst function in the repo and the safest to restructure, becausenode --testcases name its behaviour (24 inrecurrence.test.ts, ~90 across the reminders folder — ⚠ this said 117 for that one file). It also closed a latent bug: the oldswitchASSIGNED with nodefault, so an unhandled frequency would have emitted the same instantMAX_STEPStimes; the extractedstepCursorreturns from every arm, making a new frequency a compile error. Fifteen module-scope Shell components now exist across the consumer and merchant tiers (⚠ "five" until 2026-08-28), which strengthens rather than weakens the case below. The shared@/uiasync primitive that collapses them is deliberately not built —src/ui/**is the systemic surface, so it needs a design pull + drift-ledger row first (ADR-0015); sequenced to the auth round as QRS-259, when there are more than five call sites to design against.
- Re-measure the backlog; never read it off a comment. The config said 8 cognitive-complexity findings and there were 9 — the extra one was
- Layer 2 — CI, LIVE:
- CodeQL + Dependabot + secret scanning + DAST. Bundle-size + Web-Vitals + load-test gates. Branch protection (PR + CODEOWNERS + required checks).
- Promotion runbook (mandatory): every migration / EF / secret / storage / cron change is promoted to both Supabase projects deliberately (Dev first, then Prod) — Dev/UAT does not self-sync. Cloudflare gotchas: each Pages project's "Production branch" must be renamed
dev/uat/production; Bot Fight Mode can 403 smoke checks.
Phase 0.3 (Supabase/env tooling, complete): supabase/config.toml (per-function verify_jwt — Type A user-facing + public-capable EFs true; ⚠ the public_page_ops_* examples here are ARCHIVED and config.toml declares none of them — the live verify_jwt = false entries are webhooks such as razorpay-webhook, which authenticate by HMAC); .env.example documents every server secret the functions actually read (SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_URL, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, ENVIRONMENT, ENABLE_DEBUG_LOGS); npm run functions:deploy (tools/deploy-functions.js) deploys all/selected EFs to an explicit --project-ref; full promotion procedure + parity-verification SQL + CLI connectivity gotchas in supabase/docs/PROMOTION_RUNBOOK.md.
THREE Supabase projects since 2026-08-16, and TWO OF THEM HAVE HELD THE NAME qr-setu-prod. ⚠ The name is therefore ambiguous and only the REF identifies a project. Anything written before 2026-08-16 that says "qr-setu-prod" means the retired one. This is the QRS-249 duplicate-identity class applied to environments, and it is worse here than in a schema: a stale instruction points an OAuth redirect, a migration or a build at the wrong LIVE project.
| ref | name | what it is |
|---|---|---|
dyhjofjjuazhyqcvlrkx | qr-setu-dev | The source of truth and the only environment that informs design (see "Second rule"). Mumbai. |
ikkwqowfnbhdasfejojg | qr-setu-prod | PRODUCTION, created 2026-08-16 on a separate Supabase account so prod is isolated from the account holding Dev. ⚠ It sits in the same org as Nefoxx-Prod, so a token for it reaches another product's production — never make that token the ambient CLI login. |
ygmqxyrbnemhwkiyoboc | qr-setu-legacy-bkp | A BACKUP, NOT AN ENVIRONMENT. Was qr-setu-prod until 2026-08-16. Pre-ADR-0020 v1 schema, so an app pointed at it installs, launches and shows an EMPTY product — indistinguishable from client drift (QRS-672). Never a deploy or build target again. |
⚠ The two accounts cannot be seen by one CLI login. A Supabase PAT is scoped to a user, so supabase login --token is last-write-wins and the other account's projects vanish entirely — which surfaces as a 403 that reads like a permissions problem and is actually a wrong-account problem. Keep Dev as the stored login (used constantly, cheap to get wrong) and supply the prod token per-command via SUPABASE_ACCESS_TOKEN, which overrides the stored login for one invocation and leaves it intact (verified 2026-08-16).
⚠ THAT IS THE POLICY, NOT NECESSARILY THE CURRENT STATE — CHECK, DO NOT ASSUME. On 2026-08-18 the stored login was the PROD account: npx supabase projects list returned qr-setu-prod and nefoxx-prod and not qr-setu-dev, which meant a session that believed it was working against Dev could reach another product's production and could not apply a migration to Dev at all. The token gets flipped by hand, so which account is stored is volatile state, and no written claim about it stays true.Run npx supabase projects list and read the names before any db push, functions deploy or secrets set — it is one read-only call, it is the only thing that answers the question, and the failure it prevents is pointing a write at the wrong live project. check-env.mjs's KNOWN_REFS is keyed by ref for exactly this reason and is the one place that maps ref → name.
⚠ 20260710134136_baseline_schema_from_prod.sql IS RETIRED AS A FOUNDATION [2026-08-07, ADR-0020]. It was a one-time schema-only squash lifted verbatim from a pre-vision production database, and it became the implicit reference for every schema decision made afterwards — which is exactly the drift ADR-0020 corrects. Its 58 tables / 705 columns are not a starting point to extend; the ADR-0020 baseline replaces them. Read the old file only as history, never as the shape of anything. [TRANSITIONAL] the pg_cron job (it hardcodes a project-specific Edge Function URL and needs a project-URL-aware rewrite) and the profile-pictures storage bucket are environment configuration rather than portable schema; both are re-authored against the new baseline, not copied.