vsa-config-and-flags
Load when you need to know how ANY behavior of the VSA website is configured or toggled — frontend REACT_APP_* env vars, script env vars (RLS_TEST_*, SUPABASE_SERVICE_ROLE_KEY, CONFIRM_DELETE), Supaba
SKILL.md.claude/skills/vsa-config-and-flags/SKILL.md
GitHubVSA Config and Flags Catalog
What this skill is for. This is the single catalog of every configuration axis in the VSA website: what each knob is, where it is set, who consumes it, what happens when it is absent, and whether it is production-load-bearing or experimental/legacy. It also contains the checklist for adding a new configuration axis. Because flags drift, every section carries a one-line re-verification command — run it before trusting the table.
When NOT to use this skill:
| If you need… | Load instead |
|---|---|
How to create .env.local from scratch, setup traps, node version |
vsa-build-and-env |
| How to deploy, apply migrations, set secrets in prod, run image migration | vsa-run-and-operate |
| RLS policy mechanics, revoke-then-grant view pattern, check-in secrets design | vsa-supabase-security-reference |
| Whether a config change is allowed / gated (protected domains, freeze windows) | vsa-change-control |
| Seasonal-state policy (what changes when, House reveals, application season) | vsa-seasonal-operations |
| Tailwind design tokens themselves (colors, spacing, dark mode) | vsa-design-system-reference |
Running verify-rls-security.mjs and interpreting its output |
vsa-diagnostics-and-measurement |
Jargon used below: CRA = Create React App (only env vars prefixed REACT_APP_ are inlined into the JS bundle at build time — they are PUBLIC, never put secrets in them). Edge Function = Deno function under supabase/functions/, whose secrets live in Supabase (dashboard/CLI), NOT in this repo. RLS = Row Level Security, Postgres per-row access policies.
1. Frontend env vars (REACT_APP_*)
Set in .env.local for dev; in GitHub/Vercel secrets for builds. Baked into the bundle at npm run build — changing them requires a rebuild, and they are visible to anyone with browser devtools.
Re-verify: grep -rn "process.env.REACT_APP" src --include="*.ts" --include="*.tsx" and cat .env.example
| Var | Required? | Consumed by | Behavior when absent | Status |
|---|---|---|---|---|
REACT_APP_SUPABASE_URL |
YES | src/lib/supabase.ts (singleton client), src/components/features/ai/VsaAiAssistant.tsx (Edge Function URL), src/scripts/migrateCabinet.ts |
supabase.ts throws Error('Missing Supabase environment variables') at module import time (export const supabase = getSupabaseClient() runs on load) — the app fails at boot |
Production |
REACT_APP_SUPABASE_ANON_KEY |
YES | Same three files as above | Same boot failure | Production |
REACT_APP_GA4_MEASUREMENT_ID |
No | src/lib/analytics.ts (GA_ID) |
initGA() returns early — analytics silently disabled |
Production (optional) |
REACT_APP_SUPABASE_IMAGE_TRANSFORMS |
No | src/lib/supabaseImages.ts (ENABLE_IMAGE_TRANSFORMS = === 'true') |
Defaults OFF — plain URLs, no render/image transform params | Experimental flag, default-off (egress guard; see vsa-failure-archaeology) |
REACT_APP_OPENAI_API_KEY |
No | NOTHING in code. Removed from public setup docs in 2026-07 and never needed by the current Ask VSA assistant, which runs server-side via vsa-ai-assistant. |
No effect | Legacy — safe to omit everywhere. Do NOT reintroduce a client-side AI key |
Notes:
src/setupTests.tsinjects fakeREACT_APP_SUPABASE_URL/_ANON_KEYvalues so Jest never needs a real.env.local.- Drift resolved (2026-07-28):
.env.examplepreviously omitted the requiredREACT_APP_SUPABASE_ANON_KEYand listed four deadREACT_APP_PLAUSIBLE_*vars. Both are fixed — the file now lists the required URL + ANON_KEY pair and GA4 only. GA4 is the analytics system; Plausible was never wired to anything and has been removed repo-wide (owner-confirmed 2026-07-28). Re-verify:grep -c "ANON_KEY" .env.example(expect 1) andgrep -ric plausible .env.example README.md(expect 0). process.env.NODE_ENVis used insrc/components/common/PageError.tsxandErrorBoundary.tsx(dev-only error detail) — set by CRA automatically, never set it yourself.
2. Script/tooling env vars (Node scripts in scripts/)
All three main scripts self-load .env.local (they parse it into process.env if keys are unset).
Re-verify: grep -n "process.env" scripts/*.mjs scripts/*.ts
scripts/verify-rls-security.mjs (RLS smoke test)
| Var | Required? | Purpose / default |
|---|---|---|
VITE_SUPABASE_URL OR REACT_APP_SUPABASE_URL |
YES | Target project (VITE_ checked first, line ~31) |
VITE_SUPABASE_ANON_KEY OR REACT_APP_SUPABASE_ANON_KEY |
YES | Anon client |
RLS_TEST_USER_EMAIL / RLS_TEST_USER_PASSWORD |
No | Enables authenticated-member checks; skipped if unset |
RLS_TEST_ADMIN_EMAIL / RLS_TEST_ADMIN_PASSWORD |
No | Enables admin checks; skipped if unset |
RLS_TEST_EVENT_ID, RLS_TEST_MEMBER_ID, RLS_TEST_DATA_RIGHTS_REQUEST_ID |
No | Real row IDs for targeted probes; dummy UUID used otherwise |
RLS_ALLOW_MUTATION_TESTS |
No | Must be exactly 'true' to run write-path tests. Guard: leave unset against prod |
scripts/migrate-supabase-images-to-public.ts (image egress pipeline)
| Var | Required? | Purpose |
|---|---|---|
REACT_APP_SUPABASE_URL |
YES | Target project |
SUPABASE_SERVICE_ROLE_KEY (falls back to REACT_APP_SUPABASE_ANON_KEY for dry-run reads) |
YES for --apply |
Guard: script refuses --apply without the service key. Service role bypasses RLS — never put it in .env.example, frontend vars, or client code |
GITHUB_REF, CI / GITHUB_ACTIONS |
Auto | CI-detection for branch-safety behavior |
scripts/cleanup-stale-photo-requests.mjs
| Var | Required? | Purpose |
|---|---|---|
REACT_APP_SUPABASE_URL OR SUPABASE_URL |
YES | Target project |
SUPABASE_SERVICE_ROLE_KEY |
YES | Admin deletes |
CONFIRM_DELETE |
No | Dry-run unless exactly 'true' — the guard IS the default |
scripts/verify-ace-import.mjs reads no env vars (as of 2026-07-06). Operation runbooks for these scripts live in vsa-run-and-operate / vsa-diagnostics-and-measurement.
3. Edge Function secrets (Supabase-side, never in this repo)
Set via Supabase dashboard or supabase secrets set. SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY are auto-injected by the platform into every function.
Re-verify: grep -rn "Deno.env.get" supabase/functions
| Secret | Function | Purpose / behavior when absent |
|---|---|---|
GEMINI_API_KEY |
vsa-ai-assistant |
Ask VSA LLM calls; absent → assistant errors |
GEMINI_MODEL |
vsa-ai-assistant |
Model override; defaults to DEFAULT_GEMINI_MODEL = "gemini-3.1-flash-lite" (index.ts line ~32, as of 2026-07-06) |
VSA_AI_ASSISTANT_ENABLED |
vsa-ai-assistant |
Kill switch: set to string "false" to disable the assistant; any other value (or unset) = enabled |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN, GA4_PROPERTY_ID |
analytics-proxy |
GA4 Data API OAuth quartet; each is getRequiredEnv — absent → structured error (GOOGLE_OAUTH_* / GA4_* codes) returned to admin dashboard |
IMAGE_MIGRATION_WEBHOOK_SECRET |
trigger-event-image-migration, trigger-house-event-image-migration |
Shared secret the caller must present; absent/mismatch → request rejected |
GITHUB_DISPATCH_TOKEN |
both trigger-* functions | PAT used to fire repository_dispatch to GitHub |
GITHUB_REPOSITORY |
both trigger-* functions | owner/repo target for dispatch |
GITHUB_DISPATCH_EVENT_TYPE |
trigger-event-image-migration |
Defaults to event-image-migration-requested |
GITHUB_DISPATCH_EVENT_TYPE_HOUSE |
trigger-house-event-image-migration |
Defaults to house-event-image-migration-requested |
Retired secret: OPENAI_API_KEY belonged to the legacy secure-ai path, which was removed from repo source in issue #353 after source search found no callers. No current supabase/functions code reads it. Deleting the directory does not undeploy the function or unset the Supabase secret; those remain manual production cleanup steps.
4. GitHub Actions secrets
Re-verify: grep -n "secrets\." .github/workflows/*.yml
| Secret | Workflow(s) | Purpose |
|---|---|---|
REACT_APP_SUPABASE_URL |
deploy.yml (build + Docker build-args), migrate-images.yml, migrate-event-images.yml |
Baked into CI builds; project target for migration scripts |
REACT_APP_SUPABASE_ANON_KEY |
deploy.yml (build + Docker build-args) |
Same |
SUPABASE_SERVICE_ROLE_KEY |
migrate-images.yml, migrate-event-images.yml |
Image-migration --apply runs (RLS bypass) |
VERCEL_TOKEN |
deploy.yml |
Prod deploy via npx vercel --prod; guard: the deploy step checks if [ -z VERCEL_TOKEN ] and skips deploy if unset |
ORG_ID, PROJECT_ID |
deploy.yml |
Exported as VERCEL_ORG_ID / VERCEL_PROJECT_ID for the Vercel CLI |
GITHUB_TOKEN |
deploy.yml |
Built-in token; ghcr.io Docker registry login |
5. vercel.json (hosting config)
Re-verify: cat vercel.json
| Field | Value | Why |
|---|---|---|
version: 2 |
— | Vercel platform v2 config schema |
zero-config |
Vercel automatically detects CRA | Runs npm run build (CRA) and serves the static build/ directory — this is a static SPA, no server functions on Vercel |
headers /(.*) |
Security headers on every response | X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (denies camera/mic/geolocation/etc.), X-Frame-Options: DENY, Strict-Transport-Security: max-age=31536000; includeSubDomains. Do not weaken these |
/static/** |
no explicit Cache-Control |
Deliberate: an explicit immutable header could also be applied to a missing-chunk 404 and pin it for a year. Hashed assets use Vercel's built-in static caching |
headers SPA routes and /index.html |
Cache-Control: public, max-age=0, must-revalidate |
The HTML references the hashed bundles, so browsers must revalidate it on every navigation (matches Vercel's default; pinned so it cannot silently regress) |
rewrites /((?!static/).*) → /index.html |
— | SPA fallback so React Router handles deep links. Excludes /static/ so a missing chunk 404s instead of returning HTML. Rewrites run after the filesystem, so real files (images, manifest) are served first |
src/utils/chunkLoadRecovery.ts |
One automatic reload per 60s on a stale-chunk error | Self-heals tabs opened before a deploy; guarded by sessionStorage so it cannot loop |
6. Database-resident config (admin-editable "flags")
These are runtime knobs living in Postgres rows — changeable without a deploy, via /admin UI or SQL. Schema source of truth: supabase/migrations/.
Re-verify: ls supabase/migrations | grep -i "setting\|application_link\|publish\|knowledge"
application_links — application/interest-form windows
Source: supabase/migrations/20260604000000_create_application_links.sql.
- Nine allowed keys (CHECK constraint):
ace_application,house_fall,house_winter,house_spring,intern_application,cabinet_application,vcn_stage_ninja_interest,vcn_props_team_interest,wnc_team_form. - Per-row knobs:
open_at/due_at(window; CHECKdue_at > open_at),is_enabled,title,description,button_label,target_url,before_open_message,after_close_message,sort_order. - Privacy invariant: base table has NO public read policy (admin-only CRUD). Public reads go through the
public_application_linksview (defined at line ~125 of the migration):target_urlisnullunlessis_enabled AND now() BETWEEN open_at AND due_at, and a computedstatuscolumn returns'disabled' | 'not_open' | 'closed' | 'open'. Closed/future URLs must never be exposed publicly — do not "simplify" this into a public-select policy on the base table. (View grant mechanics:vsa-supabase-security-reference.) - Audit trigger auto-fills
created_by/updated_byfromauth.uid().
events.is_published — event draft/publish flag
Source: supabase/migrations/20260602000000_add_event_publish_status.sql. Boolean, default true (pre-existing events stayed public). Public select policy is using (is_published = true); a separate admin policy sees all. Every public consumer must exclude drafts — the RLS policy enforces it even if frontend code forgets.
site_settings — global site row
Source: supabase/migrations/20260424000000_create_site_settings.sql (+ ..._fix_site_settings_rls.sql). Singleton row id='global' with logo_url, logo_alt. Public read, admin update via /admin/settings. Also creates the public site_assets storage bucket (admin upload/delete).
uvsa_network_page_settings — UVSA Network page copy
Source: supabase/migrations/20260704000000_create_uvsa_network_page_settings.sql. Singleton row id='main' (CHECK-enforced) holding ~15 text columns of hero/intro/stats/showcase/empty-state copy, each with a sensible default. Public read, admin manage. Pattern to copy when making page copy admin-editable.
ai_knowledge_base — Ask VSA v2 knobs
Sources: table + is_active/is_public (both default true) created in supabase/migrations/20260526000002_create_ai_assistant_tables.sql; v2 columns added in 20260704000000_ai_knowledge_v2_schema.sql: aliases text[], confidence (default 'high'), freshness (default 'stable'), academic_year, valid_until timestamptz. Public read policy requires is_public = true AND is_active = true. Dedupe convention (20260704000001_ai_knowledge_v2_dedupe.sql): deactivate rows (is_active=false), never delete. Behavior rules live in the Edge Function SYSTEM_PROMPT; only volatile FACTS belong in these rows. Deploy ordering (migrations → frontend → edge function) is owned by vsa-run-and-operate.
Seasonal state — computed, NOT configured
src/utils/seasonalState.ts: season is derived from the date (America/Los_Angeles): summer_break between June 15 and Sept 15 (constants SUMMER_BREAK_START/END), else active_year. There is no DB flag or env var for it — to change the window you edit the constants and ship a build. Policy for what each season means: vsa-seasonal-operations.
7. Build-time config files
Re-verify: node -e "const p=require('./package.json');console.log(Object.keys(p.jest),p.browserslist)" and head -5 tailwind.config.js tsconfig.json
| File | Config that matters |
|---|---|
tailwind.config.js |
darkMode: 'class'; content: ['./src/**/*.{js,jsx,ts,tsx}']; brand token scales (brand, coral, gold…). Token semantics live in vsa-design-system-reference |
tsconfig.json |
strict: true, noEmit: true (CRA's Babel does the emitting), jsx: react-jsx, target: es5, isolatedModules: true |
package.json » jest |
`transformIgnorePatterns: ["node_modules/(?!(zod |
package.json » browserslist |
production: >0.2%, not dead, not op_mini all; development: last 1 chrome/firefox/safari |
supabase/config.toml |
Local Supabase CLI stack config; line 74 still contains Supabase Studio's openai_api_key = "env(OPENAI_API_KEY)". This is not a VSA Edge Function consumer; no current supabase/functions code reads it after issue #353. |
8. How to add a new configuration axis — checklist
First decide WHERE it lives:
| Choose | When | Existing pattern to copy |
|---|---|---|
| DB row (admin-editable) | Non-engineers must change it without a deploy (copy, links, windows, on/off per content item) | Singleton table: uvsa_network_page_settings (CHECK-locked single id, defaults, public read + admin manage). Windowed/masked: application_links (no public base-table read, masking view) |
| Env var / secret | Differs per environment, or is secret | Frontend behavior flag: REACT_APP_SUPABASE_IMAGE_TRANSFORMS (=== 'true', default off). Server secret: Edge Function Deno.env.get with explicit getRequiredEnv-style failure |
| Code constant | Changes at most yearly and needs tests | seasonalState.ts date constants (has a test file) |
Then:
- Secrets never go client-side. Anything
REACT_APP_*ships in the public bundle. Service-role keys, API keys, webhook secrets → Edge Function secrets or GitHub Actions secrets only. - Default to safe. Flags that guard risky behavior must require the explicit string
'true'to activate (pattern:CONFIRM_DELETE,RLS_ALLOW_MUTATION_TESTS,REACT_APP_SUPABASE_IMAGE_TRANSFORMS). Kill switches should disable only on explicit"false"(pattern:VSA_AI_ASSISTANT_ENABLED). - New config TABLE ⇒ full RLS treatment: enable RLS, admin policies via
user_profiles.is_admin, and if you add any VIEW, apply revoke-all-then-grant-SELECT (the ALTER DEFAULT PRIVILEGES / auto-updatable-view gotcha — seevsa-supabase-security-referencebefore writing the migration). - Never publicly expose inactive/closed values (application_links masking is the precedent).
- Update
.env.examplefor any new frontend/script env var (placeholder value + comment; never a real value). Note the existing drift in §1 — don't make it worse. - Wire every consumer or don't add it: the
REACT_APP_PLAUSIBLE_*block sat in.env.examplefor months reading nothing, and was removed 2026-07-28 — that is what unconsumed config drift costs. - Classify the change with
vsa-change-control(config touching points/attendance/leaderboard/RLS is gated). - Update THIS skill's catalog and its re-verification command.
Provenance and maintenance
All facts verified against the repo on branch codex/reactbits-ui, 2026-07-06. Sources: .env.example, vercel.json, package.json, tsconfig.json, tailwind.config.js, src/lib/{supabase,analytics,supabaseImages}.ts, src/setupTests.ts, src/utils/seasonalState.ts, src/components/features/ai/VsaAiAssistant.tsx, scripts/{verify-rls-security,cleanup-stale-photo-requests}.mjs, scripts/migrate-supabase-images-to-public.ts, supabase/functions/*/index.ts, supabase/config.toml, .github/workflows/{deploy,migrate-images,migrate-event-images}.yml, supabase/migrations/{20260424000000_create_site_settings,20260602000000_add_event_publish_status,20260604000000_create_application_links,20260704000000_create_uvsa_network_page_settings,20260526000002_create_ai_assistant_tables,20260704000000_ai_knowledge_v2_schema,20260704000001_ai_knowledge_v2_dedupe}.sql, README.md.
Drift re-verification one-liners (run all before relying on a section):
grep -rn "process.env.REACT_APP" src --include="*.ts" --include="*.tsx" # §1 frontend vars
cat .env.example # §1 example drift
grep -n "process.env" scripts/*.mjs scripts/*.ts # §2 script vars
grep -rn "Deno.env.get" supabase/functions # §3 edge secrets
grep -n "secrets\." .github/workflows/*.yml # §4 CI secrets
cat vercel.json # §5 hosting
ls supabase/migrations | grep -i "setting\|application_link\|publish\|knowledge\|ai_assistant" # §6 DB config
grep -n "application_key in" -A 12 supabase/migrations/20260604000000_create_application_links.sql # §6 nine keys
node -e "console.log(require('./package.json').jest, require('./package.json').browserslist)" # §7 build config
grep -rn "PLAUSIBLE\|REACT_APP_OPENAI" src scripts public .env.example 2>/dev/null # §1 dead client config (expect no hits; .env.example documents the server-side OPENAI_API_KEY on purpose)