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()atweb/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()inonSuccess
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) —*WithArgsvariants take an optional arg (e.g. an optionalprojectIdthat becomes a.eq(), pushing the filter to Postgres instead of client-filtering)createMutation(createMutation.ts) — multi-key / function-forminvalidateKeys,returningfor.select().single()createRpcMutation(createMutation.ts) — for Supabase RPCsmakeStorageAttachmentHooks— foruse<Entity>Pdf-style storage-attachment hook pairs- low-level builders:
tableBuilder/callRpcinweb/src/lib/hooks/supabase-builders.tsHooks 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), andCreateEntityDialog— the standard form-dialog shell (props:submitLabel/pendingLabel/maxWidth/extraFooterLeft/showPendingSpinner/trigger/description: ReactNode/contentClassName; it wraps children in a<form>so nested buttons needtype="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-authEdge Function. The Supabase client is NOT anon-only — every request carriesAuthorization: Bearer <jwt>withsub = person.id.
- Cloudflare Access ZTNA authenticates users at the edge; sets
CF_Authorizationcookie - SPA boots →
cf-auth.tsreads cookie viacf-access.ts→ POSTs it to thecf-access-authEdge Function - Edge Function verifies CF JWT against CF Access JWKS, looks up
personby email, ensuresauth.usersrow withid = person.id, returns Supabase access_token (~1h,sub = person.id,role = "authenticated") supabase.tsxattaches that token to every fetch + handles background refresh and 401 retry- RLS keys off
auth.uid()(= person.id); per-user tables likenotification,notification_preference,push_subscriptionrely on this - 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 thantsc --noEmitverbatimModuleSyntax: true- explicittypekeyword for type imports- Always verify:
cd web && npx tsc -bbefore pushing
Bilingual
- EN + HU required for all UI strings
useTranslation()hook from i18next- Locale files:
web/src/locales/en.jsonandhu.json
Architecture Quick Ref
| Component | Location | Notes |
|---|---|---|
| Admin CRM | web/ | React 19 + Vite 7, main SPA |
| Client portal | userspace/ | React 19, Clerk auth, separate instance |
| UI primitives | packages/ui/ | shadcn/Radix, CLI-managed, don’t edit |
| Shared types | packages/shared/src/db-mappings.ts | Domain type aliases |
| Generated types | packages/shared/src/database.types.ts | ~2400 lines, auto-generated |
| Edge functions | web/supabase/functions/ | Deno, 10 functions |
| Migrations | web/supabase/migrations/ | SQL, sequential |
| Sync agents | agents/ | Rust (GitHub/GitLab), TS (Jira) |
| Agent supervisor | crm_communicator_v2/ | Rust CLI + daemon + TUI |
| Reminders agent | agent/ | Swift, macOS EventKit |
| File drop | filedrop/ | macOS Share Extension |
Code Conventions
| Convention | Rule |
|---|---|
| File naming | PascalCase .tsx components, camelCase use*.ts hooks, kebab-case utils |
| Exports | Named exports for components/hooks, default for pages |
| Imports | React first, third-party, internal, hooks, type-only last |
| Styling | Tailwind + cn() utility, dark theme, semantic tokens |
| Git | Conventional commits: feat(scope):, fix(scope):, chore: |
| Push | Direct to origin/master (no PR workflow, solo dev) |
| DnD | Never put dnd-kit refs on Radix ScrollArea - use plain div wrapper |
| No comments | Code should be self-explanatory, no inline comments or docstrings |
Key Domain Types
| Type | Source | Notes |
|---|---|---|
| TaskView | db-mappings.ts | Task with relations |
| ProjectView | db-mappings.ts | Project with details |
| InvoiceView | db-mappings.ts | Has name field, NOT description |
| WorkflowStatus | db-mappings.ts | Has category field, NOT is_closed |
| DayPlanBlockView | db-mappings.ts | Includes joined task/project/related_tasks |
| BudgetEnvelope | db-mappings.ts | Four types: BUDGET, SAVINGS_GOAL, SAVINGS_TARGET, TRACKING |
Edge Functions
| Function | Purpose |
|---|---|
| cf-access-auth | Exchange CF Access token for Supabase JWT |
| billingo-sync-statuses | Invoice status sync from Billingo |
| billingo-sync-inbound | Vendor bill sync from Billingo |
| sync-outbound | Outbound task sync to Linear (possibly dead — see tech-debt) |
| market-data | Commodity prices from Yahoo Finance (Gold, Brent, TTF) |
| rss-fetch | RSS feed ingestion |
| service-health | Tailscale device health checks (wired into the app shell/sidebar) |
| ingest-email | Email ingestion (via CF Worker) |
| send-push | Web Push delivery (fired by send_push_on_notification trigger via pg_net) |
notifyremoved from the repo (2026-05-11)The legacy
notifyEdge Function was deleted fromweb/supabase/functions/in the DRY pass — superseded by theevent → fn_event_to_notifications → emit_notificationchain. 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 changesDeeper Reading
- levandor-crm - Full project overview
- security - Auth architecture (CF ZTNA + Supabase RLS)
- integrations - External integrations reference
- budget - Budget/envelope system
- timesheet-export - Timesheet export wizard
- tech-debt - Known technical debt
- kanban - Kanban board architecture
- day-planner - Day planner system
- debugging-log-crm - Past bugs and fixes
- dry-refactor-2026-05-11 - 2026-05-11 DRY/simplification pass — new shared abstractions to reuse, deferred items, behavior changes