This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Coffee Club is a map-first cafe discovery and community moderation app for Islamabad, built on TanStack Start (React 19, SSR) with Supabase persistence. Public users browse an interactive Leaflet map filtered by features/vibes/hours, search venues, recommend new spots, and post notes/photo "memories". All community content is moderated: submissions start pending and only approved content reaches the public map. A token-gated /admin dashboard handles review.
npm run dev # Vite dev server on port 3000
npm run build # production build
npm run preview # preview the build
npm run test # Vitest (jsdom) — run once
npm run check # Biome format + lint + organize-imports (the canonical check)
npm run lint # Biome lint only
npm run format # Biome format only
npm run seed:supabase # upsert seed-data.ts into Supabase (needs server creds)Run a single test: npx vitest run src/lib/types.test.ts or filter by name with npx vitest run -t "name". Use npx vitest (no run) for watch mode.
Biome uses tab indentation and double quotes; it ignores src/routeTree.gen.ts and src/styles.css. The editor auto-organizes imports on save. Run npm run check before considering work done — there is no separate tsc step in the scripts (type-checking happens via the IDE / build).
The data layer auto-selects its backend at runtime in coffee-db.server.ts:
- Supabase is used when
VITE_SUPABASE_URL,VITE_SUPABASE_PUBLISHABLE_KEY, andSUPABASE_SERVICE_ROLE_KEYare all set. Server code uses the service-role client (RLS-bypassing) for all reads/writes. - Local JSON fallback (
.local-data/coffee-club.json, seeded fromsrc/lib/seed-data.ts) is used in development when Supabase is not configured. In production, missing Supabase credentials throw — the local store is never a production backend.
COFFEE_CLUB_ADMIN_TOKEN gates all admin server functions via assertAdminAccess. Copy .env.example to configure. The admin UI stores the entered token in localStorage (coffee-club-admin-token) and sends it with every admin request — there is no real auth/session yet.
Data flows client component → TanStack Query → server function → data-access layer → Supabase or local JSON. Keep these layers distinct:
src/lib/coffee.functions.ts— TanStack Start server functions (createServerFn). The RPC boundary. Each validates input with a Zod schema (inputValidator) then delegates to the data layer. This is the only thing client code calls for data.src/lib/coffee-db.server.ts—.server.tsdata-access layer (never imported by client). Owns backend selection, Supabase queries, the local-store read/write, row↔domain mapping (snake_case DB rows → camelCase domain types), moderation filtering, asset upload, andadmin_audit_logwrites. Public reads return onlyapprovedcontent; admin reads include all statuses.src/lib/schemas.ts— Zod schemas for every server-function input.parseLocationActivityFormDatahandles the multipart (file upload) path specially.src/lib/types.ts— domain types plus the shared pure filter functions (filterLocations,filterCommunityPins,isWithinBounds) used by both the data layer and the client, and the tag/filter taxonomy (LOCATION_TAG_DEFINITIONS,FILTER_GROUPS).LocationFeatureKey(thehasWifi/hasWork/… union) is the single source of truth for filterable features — adding a feature touches this union, the definitions array, the DB row types, and the migration.src/lib/query-options.ts—queryOptionsfactories shared by route loaders and components, keeping query keys/staleTimes consistent.
File-based routes in src/routes/ (__root.tsx, index.tsx, admin.tsx); routeTree.gen.ts is generated — never edit it (it's also marked read-only in VS Code). The router (src/router.tsx) wires TanStack Query into SSR via setupRouterSsrQueryIntegration, so route loaders call ensureQueryData to prefetch and components read the same query with useSuspenseQuery/useQuery.
src/components/coffee-club/ holds the UI. Two big stateful screens: home-page.tsx (public map, filters, cards, submission modals) and admin-page.tsx (moderation dashboard). map-panel.client.tsx uses the .client.tsx suffix because Leaflet is browser-only — it's loaded via lazy/ClientOnly and must never run during SSR. Keep Leaflet/window-dependent code inside client-only boundaries.
Migrations in supabase/migrations/: ..._init_coffee_club.sql (base tables) and ..._backend_admin_pipeline.sql (moderation fields, profiles, roles, claims, lists, reports, audit log, and RLS policies). scripts/seed-supabase.ts upserts seed-data.ts and calls the sync_coffee_club_sequences RPC to fix ID sequences after manual-ID inserts. When changing a table shape, update in lockstep: the SQL migration, the *Row types + mappers in coffee-db.server.ts, the domain type in types.ts, and the seed script.
- Path alias
#/*→src/*(also@/*); prefer#/…imports as the codebase does. - Server-only modules use the
.server.tssuffix and must not be imported into client bundles. - DB columns are snake_case; domain types are camelCase — convert only in the mapper functions in
coffee-db.server.ts. - Geocoding (
geocode.server.ts) calls the public OpenStreetMap Nominatim API and biases queries to Islamabad; it has no API key and is rate-limited, so avoid hammering it in loops.