Skip to content

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 ​

BranchCloudflare projectSupabase projectTrigger
developDEVqr-setu-dev (dyhjofjjuazhyqcvlrkx)auto on green CI
uatUATqr-setu-dev (shared with DEV)manual workflow_dispatch
mainPRODqr-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 DEV VITE_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 + auto deploy-dev), reusable deploy.yml + deploy-uat.yml/deploy-prod.yml (workflow_dispatch + verify-ci), and backend-ci.yml (path-filtered supabase/**) + 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/web now 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 and main→PROD (manualworkflow_dispatch). ⚠ Each build injects its own SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY (web) or EXPO_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_functions on 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's supabase/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 — jobs workspace · sonar (QRS-246) · e2e-web · card-ui-gate · docs
    • backend-ci.yml — path-filtered supabase/**: edge-functions · sql-grants · db-pgtap
    • parity-native.yml — android · ios, manual workflow_dispatch only with a platform input (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-ci and release-gate carries if: github.event.pull_request.user.login != 'dependabot[bot]'. A bump is tested once, by the develop push after merge. Dependabot-PR CI was 29% of Aug + Sep minutes. ⚠ nefoxx shares the same 2,000-minute quota and still runs CI on its Dependabot PRs. Evidence: portal guides/github-actions-usage-forensics.md.
    • release-gate.yml — the release.json gate, path-filtered to supabase/** + releases/**
    • security.yml — secret-scan · sast; push, PR and manual. Its weekly cron was removed 2026-09-23.
    • ⚠ NO WORKFLOW IN ANY digious-platforms REPO RUNS ON A SCHEDULE (owner decision 2026-09-23: "all runs should be explicitly invoked to save on quota"). Every workflow carries workflow_dispatch. Do not add a schedule: without asking. The same holds for nefoxx, which shares the quota: its nightly e2e-regression.yml is manual-only.
    • env-drift.yml — push-triggered on develop (migrations/functions/config) + manual workflow_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), and ci.yml's e2e-web, card-ui-gate and docs now needs: 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. Manual workflow_dispatch only 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_dispatch only, jobs preflight + deploy. BACKEND only (Supabase migrations + Edge Functions). It does not touch apps/web.
    • deploy-web.yml — apps/web to Cloudflare WORKERS, branch-driven [2026-08-20].develop→devv.qrsetu.com and uat→uatt.qrsetu.com deploy automatically on push; main→qrsetu.com is workflow_dispatch only, gated on typing the hostname by hand. Jobs resolve (branch→environment mapping) + deploy. ⚠ WORKERS, NOT PAGES, and apps/web COULD NOT RUN ON CLOUDFLARE AT ALL BEFORE THIS. It shipped with @react-router/serve, so the SSR entry used renderToPipeableStream and the bundle imported node:stream — Node APIs that workerd does not have. It built green, ran fine under npm start, and would have 500'd on the first production request; the conversion needed a new entry.server.tsx (renderToReadableStream, await body.allReady for bots so a crawler gets a complete document) plus workers/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 a main ruleset 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 omitted payments-watchdog.yml entirely, 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 nested release-gate/security/env-drift inside the parity-native bullet, 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.yml does 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 manual workflow_dispatch behind the release.json gate, and there is still no deploy-dev.yml and no deploy-uat.yml for 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.yml ships apps/web to devv.qrsetu.com on every push to develop and to uatt.qrsetu.com on every push to uat. 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 backend deploy-dev/deploy-uat path.
  • 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's sonar job 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 a services: block: a service container starts before any step, so the mandatory vm.max_map_count fix could never apply and embedded Elasticsearch would refuse to boot. ⚠ The baseline IS bootstrapped — sonar-baseline.json carries "bootstrapped": true with 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 red sonar job 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 EOF and took Docker Desktop down with it — embedded Elasticsearch + a 3 GB scanner heap + a bind mount over node_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-security run in the whole-tree eslint . 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 already error in the recommended set — including prefer-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 on projectService was 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-staged passes --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 reach develop.
    • Never assume ESLint == Sonar. It is a subset even now: SonarLint reported S7781 (prefer replaceAll) and S4036 (PATH) during this work and neither rule exists in eslint-plugin-sonarjs. A green npm run lint is much stronger evidence than it was, but only the sonar job 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) and cognitive-complexity (9) — one piece of work: the async-screen loading/error/empty/content ladder in 5 screens, plus packages/domain/src/reminders/recurrence.ts at 38 and schedule.ts at 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.ts was the worst function in the repo and the safest to restructure, because node --test cases name its behaviour (24 in recurrence.test.ts, ~90 across the reminders folder — ⚠ this said 117 for that one file). It also closed a latent bug: the old switch ASSIGNED with no default, so an unhandled frequency would have emitted the same instant MAX_STEPS times; the extracted stepCursor returns 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 @/ui async 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.
  • 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.

refnamewhat it is
dyhjofjjuazhyqcvlrkxqr-setu-devThe source of truth and the only environment that informs design (see "Second rule"). Mumbai.
ikkwqowfnbhdasfejojgqr-setu-prodPRODUCTION, 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.
ygmqxyrbnemhwkiyobocqr-setu-legacy-bkpA 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.