Infrastructure Gotchas

Three durable, non-obvious infrastructure traps in the Levandor CRM monorepo, found during a 2026-06-09 codebase validation. Each contradicts a checked-in doc (mostly the root CLAUDE.md and supabase/CLAUDE.md), so an agent that trusts those docs will be misled.

For Agents — trust order

When the in-repo docs disagree: web/CLAUDE.md is correct; the root /CLAUDE.md and supabase/CLAUDE.md are partially STALE. This note records exactly where, with ground-truth verified against git ls-tree HEAD, web/package.json, and the source files on 2026-06-09.

1. Migrations: two directories, only one is live

Put new migrations in web/supabase/migrations/, timestamped — NOT in supabase/migrations/

The live Supabase project (mkofmdtdldxgmmolxxhc) tracks timestamped migrations. The numbered 00N_ files are a stale secondary copy.

DirectoryNamingStatus
web/supabase/migrations/YYYYMMDDHHMMSS_<name>.sql (timestamped)LIVE — what the remote project actually tracks (~79 files)
supabase/migrations/00N_<name>.sql (numbered: 001_client_portal_schema010_disclosure_schema)STALE / secondary copy — bodies were re-applied under timestamped names
  • Migrations are applied to the remote via the Supabase MCP apply_migration tool (which records a timestamp version), or supabase db push.
  • There is NO supabase/config.toml anywhere in the repo, and NO CI / GitHub Actions that auto-applies a migrations folder (.github/workflows/ has nothing migration-related). Nothing links the numbered folder to the live DB.
  • The numbered 00N_ files still get written inside feature commits (e.g. dfa7666 feat(disclosure): crm_vuln_lifecycle … tables, 0748631 … promote_vuln RPCs) — so they look authoritative but are not the applied source.

Rule: New migration → web/supabase/migrations/<timestamp>_<name>.sql, apply via MCP apply_migration (or supabase db push). Do NOT add new numbered 00N_ files to supabase/migrations/.

Contradicts supabase/CLAUDE.md

supabase/CLAUDE.md shows migrations living in supabase/migrations/ with only 001_client_portal_schema.sql and says “Run migrations via Supabase CLI or dashboard.” Both the location emphasis and the file list are stale.

2. Generated DB types are duplicated in two files — and the regen script touches the wrong one

types.ts is the file most code actually imports, but gen-types only regenerates database.types.ts

The two files drift unless you regenerate both.

Two near-identical ~146K generated copies exist:

FileSizeWho imports it
packages/shared/src/types.ts~146KThe live one. db-mappings.ts imports Tables/Enums/TablesInsert from ./types; the @levandor/shared barrel (index.ts) re-exports Database/DashboardSummary from ./types. So nearly every @levandor/shared consumer transitively uses types.ts.
packages/shared/src/database.types.ts~145KA second copy; some components import it directly.

The trap

The gen-types script in web/package.json writes only to database.types.ts:

"gen-types": "supabase gen types typescript --project-id mkofmdtdldxgmmolxxhc --schema public > ../packages/shared/src/database.types.ts"

But db-mappings.ts and the shared barrel read from types.ts. Running gen-types alone updates the copy that most code does NOT import. After a schema change you must regenerate (or hand-sync) both files, or the live types.ts silently goes out of date.

  • The script correctly targets the remote project (--project-id mkofmdtdldxgmmolxxhc).
  • The supabase/CLAUDE.md snippet showing supabase gen types typescript --local > … database.types.ts is stale on two counts: it uses --local (no local stack is the source of truth here) and it only writes the one file.

3. Auth is Cloudflare ZTNA, NOT Clerk — the root docs are wrong

The admin CRM does NOT use Clerk. Root /CLAUDE.md and supabase/CLAUDE.md are wrong.

supabase/CLAUDE.md literally says “Clerk handles authentication.” This is FALSE for the live admin app.

Real flow (per web/CLAUDE.md, which is correct — see security for the full version):

  1. Cloudflare Zero Trust (ZTNA) authenticates at the edge → sets the CF_Authorization cookie. There is no in-app sign-in page, no Clerk in web/.
  2. The SPA lifts the cookie and POSTs it to the cf-access-auth edge function, which verifies the CF JWT against CF Access JWKS, looks up person by email, and mints a Supabase access token with sub = person.id (role = "authenticated", ~1h).
  3. RLS keys off auth.uid() / auth.jwt()->>'email' (via the get_my_person_id() helper).

RLS shape

Most RLS tables use authenticated-role policies with USING (true) — auth is enforced upstream at the Cloudflare edge, so the inner DB policies are permissive for any authenticated principal. service_role FOR ALL policies exist for edge-function workers. (Per-user tables like notification are the exception and lock down to auth.uid().)

Establishing migrations (in web/supabase/migrations/):

  • 20260331000000_auth_cfztna_migration.sql
  • 20260401010000_ztna_open_all_rls.sql
  • 20260426100000_enforce_authenticated_rls.sql

Clerk is NOT entirely absent from the repo

The client portal (userspace/) — a separate app for external clients — does use Clerk. The trap is specifically the root/supabase docs claiming Clerk for the admin CRM (web/), which is wrong.

Verification snapshot (2026-06-09)

For Agents

Ground-truth checks behind this note, in case you want to re-verify:

  • git ls-tree -r --name-only HEAD -- supabase/migrations → lists 001_010_ numbered files (the stale copy).
  • ls web/supabase/migrations | wc -l → ~79 timestamped files (the live copy).
  • No supabase/config.toml; .github/workflows/ has no migration-applying job.
  • packages/shared/src/{types.ts,database.types.ts} → both ~146K; db-mappings.ts + index.ts import from ./types; gen-types (web/package.json) writes only database.types.ts.
  • supabase/CLAUDE.md:24 → “Clerk handles authentication.” (stale)