Levandor CRM - Agent Quick Context

Agent Onboarding

Read this first when working on the Levandor CRM project.

What is it?

Personal business management CRM. React 19 SPA → Supabase (no custom backend). Monorepo with pnpm workspaces. Admin CRM is protected by Cloudflare Access ZTNA at the edge (no in-app auth flow).

Critical Knowledge

Data Layer Pattern

Component → useMutation/useQuery hook → supabaseQuery<T>() → Supabase client → Postgres
                                              ↓
                                    TanStack Query cache invalidation
  • All queries go through supabaseQuery() at web/src/lib/query.ts - wraps Supabase {data, error} and throws errors
  • ~80 hooks in web/src/lib/hooks/ with barrel export
  • Query hooks: useSupabase() + useQuery() + supabaseQuery<ViewType>()
  • Mutation hooks: useMutation() + queryClient.invalidateQueries() in onSuccess

Prefer the hook factories (since 2026-05-11)

~50 CRUD/read hooks are now built by factories — don’t hand-roll a plain table query or a single-table mutation, use these:

  • createTableQuery / createMaybeSingleQuery / createSingleQuery / createTableQueryWithArgs / createMaybeSingleQueryWithArgs (web/src/lib/hooks/createTableQuery.ts) — *WithArgs variants take an optional arg (e.g. an optional projectId that becomes a .eq(), pushing the filter to Postgres instead of client-filtering)
  • createMutation (createMutation.ts) — multi-key / function-form invalidateKeys, returning for .select().single()
  • createRpcMutation (createMutation.ts) — for Supabase RPCs
  • makeStorageAttachmentHooks — for use<Entity>Pdf-style storage-attachment hook pairs
  • low-level builders: tableBuilder / callRpc in web/src/lib/hooks/supabase-builders.ts

Hooks with transforms / key-remaps / upserts / functions.invoke / extra side-effects are deliberately still hand-rolled. Also reach for the shared list-page primitives before re-rolling UI: PageHeader, ExportCsvButton, CreateButton, ErrorBanner, CardSkeleton+SkeletonLines, ResourceTable(+Column<T>), TaskChips/getTaskCardModel, useUrlListState (URL-search-param list state), and CreateEntityDialog — the standard form-dialog shell (props: submitLabel/pendingLabel/maxWidth/extraFooterLeft/showPendingSpinner/trigger/description: ReactNode/contentClassName; it wraps children in a <form> so nested buttons need type="button"). See dry-refactor-2026-05-11 for the full catalogue.

Auth Flow (Admin CRM)

Auth flow (current)

Admin CRM does NOT use Clerk. CF Access ZTNA at the edge + Supabase JWT issued per-session by the cf-access-auth Edge Function. The Supabase client is NOT anon-only — every request carries Authorization: Bearer <jwt> with sub = person.id.

  1. Cloudflare Access ZTNA authenticates users at the edge; sets CF_Authorization cookie
  2. SPA boots → cf-auth.ts reads cookie via cf-access.ts → POSTs it to the cf-access-auth Edge Function
  3. Edge Function verifies CF JWT against CF Access JWKS, looks up person by email, ensures auth.users row with id = person.id, returns Supabase access_token (~1h, sub = person.id, role = "authenticated")
  4. supabase.tsx attaches that token to every fetch + handles background refresh and 401 retry
  5. RLS keys off auth.uid() (= person.id); per-user tables like notification, notification_preference, push_subscription rely on this
  6. Client portal (userspace/) is a separate world — uses Clerk for external clients

See security for full auth architecture.

TypeScript Strictness

  • tsc -b (project build mode) in CI - stricter than tsc --noEmit
  • verbatimModuleSyntax: true - explicit type keyword for type imports
  • Always verify: cd web && npx tsc -b before pushing

Bilingual

  • EN + HU required for all UI strings
  • useTranslation() hook from i18next
  • Locale files: web/src/locales/en.json and hu.json

Architecture Quick Ref

ComponentLocationNotes
Admin CRMweb/React 19 + Vite 7, main SPA
Client portaluserspace/React 19, Clerk auth, separate instance
UI primitivespackages/ui/shadcn/Radix, CLI-managed, don’t edit
Shared typespackages/shared/src/db-mappings.tsDomain type aliases
Generated typespackages/shared/src/database.types.ts~2400 lines, auto-generated
Edge functionsweb/supabase/functions/Deno, 10 functions
Migrationsweb/supabase/migrations/SQL, sequential
Sync agentsagents/Rust (GitHub/GitLab), TS (Jira)
Agent supervisorcrm_communicator_v2/Rust CLI + daemon + TUI
Reminders agentagent/Swift, macOS EventKit
File dropfiledrop/macOS Share Extension

Code Conventions

ConventionRule
File namingPascalCase .tsx components, camelCase use*.ts hooks, kebab-case utils
ExportsNamed exports for components/hooks, default for pages
ImportsReact first, third-party, internal, hooks, type-only last
StylingTailwind + cn() utility, dark theme, semantic tokens
GitConventional commits: feat(scope):, fix(scope):, chore:
PushDirect to origin/master (no PR workflow, solo dev)
DnDNever put dnd-kit refs on Radix ScrollArea - use plain div wrapper
No commentsCode should be self-explanatory, no inline comments or docstrings

Key Domain Types

TypeSourceNotes
TaskViewdb-mappings.tsTask with relations
ProjectViewdb-mappings.tsProject with details
InvoiceViewdb-mappings.tsHas name field, NOT description
WorkflowStatusdb-mappings.tsHas category field, NOT is_closed
DayPlanBlockViewdb-mappings.tsIncludes joined task/project/related_tasks
BudgetEnvelopedb-mappings.tsFour types: BUDGET, SAVINGS_GOAL, SAVINGS_TARGET, TRACKING

Edge Functions

FunctionPurpose
cf-access-authExchange CF Access token for Supabase JWT
billingo-sync-statusesInvoice status sync from Billingo
billingo-sync-inboundVendor bill sync from Billingo
sync-outboundOutbound task sync to Linear (possibly dead — see tech-debt)
market-dataCommodity prices from Yahoo Finance (Gold, Brent, TTF)
rss-fetchRSS feed ingestion
service-healthTailscale device health checks (wired into the app shell/sidebar)
ingest-emailEmail ingestion (via CF Worker)
send-pushWeb Push delivery (fired by send_push_on_notification trigger via pg_net)

notify removed from the repo (2026-05-11)

The legacy notify Edge Function was deleted from web/supabase/functions/ in the DRY pass — superseded by the event → fn_event_to_notifications → emit_notification chain. Before removing it server-side, confirm no Supabase-dashboard Database Webhook still points at it.

Context Providers

Provider stack in App.tsx:

BrowserRouter
  └─ SupabaseProvider        ← Supabase client w/ CF→Supabase JWT exchange (Authorization: Bearer)
      └─ QueryClientProvider
          └─ CurrencyProvider   ← HUF/EUR/USD live rates
              └─ CurrentPersonProvider  ← person lookup by CF Access email
                  └─ AppShell > PageLayout > Routes

Common Commands

pnpm --filter web dev       # Dev server
pnpm --filter web build     # tsc -b && vite build
pnpm --filter web test      # vitest run
pnpm --filter web deploy    # Build + wrangler deploy to CF Pages
cd web && npx tsc -b        # Type check (CI-equivalent)
npx supabase gen types      # Regenerate database.types.ts after schema changes

Deeper Reading