From 2c23906716641df5f792679abf62922169e55c41 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 14 May 2026 16:01:31 +0200 Subject: [PATCH] docs: map existing codebase --- .planning/codebase/ARCHITECTURE.md | 162 ++++++++++++++++ .planning/codebase/CONCERNS.md | 126 ++++++++++++ .planning/codebase/CONVENTIONS.md | 161 ++++++++++++++++ .planning/codebase/INTEGRATIONS.md | 140 ++++++++++++++ .planning/codebase/STACK.md | 142 ++++++++++++++ .planning/codebase/STRUCTURE.md | 296 +++++++++++++++++++++++++++++ .planning/codebase/TESTING.md | 141 ++++++++++++++ 7 files changed, 1168 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..d237a89 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,162 @@ +# Architecture + +_Last updated: 2026-05-14_ + +This document describes the high-level architecture of the `xtablo-source` monorepo: how apps, packages, and external services fit together, the dominant data-flow patterns, and where the key abstractions live. + +## High-Level Diagram + +``` + +-----------------------------------------------------+ + | Frontend Apps | + | | + | apps/main apps/external apps/clients | + | (dashboard) (booking widget) (client portal)| + | apps/admin | + | (internal admin) | + +------+----------------+--------------------+--------+ + | | | + | shared packages (source-only) | + | @xtablo/shared @xtablo/ui | + | @xtablo/shared-types | + | @xtablo/auth-ui @xtablo/chat-ui | + | @xtablo/tablo-views | + | | + v v + +-----------------+ +---------------------+ + | apps/api |<------------>| apps/chat-worker | + | (Hono REST) | | (CF Durable Obj) | + +--------+--------+ +----------+----------+ + | | + +----------------+----------------+-----------------+ + | | | | + v v v v + Supabase Stripe Cloudflare R2 Stream Chat + (Postgres+Auth) (payments) (file storage) (messaging) + | + + Datadog (RUM/APM) + + Google Secret Manager +``` + +## Application Layers + +- **Frontend dashboard** (`apps/main`): primary authenticated SPA. Tablos, planning, events, chat, notes, billing. Entry: `apps/main/src/main.tsx`, root component `apps/main/src/App.tsx`. +- **Public booking widget** (`apps/external`): embeddable / floating booking widget. Entry: `apps/external/src/main.tsx`. Query params drive mode (`?mode=embed&eventTypeId=...`). +- **Client portal** (`apps/clients`): public-facing client portal experience. Entry: `apps/clients/src/main.tsx`, routes in `apps/clients/src/routes.tsx`. +- **Admin app** (`apps/admin`): internal admin tools. Entry: `apps/admin/src/main.tsx`, routes in `apps/admin/src/routes.tsx`. +- **API** (`apps/api`): Hono-based REST API serving all frontends. Entry: `apps/api/src/index.ts` (compiled to Node, deployed to Google Cloud Run). +- **Chat worker** (`apps/chat-worker`): Cloudflare Worker with Durable Objects for real-time chat presence. Entry: `apps/chat-worker/src/index.ts`. + +## Data Flow Patterns + +### React Query (TanStack Query v5) — primary server-state tool + +All server state flows through React Query. Default cache time is 5 minutes. Query keys follow a hierarchical convention so that mutations can invalidate just the affected sub-tree: + +```ts +["tablos"] // list +["tablos", tabloId] // single +["tablo-files", tabloId] // related collection +``` + +Hooks live in: +- `apps/main/src/hooks/` — feature hooks (`tablos.ts`, `events.ts`, `tasks.ts`, `availabilities.ts`, `stripe.ts`, `notes.ts`, ...). +- `packages/shared/src/hooks/` — cross-app hooks (`auth.ts`, `book.ts`, `public.ts`). + +### Zustand — global client state + +Used sparingly, primarily for the current user. The user is fetched via React Query then mirrored into a Zustand store so any component can read it synchronously: +- `useUser()` — throws if no session (use inside protected routes). +- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public-aware components). + +Provider: `apps/main/src/providers/UserStoreProvider.tsx` (mirrored in `apps/external/src/UserStoreProvider.tsx`). + +### Direct Supabase queries vs API calls + +Two parallel data-access patterns coexist: + +1. **Direct Supabase** (`supabase.from("table").select()...`) — used from frontend hooks when row-level security is sufficient and no server-side logic is required. Client lives in `packages/shared/src/lib/supabase.ts` (re-exported via `apps/main/src/lib/supabase.ts`). +2. **API calls** (`api.get("/api/v1/...")`) — used when the operation needs the service role key, must run server-side logic (Stripe, file ops, email, multi-tenant integrity), or aggregates data. The HTTP client wrapper lives in `packages/shared/src/lib/api.ts` (re-exported via `apps/main/src/lib/api.ts`) and attaches the Supabase JWT as a Bearer token. + +File operations use specialized mutation hooks (e.g. `useUploadTabloFile`, `useDeleteTabloFile`) that invalidate `["tablo-files", tabloId]` automatically. + +## Authentication Flow + +- **Supabase Auth** issues JWTs on login / passwordless flows. +- **SessionContext** (`packages/shared/src/contexts/SessionContext.tsx`) subscribes to `supabase.auth.onAuthStateChange()` and exposes the current session. +- Frontend HTTP client reads the session token and sends `Authorization: Bearer ` on every API call. +- The API's `supabase` middleware validates the JWT and attaches the resolved user / supabase clients to the Hono context. +- **Passwordless onboarding** generates temporary accounts flagged with `is_temporary: true`. +- **Protected routes** check `useMaybeUser()` and redirect to landing when null. +- **Client portal** has a parallel auth path: magic-link based, signed cookies issued by `apps/api/src/routers/clientAuth.ts`. Configurable TTLs, cookie domain, JWT secret are passed into the router factory. + +## API Architecture (Hono) + +Entry: `apps/api/src/index.ts`. Flow: + +1. `loadSecrets()` pulls secrets (locally from env, in staging/prod from Google Secret Manager) — `apps/api/src/secrets.ts`. +2. `createConfig(secrets)` produces the typed `AppConfig` — `apps/api/src/config.ts`. +3. `MiddlewareManager.initialize(config)` constructs the middleware singleton — `apps/api/src/middlewares/middleware.ts`. +4. The root Hono app applies `logger()` and a CORS middleware that only accepts `*.xtablo.com` and `localhost` origins. +5. All routes mount under `/api/v1` via `getMainRouter(config)` (`apps/api/src/routers/index.ts`). + +### Middleware Manager (singleton pattern) + +`MiddlewareManager` builds each piece of middleware once on init and exposes them as instance properties. The main router pulls them via `MiddlewareManager.getInstance()` and chains them in this fixed order: + +``` +supabase -> r2 -> transporter -> stripe -> stripeSync +``` + +Auxiliary middleware modules: +- `apps/api/src/middlewares/middleware.ts` — central singleton and supabase / r2 / email / stripe middlewares. +- `apps/api/src/middlewares/stripeSync.ts` — bidirectional Supabase <-> Stripe sync engine. +- `apps/api/src/middlewares/transporter.ts` — email transporter. + +### Router ordering + +Public-first, then authenticated. From `apps/api/src/routers/index.ts`: + +1. `/public` — unauthenticated (`public.ts`). +2. `/tasks` — task router (`tasks.ts`). +3. `/revenuecat-webhook`, `/stripe-webhook` — webhook receivers. +4. `/admin` — admin-only routes (`admin.ts`, `adminAuth.ts`, ...). +5. `/client-auth`, `/client-portal`, `/client-invites` — client portal stack. +6. `/` — `maybeAuthRouter.ts` (optional auth — must come before authed to allow public booking). +7. `/` — `authRouter.ts` (requires JWT). + +The exported `ApiRoutes` type (`ReturnType`) enables Hono RPC clients to consume the API in a type-safe way. + +## Key Abstractions + +- **`packages/shared`** is the central runtime sharing point. Re-exports cover contexts (`SessionContext`, `ThemeContext`), cross-app hooks, the API client, the Supabase client, toast helpers, and shared types. Public surface in `packages/shared/src/index.ts`. +- **`packages/ui`** — Radix + Tailwind component library. Source-only. Components in `packages/ui/src/components/` (`button.tsx`, `dialog.tsx`, `select.tsx`, ...). +- **`packages/shared-types`** — zero-runtime-dependency TypeScript types. Auto-generated `database.types.ts` + hand-written domain layers (`tablos.types.ts`, `tablo-data.types.ts`, `events.types.ts`, `kanban.types.ts`, `stripe.types.ts`, `admin.types.ts`). +- **`packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views`** — feature-scoped UI packages, also source-only. +- **Query keys** — convention enforced by colocation in `apps/main/src/hooks/` and feature naming (`["tablos", id]`, `["tablo-files", id]`, etc.). + +## Source-Only Package Pattern + +`@xtablo/shared`, `@xtablo/ui`, `@xtablo/shared-types`, `@xtablo/auth-ui`, `@xtablo/chat-ui`, and `@xtablo/tablo-views` export TypeScript directly with no build step. Consumers import source files; Vite handles transpilation and HMR. Benefits: instant updates, no watch processes, simpler dependency graph. Constraint: no circular dependencies between packages, and the API can only depend on `@xtablo/shared-types` (pure types, no React). + +## Entry Points + +| App / package | Entry file | +|----------------------|---------------------------------------------------------| +| `apps/main` | `apps/main/src/main.tsx` -> `App.tsx` | +| `apps/external` | `apps/external/src/main.tsx` -> `routes.tsx` | +| `apps/clients` | `apps/clients/src/main.tsx` -> `App.tsx` / `routes.tsx` | +| `apps/admin` | `apps/admin/src/main.tsx` -> `routes.tsx` | +| `apps/api` | `apps/api/src/index.ts` -> `routers/index.ts` | +| `apps/chat-worker` | `apps/chat-worker/src/index.ts` | +| `@xtablo/shared` | `packages/shared/src/index.ts` | +| `@xtablo/ui` | `packages/ui/src/components/index.ts` | +| `@xtablo/shared-types` | `packages/shared-types/src/index.ts` | + +## Build & Deployment Notes + +- **Turborepo** orchestrates tasks with caching (`turbo.json` at repo root). +- `apps/main` and other Vite apps deploy to **Cloudflare Workers** via `wrangler.toml` and the bundled `worker/` folder. +- `apps/api` compiles TypeScript and deploys to **Google Cloud Run**; secrets resolved via Google Secret Manager. +- `apps/chat-worker` deploys as a **Cloudflare Worker with Durable Objects**. +- Observability: Datadog RUM on frontends (`apps/main/src/lib/rum.ts`), `dd-trace` on the API (initialized at the top of `apps/api/src/index.ts`). diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..6b0b97a --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,126 @@ +# Codebase Concerns Map + +> Generated 2026-05-14. Catalogs tech debt, security considerations, fragile zones, and known gotchas across the `xtablo-source` Turborepo. Pointers use repo-relative paths; line numbers are accurate as of this scan. + +## TODOs / FIXMEs / HACKs / XXX + +Grep of `apps/` + `packages/` (excluding `node_modules`, `dist`) — TODO markers are sparse, suggesting either healthy hygiene or undocumented debt. + +- `apps/api/src/index.ts:67` — `// TODO: Add health check endpoint`. No `/healthz` for Cloud Run, which complicates liveness/readiness probes. +- `apps/api/src/routers/invite.ts:26` — `// TODO: Verify that the owner_id is correct`. Authorization gap on invite creation. +- `apps/api/src/routers/invite.ts:128` — `// TODO: Verify that the event start and end correspond to a slot`. Booking integrity not enforced server-side. +- `apps/main/src/components/WebcalModal.tsx:125` — `{/* TODO: Add webcal URL */}` — feature placeholder shipping incomplete. +- `apps/main/src/lib/rum.ts:25,31` — Datadog RUM session sampling rates are commented `// TODO: Uncomment when we have enough data` — observability runs in a partial state. +- `apps/api/src/routers/user.ts:54` — `// Deprecated: name field is deprecated, use first_name and last_name instead` — dual fields still flowing through APIs. + +No `FIXME`, `HACK`, or `XXX` markers found in `apps/` or `packages/` source — but absence is not assurance; many concerns live under euphemisms (see `@deprecated` below). + +`@deprecated` markers: +- `apps/main/src/hooks/user.ts:16` — `useUser` hook deprecated in favor of `useSession` from `SessionContext`. Likely still has callers. + +## Known issues (from docs) + +The `docs/` directory contains 35+ retrospective `*_FIX.md`, `*_SETUP.md`, and migration notes. No single `TROUBLESHOOTING.md` exists; institutional knowledge is scattered. Notable items: + +- `docs/MIDDLEWARE_INITIALIZATION_FIX.md` — November 2025 incident: routers called `MiddlewareManager.getInstance()` at module-load time before `initialize()` ran. Fixed by passing the manager into router factories, but the singleton's eager-throw API (`getInstance()` throws if not initialized) remains a footgun for any new router that forgets the pattern. +- `docs/STRIPE_SECURITY_FIX.md` — Migration 37: `public.active_subscriptions` view exposed all users' subscription data without RLS. Replaced with `SECURITY DEFINER` function `get_my_active_subscription()`. Future views need similar audit. +- `docs/STRIPE_MIGRATION_36.md`, `STRIPE_WITH_SYNC_ENGINE.md`, `STRIPE_FINAL_SETUP.md`, `STRIPE_IMPLEMENTATION_SUMMARY.md`, `STRIPE_CLEANUP_*` — eleven Stripe documents indicate repeated rework on the billing surface. +- `docs/TEST_FIXES.md`, `docs/TEST_ROUTER_REFACTOR.md`, `docs/API_TESTS.md`, `docs/MIDDLEWARE_TESTS.md` — test setup has needed periodic refactoring; mocks are tightly coupled to the singleton. +- `SECURITY_NOTICE.md` (repo root) — `.env` files were previously committed to git history. The checklist of credentials to rotate (Supabase service role, Stripe, Stream, Google OAuth, R2) is in the file; verification that rotation occurred is not tracked here. + +## Security considerations + +### JWT handling +- Validation occurs in `apps/api/src/helpers/auth.ts` via `validateAuthHeader` + Supabase `auth.getUser(token)`. Wired into `authMiddleware` / `maybeAuthenticatedMiddleware` in `apps/api/src/middlewares/middleware.ts`. +- `apps/main/src` uses `jwt-decode` (^4.0.0) to inspect tokens client-side — purely decode, no verification (correct), but any code path that *trusts* decoded claims for authorization would be a bug. +- Client portal sessions use a separate JWT (`CLIENT_AUTH_JWT_SECRET`, `apps/api/src/helpers/clientSessions.ts`) with cookie storage (`CLIENT_AUTH_COOKIE_NAME`). Two independent token systems = two attack surfaces. +- Admin sessions live in `apps/api/src/helpers/adminTokens.ts` signed by `ADMIN_TOKEN_SIGNING_SECRET` — a third token surface. + +### Service role key usage (RLS bypass) +The Supabase service role key is created exactly once in `apps/api/src/middlewares/middleware.ts:164` and injected into every authenticated request via `supabaseMiddleware`. This means **every API handler operates with full RLS bypass**. Authorization logic therefore must live in handler code; any forgotten ownership check is a data-exposure bug. Notable areas relying on handler-side checks: +- `apps/api/src/routers/tablo.ts`, `tablo_data.ts` +- `apps/api/src/routers/admin*.ts` (six admin routers) +- `apps/api/src/routers/clientPortal.ts` +- The two `TODO: Verify ... owner_id`/`slot` comments in `invite.ts` are direct evidence of this risk. + +### Stripe webhook verification +- `apps/api/src/routers/stripe.ts:167-191` — verification is delegated to `@supabase/stripe-sync-engine`'s `processWebhook(rawBody, signature)`. The route does retrieve raw body via `c.req.text()` (correct — verification needs unmodified bytes). Note: webhook router is wired pre-auth in `routers/index.ts`, which is required by Stripe — verify any future restructuring preserves this ordering. +- Webhook secret comes from `STRIPE_WEBHOOK_SECRET` in config; rotation procedure not documented in repo. + +### Secrets / env vars +- Production/staging: loaded from Google Secret Manager via `apps/api/src/secrets.ts` and assembled in `apps/api/src/config.ts`. +- Dev: `.env` via `dotenv`. `.env*` is now gitignored (see `SECURITY_NOTICE.md`). +- `apps/api/src/config.ts:32` requires a long list of secrets — `validateEnvVar` throws on missing; good fail-fast, but means a single missing env aborts boot with no partial-feature degradation. +- Frontend env: `apps/main` reads `VITE_*` env at build time per environment (`build:staging`, `build:prod`). Anything `VITE_*` is bundled into the public JS — only public keys belong here. + +## Performance considerations + +- **React Query defaults**: `packages/shared/src/lib/api.ts:18` sets a global `staleTime` of 5 minutes. Aggressive caching is appropriate for dashboard data but can hide write-after-read bugs; mutations must explicitly invalidate hierarchical keys (`["tablos", id]`, etc.). +- **Custom stale times**: `apps/main/src/hooks/stripe.ts:114` (5 min) and `:221` (10 min) — billing data caching for 10 minutes risks displaying a stale subscription state after a webhook arrives. UI should also listen to mutation success or refresh on Stripe-portal-return paths. +- **Pagination**: ~63 hits for `.select("*")` / `.range(` / `.limit(` across `apps/main/src` + `apps/api/src` (excluding tests). Many list endpoints (`apps/api/src/routers/tablo.ts`, `admin*.ts`) appear to return full tables; no shared cursor/offset helper exists. AG-Grid in main app loads client-side which exacerbates this for orgs with large datasets. +- **Source-only packages**: `@xtablo/shared`, `@xtablo/ui`, `@xtablo/chat-ui`, `@xtablo/auth-ui`, `@xtablo/tablo-views`, `@xtablo/shared-types` export TS directly. Pros: instant HMR. Cons: every app re-typechecks and re-bundles them; tree-shaking depends on each app's bundler being able to drop unused exports (Vite generally handles this, but barrel files in `packages/shared/src/index.ts`-style modules can defeat it — worth auditing if bundle size matters). +- **Bundle size**: `apps/main` has `rollup-plugin-visualizer` available for analysis but no tracked size budgets. Heavy deps: `@blocknote/*` (rich text editor), `ag-grid-community` + `ag-grid-react`, `jspdf`, `@datadog/browser-rum*` — all in `dependencies` of `apps/main/package.json`. +- **Datadog dd-trace** (`apps/api`) is initialized in `apps/api/src/index.ts:13` before everything else; misconfigured tracing has measurable cold-start cost on Cloud Run. + +## Fragile areas + +### Stripe sync engine (Supabase ↔ Stripe) +- `apps/api/src/middlewares/stripeSync.ts` instantiates `@supabase/stripe-sync-engine@^0.45.0` with a direct Postgres connection string (`SUPABASE_CONNECTION_STRING`) and base64-encoded CA cert (`SUPABASE_CA_CERT`). Bypasses Supabase API entirely. +- `revalidateObjectsViaStripeApi: ["subscription", "customer"]` — explicit workaround for stale-data bugs in the sync engine. +- Schema is `"stripe"` (separate from `public`); migrations must be applied carefully; eleven separate Stripe docs in `docs/` evidence repeated breakage. +- Sync is *eventually consistent*: UI hooks with 5–10 min `staleTime` (above) can show pre-webhook state. + +### Auth flow (passwordless + temporary accounts) +- Passwordless flow creates accounts flagged `is_temporary: true`. Lifecycle (cleanup of abandoned temp accounts, upgrade path to permanent) is not documented in `docs/`. +- Three independent token systems (user JWT, client-portal JWT, admin token) live side by side — see Security section. Test reference: `apps/api/src/__tests__/README.md:28` documents a `test_temp@example.com` fixture. +- `SessionContext` (main app) listens to `supabase.auth.onAuthStateChange()`. Deprecated `useUser` hook at `apps/main/src/hooks/user.ts:16` still exists and likely has stragglers. + +### Middleware singleton initialization order +- `MiddlewareManager` in `apps/api/src/middlewares/middleware.ts` is a singleton with throw-on-misuse semantics. The November 2025 fix (see `docs/MIDDLEWARE_INITIALIZATION_FIX.md`) is a pattern convention, not a structural guarantee. Any new router that calls `MiddlewareManager.getInstance()` at module top-level reintroduces the bug. +- The `index.ts` router order (`apps/api/src/routers/index.ts`) is load-bearing: public routes → stripe webhook (no auth, raw body) → auth-applied routes. Refactors that reorder these break either auth or signature verification. + +## Build / deploy concerns + +- **Cloudflare Workers** (`apps/main`, possibly `apps/clients`, `apps/external`, `apps/admin`): `apps/main/wrangler.toml` sets `compatibility_date = "2025-07-09"` and no `compatibility_flags`. Notably **no `nodejs_compat`** — any dep pulling Node built-ins at runtime will fail at the edge. Watch for incidental Node imports in shared packages. +- **Type generation**: per `CLAUDE.md`, run `npx supabase gen types typescript > packages/shared-types/src/database.types.ts` after schema changes. There's no CI guard that types are up to date; drift between DB and types is silent. +- **Cache invalidation gotchas** (from `CLAUDE.md` "Important Notes"): stale builds resolved only by `pnpm clean && rm -rf node_modules/.cache && pnpm install && pnpm build`. Indicates Turborepo cache occasionally misses dependency changes — possibly because source-only packages don't declare outputs. +- **API deploy** uses Cloud Run with Cloud Build (`docs/CLOUD_BUILD_*.md`, `docs/DOCKER_*.md`). Multi-stage pnpm Docker build is documented and has needed multiple optimization passes (`DOCKER_BUILD_PERFORMANCE.md`, `DOCKER_PNPM_OPTIMIZATION.md`, `DOCKER_FIX_SUMMARY.md`). +- **API uses `tsc` only** (`apps/api/package.json` `build: tsc`) — no bundling, ships `dist/` + `node_modules`. Large image surface; dependency vulnerabilities are deploy-time concerns. +- **Pre-commit**: `.pre-commit-config.yaml` exists at root; behavior not audited here. + +## Dependencies of concern + +Pinned/notable in `apps/main/package.json`: +- `@typescript/native-preview: 7.0.0-dev.20251010.1` — a *preview/nightly* TypeScript native compiler pinned to a dated dev build. High churn risk; may break unexpectedly. +- `@types/react: 19.0.10`, `@types/react-dom: 19.0.4` — exact-pinned (no caret). `react: 19.0.0` itself also exact-pinned. Upgrades will require coordinated change. +- `vitest: ^3.2.4` (in `apps/main`) vs `vitest: ^4.0.8` (in `apps/api`) — different major versions across the monorepo; shared test utilities will be incompatible. +- `@types/react-router-dom: ^5.3.3` listed alongside `react-router-dom: ^7.9.4` — the v5 type stubs are wrong for v7; either unused or actively misleading. +- `eslint: ^9.22.0` + `@typescript-eslint/*: ^7.0.2` — typescript-eslint v7 predates flat-config-stable ESLint 9; potential plugin compat issues. Note also Biome is the primary linter (`biome.json`), so ESLint may be vestigial. +- `jest` + `jest-environment-jsdom` + `@types/jest` in `apps/main` devDeps despite using Vitest — dead dependencies inflating install. +- `pnpm.overrides`: `form-data: ^4.0.4`, `linkifyjs: ^4.3.2` — root-level overrides usually indicate working around a transitive vulnerability or bug; reason isn't documented in repo. + +In `apps/api/package.json`: +- `stripe: ^20.0.0` — Stripe SDK is currently on v17+ as of cutoff; v20 is a future major. Verify lockfile matches expected runtime version. +- `ts-node: ^10.9.2` listed in `dependencies` (not devDeps) — likely unused at runtime; should be a devDep. +- `multer: ^2.0.2` — major version 2.x is recent; ensure middleware patterns aren't using legacy 1.x APIs. + +`pnpm-lock.yaml` is ~763 KB indicating substantial dependency graph; `pnpm audit` not run as part of this scan. + +## Documentation gaps + +- **No `TROUBLESHOOTING.md`** despite 35+ retrospective fix docs — there's no central index. +- **`docs/`** is fix-log heavy and architecture-light. `STRIPE_ARCHITECTURE.md` and `MIDDLEWARE_TESTS.md` exist but most flows lack an "as-built" diagram. +- **Three token systems** (user JWT, client session JWT, admin token) — no unified auth doc; you must read `apps/api/src/helpers/{auth,clientSessions,adminTokens}.ts` separately. +- **Temporary accounts** lifecycle (creation, retention, cleanup, upgrade to permanent) — undocumented. +- **Six admin routers** (`adminActions`, `adminAuth`, `adminDatasets`, `adminOverview`, `adminTables`, `admin.ts`) — admin surface is large and the only docs are `ADMIN_APP_ACCESS_SETUP.md` for access setup, not authorization model. +- **Frontend bundle budgets** — none defined; `rollup-plugin-visualizer` available but not enforced. +- **Type-generation workflow** — only mentioned in `CLAUDE.md`; no CI check. +- **`apps/chat-worker`, `apps/clients`, `apps/admin`, `apps/external`** — minimal architectural docs; main app dominates `CLAUDE.md`. +- **Legacy directories** at repo root (`backend/` Python, `go-backend/` Go, `frontend_v2/`, `xtablo-expo/`) are present but unmentioned in `CLAUDE.md`. Status (active? abandoned?) is unclear — this itself is a debt signal. +- **`SECURITY_NOTICE.md`** lists credentials to rotate after the `.env`-in-git incident, but completion status of that rotation checklist is not tracked. + +## Tracked-but-unaddressed observations + +- 43 `console.error`/`console.warn` calls in `apps/api/src/routers/` — direct logging instead of going through `pino` (which is a devDep but not wired to the routers). +- 21 explicit `: any` annotations in `apps/api/src` (excluding tests). Two visible examples: `apps/api/src/routers/clientInvites.ts:192` (`(candidate: any) =>`) and `apps/api/src/helpers/helpers.ts:374` (`(u: any) =>`). +- Only 2 `@ts-ignore`/`@ts-expect-error` comments across `apps/` + `packages/` — TypeScript discipline appears solid where types exist. diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..7c06e8c --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,161 @@ +# Code Conventions + +**Last updated:** 2026-05-14 +**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source` +**Apps:** `apps/main`, `apps/external`, `apps/api`, `apps/admin`, `apps/chat-worker`, `apps/clients` +**Packages:** `packages/shared`, `packages/ui`, `packages/shared-types`, `packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views` + +## Linter & Formatter — Biome + +The repo uses a single root [Biome](https://biomejs.dev) config: `biome.json` (Biome 2.2.5, pinned in root `package.json` devDependencies). + +Formatting rules (from `biome.json`): +- `indentStyle: "space"`, `indentWidth: 2` +- `lineEnding: "lf"`, `lineWidth: 100` +- JS: `quoteStyle: "double"`, `jsxQuoteStyle: "double"`, `semicolons: "always"`, `trailingCommas: "es5"`, `arrowParentheses: "always"`, `bracketSpacing: true`, `bracketSameLine: false` +- JSON formatter enabled with same indent settings; parser allows comments but not trailing commas + +Key lint rules turned on (Biome `recommended: false` — rules are explicit): +- `suspicious.noExplicitAny: "error"` (overridden to `off` for some files via overrides; warn in `xtablo-expo`) +- `correctness.noUnusedVariables`, `noUnusedImports`, `noUndeclaredVariables` — all `error` +- `style.useConst`, `useTemplate`, `noNamespace`, `noCommonJs` — all `error` +- `complexity.noBannedTypes`, `suspicious.noDebugger`, `suspicious.noEmptyBlockStatements` — all `error` + +Per-package scripts wrap Biome: +- `pnpm lint` → `turbo lint` → each package runs `biome check .` +- `pnpm lint:fix` → `biome check --write .` +- `pnpm format` → `biome format --write .` + +## TypeScript Conventions + +Every package/app ships its own `tsconfig.json`; there is no root TS config. + +Strictness — every config sets `strict: true`. Additional flags consistent across configs: +- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true` +- `noUncheckedIndexedAccess: true` for `packages/shared-types`, `packages/shared` +- `isolatedModules: true`, `moduleDetection: "force"`, `skipLibCheck: true` +- API uses `verbatimModuleSyntax: true` (`apps/api/tsconfig.json`) — type-only imports must be explicit + +Module systems: +- Frontend apps use `module: ESNext` + `moduleResolution: "bundler"` (e.g. `apps/main/tsconfig.app.json`) +- API uses `module: NodeNext` (TypeScript compiled output via `tsc`) +- All packages declare `"type": "module"` in package.json + +Path aliases (defined per app, resolved by Vite via `vite-tsconfig-paths`): +- `apps/main`: `@ui/*` → `./src/*`, `@xtablo/auth-ui` → `../../packages/auth-ui/src`, `@xtablo/ui/*` → `../../packages/ui/src/*` (see `apps/main/tsconfig.app.json`) +- `apps/main/tsconfig.json`: also `@external/*` → `src/external/*` + +Package import discipline (per `CLAUDE.md`): +- No circular dependencies between packages +- `apps/api` may only import from `@xtablo/shared-types` +- Frontend apps may import from all shared packages +- `@xtablo/shared` and `@xtablo/ui` are **source-only** — TypeScript is consumed directly via `vite-tsconfig-paths`; no build step + +Types-first workflow: +- Database types are auto-generated into `packages/shared-types/src/database.types.ts` via `npx supabase gen types typescript` +- Domain types in `@xtablo/shared-types` are derived from `database.types.ts` (nulls removed, refined shapes) +- API response types live in the same package so frontends and the API agree + +## React Component Conventions + +- Functional components only (no class components observed) +- TSX files use named exports for components, e.g. `export function CustomModal(...)` (see `apps/main/src/components/CustomModal.tsx`) +- Co-located unit tests: `Foo.tsx` + `Foo.test.tsx` +- Naming suffix conventions (per `CLAUDE.md`): + - Dialogs / modals → `*Modal.tsx` (e.g. `CustomModal.tsx`) + - Page sections → `*Section.tsx` (e.g. `TabloEventsSection.tsx`) + - Card surfaces → `*Card.tsx` (e.g. `EventTypeCard.tsx`, `AvailabilityCard.tsx`, `EventTypeCard.test.tsx`) +- Shared primitives (Radix + Tailwind) live in `packages/ui/src` +- Cross-app business hooks/contexts live in `packages/shared/src` + +## Hook Patterns + +React Query (TanStack Query v5) is the primary server-state tool. Standard return shapes: + +```ts +// Queries +const { data, isLoading, error } = useMyQuery(); + +// Mutations +const { mutate, isPending } = useMyMutation(); +``` + +- Default cache time: 5 minutes (configured in `packages/shared`'s QueryClient) +- Mutations invalidate targeted keys explicitly rather than blowing away the whole cache + +Zustand handles global client state (notably the authenticated user store): +- `useUser()` — throws if no session (use inside protected routes) +- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public surfaces) + +## Query Key Conventions + +Hierarchical keys, with the resource name first and identifiers cascading deeper: + +```ts +["tablos"] // list +["tablos", tabloId] // single +["tablo-files", tabloId] // related collection +``` + +Invalidations should match the deepest key that needs to be refreshed. + +## Error Handling + +- User-facing: `toast.add({ ... })` (toast system in `packages/shared` / `packages/ui`) — messages should be friendly and actionable +- Technical: `console.error` for developer-only context (stack traces, raw API errors) +- API errors are caught at the React Query hook layer and surfaced via `error` from `useQuery`/`useMutation`; UI components branch on `isError` +- Server side (`apps/api`): Hono routers return `c.json({ error: ... }, statusCode)`; middleware handles auth failures with 401s (see `docs/MIDDLEWARE_TESTS.md`) + +## Loading States — three levels + +1. **Route level** — `ProtectedRoute` shows a full-page spinner while the session resolves +2. **Feature level** — React Query `isLoading` / `isPending` drives section-level skeletons or spinners +3. **Action level** — Buttons set `disabled` during the related mutation's `isPending` + +Empty / error states are explicit branches (no silent fallbacks). + +## Import / Export Patterns + +- ESM throughout (`"type": "module"` in every package.json; Biome enforces `noCommonJs`) +- Source-only packages (`@xtablo/shared`, `@xtablo/ui`) export from `src/index.ts` and are consumed via TS path aliases — no `dist/` involved +- Compiled packages (`@xtablo/shared-types` and `apps/api`) emit `dist/` via `tsc`; `shared-types` includes `declaration: true` and `declarationMap: true` +- Type-only imports preferred where supported (`verbatimModuleSyntax` is on for the API) +- Biome's `noUnusedImports` rule will flag dead imports at lint time + +## File Organization + +``` +apps//src/ + components/ # UI components, *.tsx + *.test.tsx + contexts/ # React contexts (e.g. UpgradeBlockContext.tsx) + providers/ # Store / provider wrappers (e.g. UserStoreProvider.tsx) + hooks/ # App-specific hooks + pages/ or routes/ # Route entry points + lib/ # Utilities, route table, api client setup + utils/testHelpers # Render-with-providers wrappers for tests +packages//src/ + index.ts # Single barrel export + ... # Domain folders mirror app layout +``` + +API layout (`apps/api/src`): +- `routers/` — Hono routers grouped by concern (`public.ts`, `authRouter.ts`, `tablo.ts`, `stripe.ts`, etc.) +- `middlewares/` — Auth, Supabase, R2, Stream, email injection +- `helpers/` — Pure logic (testable in isolation, e.g. `helpers/orgIcons.ts` + `orgIcons.test.ts`) +- `__tests__/` — Test-only fixtures, setup, globalSetup, route + middleware suites + +## Type Safety + +- `noExplicitAny` is on as `error` in the root config (relaxed to `warn` in `xtablo-expo` and `off` for select API/legacy file overrides) +- Prefer derived types from `@xtablo/shared-types` over inline shape literals +- `Database` table types are generated; domain types should narrow them rather than redeclare from scratch +- `noUncheckedIndexedAccess: true` in shared packages — index access returns `T | undefined`; handle the undefined branch explicitly +- API enforces `verbatimModuleSyntax`, so `import type { ... }` is required for type-only imports — relevant for compiled API code + +## Reference files + +- `biome.json` — single source for lint + format +- `apps/main/tsconfig.app.json` — canonical frontend TS config +- `apps/api/tsconfig.json` — canonical backend TS config +- `packages/shared-types/tsconfig.json` — type-only package config +- `CLAUDE.md` — high-level conventions (this doc expands it with verified specifics) diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..211e4a6 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,140 @@ +# INTEGRATIONS + +External systems wired into the xtablo monorepo. Last updated 2026-05-14. + +## Database — Supabase Postgres + +- **Hosted Postgres** managed by Supabase. The frontend talks to it via the JS SDK; the API talks to it via the service-role key and (for `stripe-sync-engine`) a direct `postgres://` connection string. +- **Service-role client** is constructed per-request inside the API in `apps/api/src/middlewares/middleware.ts` (`supabaseMiddleware`): + ```ts + createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY) + ``` + Mounted globally for every route in `apps/api/src/routers/index.ts`. +- **Browser client** factory at `packages/shared/src/lib/supabase.ts` re-exports `createClient`; instances are produced inside `@xtablo/shared` consumers and used directly (`supabase.from("table")...`). +- **RLS** — bypassed by the API using the service-role key when needed (see `CLAUDE.md` notes); browser clients use the anon key plus the user's JWT and rely on RLS policies. +- **Direct Postgres** — `SUPABASE_CONNECTION_STRING` (with base64-encoded `SUPABASE_CA_CERT`) is used by the Stripe sync engine `poolConfig` in `apps/api/src/middlewares/stripeSync.ts`. +- **Schema, migrations, snippets, tests** — `supabase/config.toml`, `supabase/migrations/`, `supabase/snippets/`, `supabase/tests/`. +- **Type codegen** — regenerate `packages/shared-types/src/database.types.ts` with: + ```bash + npx supabase gen types typescript > packages/shared-types/src/database.types.ts + ``` + These types are then refined into domain/API types inside `@xtablo/shared-types` (`tablos.types.ts`, `tablo-data.types.ts`, `events.types.ts`, `stripe.types.ts`, `kanban.types.ts`). + +## Authentication + +### Supabase Auth (primary users) +- JWT-based, with sessions managed by `SessionContext` (`packages/shared/src/contexts/SessionContext.tsx`) listening to `supabase.auth.onAuthStateChange()`. +- API extracts and validates the bearer token via `authenticateFromHeader` in `apps/api/src/helpers/auth.ts`, wired in `authMiddleware` (`apps/api/src/middlewares/middleware.ts`). +- `maybeAuthenticatedMiddleware` (same file) sets `c.var.user = null` when the header is missing — used by booking and other "maybe authed" endpoints in `apps/api/src/routers/maybeAuthRouter.ts`. +- Passwordless temporary accounts (`is_temporary: true`) created via the public booking flow. +- Front-end auth UI lives in `@xtablo/auth-ui`. + +### Admin auth (internal) +- Custom JWTs signed with `ADMIN_TOKEN_SIGNING_SECRET` (Google Secret Manager). +- Helpers in `apps/api/src/helpers/adminTokens.ts`, verified by `adminAuthMiddleware` (Bearer scheme) in `apps/api/src/middlewares/middleware.ts`. +- Routes: `apps/api/src/routers/admin.ts`, `adminActions.ts`, `adminAuth.ts`, `adminDatasets.ts`, `adminOverview.ts`, `adminTables.ts`. + +### Client portal auth (read-only client users) +- Magic-link → cookie-based session JWT, stored in cookie `xtablo_client_session` on domain `clients.xtablo.com`. +- Issuance: `apps/api/src/helpers/clientMagicLinks.ts`, `apps/api/src/helpers/clientSessions.ts`. +- Middlewares `clientAuthMiddleware` / `maybeClientAuthMiddleware` (`apps/api/src/middlewares/middleware.ts`) verify cookies using `CLIENT_AUTH_JWT_SECRET`. +- Routers: `apps/api/src/routers/clientAuth.ts`, `clientInvites.ts`, `clientPortal.ts`. + +### Task auth (cron / job runners) +- HTTP Basic with `TASKS_SECRET`, enforced by `basicAuthMiddleware` for `apps/api/src/routers/tasks.ts`. + +### Chat worker auth +- Independent JWT verification using `jose` against `JWT_SECRET` Wrangler secret. WebSocket connections receive the token via `?token=` query string; REST via Authorization header (`apps/chat-worker/src/index.ts`, `apps/chat-worker/src/lib/auth.ts`). + +## Payments — Stripe + +- **Server SDK** — `stripe ^20.0.0` instantiated per-request inside `stripeMiddleware` with `apiVersion: "2025-11-17.clover"` (`apps/api/src/middlewares/middleware.ts`). +- **Browser SDK** — `@stripe/stripe-js` in `apps/main`. +- **Webhook router** — `apps/api/src/routers/stripe.ts`, mounted at `/api/v1/stripe-webhook` in `apps/api/src/routers/index.ts`. Plan tiers (`solo`, `team`, `founder`) keyed off `STRIPE_*_PRICE_ID` env vars. +- **Sync engine** — `@supabase/stripe-sync-engine` writes Stripe objects directly into the `stripe` schema of Supabase via a direct Postgres pool (`apps/api/src/middlewares/stripeSync.ts`). Refetches `subscription` and `customer` objects to avoid stale state. +- **Secrets** — `stripe-secret-key`, `stripe-webhook-secret` (prod) and `…-staging` variants pulled from Google Secret Manager (`apps/api/src/secrets.ts`). +- **Billing helpers** — `apps/api/src/helpers/billing.ts`. Active-plan enforcement via `activePlanAccessMiddleware`; only the org's billing owner can manage billing. +- **Docs** — `docs/STRIPE_ARCHITECTURE.md`, `docs/STRIPE_README.md`, `docs/STRIPE_WITH_SYNC_ENGINE.md`, `docs/STRIPE_INTEGRATION_COMPLETE.md` plus several setup/testing guides. + +## Mobile Payments — RevenueCat + +- Webhook router `apps/api/src/routers/revenuecat.ts`, mounted at `/api/v1/revenuecat-webhook`. +- Maps Apple in-app product IDs to plans via `apps/api/src/helpers/appleBilling.ts`. +- Env: `REVENUECAT_WEBHOOK_AUTH_HEADER`, `REVENUECAT_SOLO_PRODUCT_ID`, `REVENUECAT_ANNUAL_PRODUCT_ID`. + +## Storage — Cloudflare R2 (S3 SDK) + +- `@aws-sdk/client-s3` pointed at `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com` with `region: "auto"`. +- Instantiated per-request in `r2Middleware` (`apps/api/src/middlewares/middleware.ts`) and exposed as `c.get("s3_client")`. +- Credentials: `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` from Secret Manager. +- Upload pipelines use `multer` + `sharp` for image processing (declared in `apps/api/package.json`). + +## Email — Gmail OAuth2 (Nodemailer) + +- SMTP through `smtp.gmail.com:465` using Google OAuth2 tokens. +- Built in `apps/api/src/middlewares/transporter.ts` via `nodemailer.createTransport` with `EMAIL_USER`, `EMAIL_CLIENT_ID`, `EMAIL_CLIENT_SECRET`, `EMAIL_REFRESH_TOKEN`. +- Exposed as `c.get("transporter")` everywhere `transporterMiddleware` is applied (mounted globally in `apps/api/src/routers/index.ts`). + +## Chat — Custom Cloudflare Durable Objects + +The CLAUDE.md mentions Stream Chat historically, but the current implementation is a **custom WebSocket chat** running on Cloudflare Workers + Durable Objects (no `stream-chat` dependency found anywhere in the repo). + +- Worker entry: `apps/chat-worker/src/index.ts` (Hono). +- Durable Object class: `apps/chat-worker/src/durable-objects/ChatRoom.ts`. Declared in `apps/chat-worker/wrangler.toml`: + ```toml + [durable_objects] + bindings = [{ name = "CHAT_ROOM", class_name = "ChatRoom" }] + [[migrations]] + tag = "v1" + new_sqlite_classes = ["ChatRoom"] + ``` +- Membership enforced by querying Supabase via PostgREST (`apps/chat-worker/src/lib/supabase.ts`). +- UI consumed via `@xtablo/chat-ui` (used by `apps/main`, `apps/clients`, `@xtablo/tablo-views`). +- Custom domain: `chat.xtablo.com`. + +## Observability + +### Frontend — Datadog RUM +- `apps/main/src/lib/rum.ts` initialises `@datadog/browser-rum` with `applicationId: "8e268e1a-1be0-44c6-b12a-978530d497c7"`, `service: "xtablo-ui"`, `sessionSampleRate: 100`, `sessionReplaySampleRate: 80`, `defaultPrivacyLevel: "mask-user-input"`, and the React plugin with router instrumentation. +- `apps/clients/src/lib/rum.ts` does the same for the clients app. +- User is set in `apps/main/src/providers/UserStoreProvider.tsx` (`datadogRum.setUser` on login, `clearUser` on logout). +- Manual view names via `apps/main/src/hooks/useDatadogRumViewName.tsx`. + +### Backend — dd-trace +- `tracer.init({ logInjection: true })` is the very first call in `apps/api/src/index.ts`. +- Datadog CI tooling (`@datadog/datadog-ci`, `@datadog/datadog-ci-plugin-cloud-run`) is wired for Cloud Run instrumentation uploads. +- Static analysis is configured at the repo root in `static-analysis.datadog.yml`. + +## Secrets Management — Google Secret Manager + +- `apps/api/src/secrets.ts` fetches every sensitive value from `projects/xtablo/secrets/{name}/versions/latest` using `@google-cloud/secret-manager`. +- Secrets loaded: `supabase-service-role-key`, `supabase-connection-string`, `supabase-ca-cert`, `admin-token-signing-secret`, `client-auth-jwt-secret`, `email-client-secret`, `email-refresh-token`, `r2-access-key-id`, `r2-secret-access-key`, `stripe-secret-key`, `stripe-webhook-secret`, plus their `…-staging` variants. +- Setup guide: `docs/GOOGLE_SECRET_MANAGER_SETUP.md`. + +## Deployment Platforms + +### Cloudflare Workers (web apps + chat) +- All Vite-built frontends are deployed to Workers via Wrangler (see `apps/*/wrangler.toml`). +- `apps/main/wrangler.toml` binds `app.xtablo.com` (prod) and `app-staging.xtablo.com` (staging), uses `not_found_handling = "single-page-application"`. +- `apps/external/wrangler.toml`, `apps/admin/wrangler.toml`, `apps/clients/wrangler.toml` each have their own custom domain. +- `apps/chat-worker/wrangler.toml` declares the `ChatRoom` Durable Object SQLite class and binds `chat.xtablo.com`. +- Worker scripts include observability blocks (`[observability] enabled = true`). + +### Google Cloud Run (API) +- Containerised by `apps/api/Dockerfile` (multi-stage Node 20 Alpine, pnpm-driven). +- CI/CD via Google Cloud Build using `apps/api/cloudbuild.yaml`. +- Runtime configuration uses Cloud Run env vars + Secret Manager (no env files inside the image). +- Deployment guides: `docs/CLOUD_BUILD_SETUP.md`, `docs/CLOUD_BUILD_ENV_CONFIG.md`, `docs/DOCKER_BUILD.md`, `docs/DOCKER_PNPM_OPTIMIZATION.md`, `docs/DOCKER_BUILD_PERFORMANCE.md`. + +### Go backend (separate) +- `go-backend/` deploys via its own `compose.yaml` / `justfile` and is independent of the TypeScript build pipeline (uses `chi`, `templ`, `sqlc`, `pgx`). + +### Infra utilities +- `infra/docker-compose.yaml`, `infra/docker-compose.traefik.yaml`, `infra/Dockerfile` — generic infra/traefik setup. `infra/app/main.py` is a small Python helper. + +## Other Integrations / Tooling + +- **Google APIs** (`googleapis ^161.0.0`) — currently used to mint OAuth2 access tokens for the Gmail SMTP transport (`apps/api/src/middlewares/transporter.ts`). +- **PWA** — `vite-plugin-pwa` in `apps/main` generates the service worker; `workbox-window` registers it. +- **Sourcemaps to Datadog** — `@datadog/datadog-ci` is available at the workspace root for `datadog-ci sourcemaps upload`. +- **Chromatic** — `chromatic ^11.5.0` listed in `apps/main` dev deps for visual regression (no CI workflow inspected here). diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..61b523d --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,142 @@ +# STACK + +Technology stack reference for the xtablo monorepo. Last updated 2026-05-14. + +## Languages & Runtimes + +- **TypeScript** — pinned at `^5.7.0` across most workspaces (api uses `^5.8.3`). Root devDependency in `package.json`. +- **Node.js** — `>=20.0.0` enforced via the `engines` field in the root `package.json`. The API Dockerfile builds on `node:20-alpine` (`apps/api/Dockerfile`). +- **Package manager** — pnpm `10.19.0` (declared as `packageManager` in the root `package.json`). Cloudflare Workers builds and the API container use `corepack` to install pnpm. +- **Go** — a parallel `go-backend/` service exists alongside the TS monorepo (`go-backend/go.mod`, `go 1.26.0`) using `chi`, `templ`, `pgx/v5`, and `sqlc`. It is included in the pnpm workspace but is otherwise its own toolchain. +- **Python** — a small infra utility at `infra/app/main.py` with `infra/requirements.txt` (not part of the runtime stack of the apps). + +## Frameworks + +### Frontend (React) +- **React 19.0.0** + **React DOM 19.0.0** — used by `apps/main`, `apps/external`, `apps/admin`, `apps/clients`, and all React packages. +- **React Router** — `react-router-dom ^7.9.4`. +- **Vite 6.2** — bundler/dev server for every web app (`apps/main/vite.config.ts`, `apps/external/vite.config.ts`, `apps/admin/vite.config.ts`, `apps/clients/vite.config.ts`). +- **TailwindCSS 4.1** — utility CSS via `@tailwindcss/vite`, with `tw-animate-css` and `tailwind-merge`. +- **Radix UI** + **React Aria** — primitives composed in `@xtablo/ui` (see `packages/ui/package.json`). +- **BlockNote** — rich-text editor (`@blocknote/core`, `@blocknote/mantine`, `@blocknote/react`) used in `apps/main`. +- **AG Grid Community** — data grid in `apps/main` (`ag-grid-community`, `ag-grid-react`). +- **React Hook Form** + **Zod** — forms and validation, wired through `@hookform/resolvers`. +- **i18next** + `react-i18next` — translations (`apps/main/src/i18n.ts`, plus `external`, `clients`, `tablo-views`). +- **react-day-picker** — calendar UI. +- **PWA** — `vite-plugin-pwa` + `workbox-window` in `apps/main`. + +### Backend (Hono) +- **Hono ^4.7.7** — HTTP framework for both `apps/api` and `apps/chat-worker`. +- **@hono/node-server** — Node adapter that drives `apps/api` (`apps/api/src/index.ts`). +- **hono-sessions** — session helpers in the API. +- **Cloudflare Workers + Durable Objects** — `apps/chat-worker` uses Hono with a `ChatRoom` SQLite-backed DO class (`apps/chat-worker/wrangler.toml`, `apps/chat-worker/src/durable-objects/ChatRoom.ts`). + +## Core Dependencies + +### Server State / Data Fetching +- `@tanstack/react-query ^5.69.0` — primary server-state cache. Hierarchical query keys; 5-minute default cache. Used in `apps/main`, `apps/clients`, `apps/admin`, `apps/external`, and `@xtablo/tablo-views`. +- `axios ^1.12.2` — HTTP client wrapper at `packages/shared/src/lib/api.ts`. + +### Client State +- `zustand ^5.0.5` — global stores (notably user). Lives in `@xtablo/shared` and is consumed via `useUser` / `useMaybeUser`. + +### Auth & JWT +- `@supabase/supabase-js ^2.49.x` — front-end and API client. +- `jwt-decode ^4.0.0` — decode access tokens on the client. +- `jose ^6.0.0` — JWT verification in the chat worker (`apps/chat-worker/src/lib/auth.ts`). + +### UI Primitives +- Radix: `react-avatar`, `react-checkbox`, `react-collapsible`, `react-dialog`, `react-dropdown-menu`, `react-label`, `react-popover`, `react-select`, `react-separator`, `react-slider`, `react-slot`, `react-switch`, `react-tabs`, `react-tooltip`, `react-radio-group` (`packages/ui/package.json`). +- `react-aria` / `react-aria-components ^1.7.0`, `@react-stately/*`, `@react-aria/*`. +- `lucide-react ^0.460.0` — iconography. +- `class-variance-authority`, `clsx`, `tailwind-merge` — class composition. +- `sonner ^2.0.7` — toast notifications (re-exported via `packages/shared/src/lib/toast.ts`). + +### Dates, IDs, Utilities +- `date-fns ^4.1.0`, `luxon ^3.7.2` (API only), `@internationalized/date`. +- `uuid ^11.1.0`, `pluralize ^8.0.0`, `ts-pattern ^5.6.2`. +- `jspdf ^3.0.3` — PDF export (main + shared). + +### Payments / Billing +- `stripe ^20.0.0` — server SDK (`apps/api`). +- `@stripe/stripe-js ^8.2.0` — browser SDK (`apps/main`). +- `@supabase/stripe-sync-engine ^0.45.0` — Stripe ↔ Supabase sync (`apps/api/src/middlewares/stripeSync.ts`). + +### Storage / Email +- `@aws-sdk/client-s3 ^3.850.0` — used against Cloudflare R2 in `apps/api/src/middlewares/middleware.ts` (`r2Middleware`). +- `multer ^2.0.2`, `sharp ^0.34.5` — file upload handling and image processing. +- `nodemailer ^7.0.4` + `googleapis ^161.0.0` — Gmail OAuth2 SMTP (`apps/api/src/middlewares/transporter.ts`). + +### Observability +- `@datadog/browser-rum ^6.13.0` + `@datadog/browser-rum-react ^6.13.0` — initialised in `apps/main/src/lib/rum.ts` and `apps/clients/src/lib/rum.ts`. +- `dd-trace ^5.74.0` — APM tracer started at the top of `apps/api/src/index.ts`. +- `@datadog/datadog-ci`, `@datadog/datadog-ci-base`, `@datadog/datadog-ci-plugin-cloud-run` — CI source-map upload and Cloud Run integration. +- `static-analysis.datadog.yml` — repo-level Datadog static analysis config. + +## Build / Dev Tooling + +- **Turborepo `^2.5.8`** — pipeline orchestration (`turbo.json`). Tasks: `build`, `dev`, `deploy(:staging|:prod)`, `build:staging`, `build:prod`, `lint`, `lint:fix`, `typecheck`, `test`, `test:watch`, `format`, `clean`. Caches `dist/**` and `tsconfig.tsbuildinfo`. +- **Biome `2.2.5`** — formatter + linter, config at `biome.json` with explicit per-package `files.includes`. +- **Vite `^6.2.2`** with `@vitejs/plugin-react ^4.3.4`, `vite-tsconfig-paths`, `@tailwindcss/vite`, `@cloudflare/vite-plugin`, `rollup-plugin-visualizer`, `vite-plugin-pwa`. +- **Vitest** — `^3.2.4` in frontend apps, `^4.0.8` in `apps/api`. Browser env via `happy-dom` (main, admin) or `jsdom` (clients). +- **Testing Library** — `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`. +- **tsc** — every package has its own `tsconfig.json` and runs `tsc -b` or `tsc --noEmit` for typecheck. +- **Wrangler `^4.24.3`** — Cloudflare Workers CLI used by `main`, `external`, `admin`, `clients`, `chat-worker`. +- **tsx `^4.7.1`** — dev runner for the API (`pnpm dev` invokes `tsx watch src/index.ts`). + +## Configuration + +### Environment loading +- API: `dotenv.config({ path: `.env.${NODE_ENV}` })` in `apps/api/src/config.ts`. `createConfig(secrets)` synthesizes a typed `AppConfig` from env + Google Secret Manager values. +- Frontend: Vite `import.meta.env.*`; modes `dev`, `staging`, `production` selected via `vite build --mode`. + +### Secret loading +- `apps/api/src/secrets.ts` pulls all sensitive values from `projects/xtablo/secrets/*/versions/latest` using `@google-cloud/secret-manager`. +- Test mode bypasses Secret Manager and uses raw env vars. + +### Build targets per app +| App | Bundler | Output | Deploy target | +| --- | --- | --- | --- | +| `apps/main` | Vite + `@cloudflare/vite-plugin` | `dist/` + worker | Cloudflare Workers (`apps/main/wrangler.toml`, routes `app.xtablo.com`, `app-staging.xtablo.com`) | +| `apps/external` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/external/wrangler.toml`) | +| `apps/admin` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/admin/wrangler.toml`) | +| `apps/clients` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/clients/wrangler.toml`) | +| `apps/chat-worker` | Wrangler-native | Worker bundle | Cloudflare Workers + Durable Objects, `chat.xtablo.com` | +| `apps/api` | `tsc` → `dist/` | Node 20 container | Google Cloud Run (`apps/api/Dockerfile`, `apps/api/cloudbuild.yaml`) | +| `go-backend` | `go build` | Binary | Separate (see `go-backend/justfile`) | + +### Static configs +- `biome.json` — single source of truth for formatting/linting scope. +- `turbo.json` — task graph; `globalDependencies` include `**/.env.*local`. +- `tsconfig.json` per workspace; project references across packages. + +## Workspace Structure + +`pnpm-workspace.yaml` defines: +```yaml +packages: + - 'apps/*' + - 'go-backend' + - 'packages/*' +``` + +### Apps (`apps/`) +- `@xtablo/main` — authenticated dashboard (port 5173). +- `@xtablo/external` — embeddable public booking widget (port 5174). +- `@xtablo/clients` — read-only client portal (port 5175, `clients.xtablo.com`). +- `@xtablo/admin` — internal admin app (port 5176). +- `@xtablo/api` — Hono REST API (port 8080). +- `@xtablo/chat-worker` — Cloudflare Worker hosting Durable-Object chat (`chat.xtablo.com`). + +### Packages (`packages/`) +- `@xtablo/shared` — React contexts, hooks, supabase wrapper, axios client, toast helper. **Source-only** (no build step; consumers import TS directly — see `packages/shared/package.json` `"main": "./src/index.ts"`). +- `@xtablo/ui` — Radix + Tailwind + react-aria component library. **Source-only**. +- `@xtablo/shared-types` — pure TS types including Supabase-generated `database.types.ts`. **Source-only**, zero runtime deps. +- `@xtablo/auth-ui` — shared auth screens. **Source-only**. +- `@xtablo/chat-ui` — chat UI components consumed by main, clients, tablo-views. **Source-only**. +- `@xtablo/tablo-views` — tablo view components shared between main and clients. **Source-only**. + +All shared packages export `./src/*` directly, so Vite HMR / tsc project references pick up changes instantly without a build step. + +### pnpm overrides +Root `package.json` pins `form-data ^4.0.4` and `linkifyjs ^4.3.2`, plus a `packageExtensions` entry adding `zod` as a peer of `@hookform/resolvers`. diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..938553d --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,296 @@ +# Structure + +_Last updated: 2026-05-14_ + +Directory layout, conventions, and where to look for things in the `xtablo-source` monorepo. + +## Top-Level Layout + +``` +xtablo-source/ +├── apps/ # Deployable applications +│ ├── main/ # Authenticated dashboard SPA (Vite + CF Workers) +│ ├── external/ # Public booking widget (Vite) +│ ├── clients/ # Client portal (Vite + CF Workers) +│ ├── admin/ # Internal admin tools (Vite + CF Workers) +│ ├── api/ # Hono REST API (Node, GCP Cloud Run) +│ └── chat-worker/ # Cloudflare Durable Object worker for chat +├── packages/ # Shared workspace packages (source-only) +│ ├── shared/ # Contexts, hooks, API client, toast, supabase client +│ ├── ui/ # Radix + Tailwind component library +│ ├── shared-types/ # Pure TypeScript types (zero runtime deps) +│ ├── auth-ui/ # Auth screens (login/register shells) +│ ├── chat-ui/ # Chat-specific UI components +│ └── tablo-views/ # Tablo detail / sections components +├── backend/ # Legacy / auxiliary backend code +├── go-backend/ # Go services (separate stack) +├── frontend_v2/ # Frontend v2 prototype +├── xtablo-expo/ # React Native (Expo) app +├── supabase/ # Supabase project (migrations, config) +├── infra/ # Infrastructure-as-code +├── docs/ # Architecture, deployment, integration docs +├── prompts/ # Prompt templates +├── scripts/ # Repo scripts +├── CLAUDE.md # Claude Code guidance (root) +├── DEVELOPMENT.md # Dev guide +├── turbo.json # Turborepo pipeline config +├── pnpm-workspace.yaml +└── package.json +``` + +## Apps + +### `apps/main` (`@xtablo/main`) + +Primary dashboard. Internal layout: + +``` +apps/main/src/ +├── main.tsx # Vite entry +├── App.tsx # Root component, providers +├── i18n.ts # i18next setup +├── main.css # Tailwind entry +├── components/ # UI components (Modals, Sections, Cards, ...) +├── contexts/ # App-local React contexts (UpgradeBlockContext, ...) +├── hooks/ # Feature hooks (tablos, events, tasks, stripe, ...) +├── lib/ +│ ├── api.ts # API client wrapper +│ ├── supabase.ts # Supabase client re-export +│ ├── routes.tsx # Protected routes +│ ├── publicRoutes.tsx # Public/auth routes +│ ├── billing.ts # Stripe-related helpers +│ ├── env.ts # Env validation +│ └── rum.ts # Datadog RUM init +├── pages/ # Route page components +├── providers/ +│ └── UserStoreProvider.tsx # Zustand user store +├── locales/ # i18n JSON +├── utils/ +└── assets/ +``` + +### `apps/external` (`@xtablo/external`) + +Embeddable booking widget. Internal layout: + +``` +apps/external/src/ +├── main.tsx +├── routes.tsx +├── EmbeddedBookingPage.tsx +├── FloatingBookingWidget.tsx +├── CustomModal.tsx +├── UserStoreProvider.tsx +├── lib/ +└── locales/ +``` + +Mode is driven by query string: `?mode=embed&eventTypeId=...` or `?mode=floating`. + +### `apps/clients` (`@xtablo/clients`) + +Public-facing client portal. + +``` +apps/clients/src/ +├── main.tsx +├── App.tsx +├── routes.tsx +├── components/ +├── hooks/ +├── lib/ +├── pages/ +├── locales/ +└── test/ +``` + +### `apps/admin` (`@xtablo/admin`) + +Internal admin app. + +``` +apps/admin/src/ +├── main.tsx +├── App.tsx +├── routes.tsx +├── components/ +├── hooks/ +├── lib/ +├── pages/ +└── registry/ +``` + +### `apps/api` (`@xtablo/api`) + +Hono REST API. Internal layout: + +``` +apps/api/src/ +├── index.ts # Entry: tracer, secrets, server start +├── config.ts # createConfig(secrets) -> AppConfig +├── secrets.ts # loadSecrets() (env or GCP Secret Manager) +├── client.ts # Supabase admin client factory +├── middlewares/ +│ ├── middleware.ts # MiddlewareManager singleton + supabase/r2/stripe +│ ├── stripeSync.ts # Stripe<->Supabase sync engine +│ └── transporter.ts # Email transport middleware +├── routers/ +│ ├── index.ts # getMainRouter — composition + ordering +│ ├── public.ts # Unauthenticated endpoints +│ ├── authRouter.ts # Requires JWT +│ ├── maybeAuthRouter.ts # Optional auth (booking-aware) +│ ├── tablo.ts # Tablo CRUD +│ ├── tablo_data.ts # Tablo content blocks +│ ├── tasks.ts # Tasks +│ ├── notes.ts # Notes +│ ├── events.ts (in user.ts/tablo_data.ts) +│ ├── stripe.ts # Stripe webhook + ops +│ ├── revenuecat.ts # RevenueCat webhook +│ ├── invite.ts # Tablo invites +│ ├── user.ts # User profile / settings +│ ├── admin*.ts # admin.ts, adminActions.ts, adminAuth.ts, +│ │ # adminDatasets.ts, adminOverview.ts, adminTables.ts +│ ├── clientAuth.ts # Client portal magic-link auth +│ ├── clientPortal.ts # Client portal endpoints +│ └── clientInvites.ts # Public client invites +├── helpers/ +├── types/ # BaseEnv, app.types.ts +└── __tests__/ +``` + +### `apps/chat-worker` + +``` +apps/chat-worker/src/ +├── index.ts # Worker entry +├── durable-objects/ # Durable Object classes +└── lib/ +``` + +## Packages + +### `packages/shared` (`@xtablo/shared`) + +Public surface via `packages/shared/src/index.ts`. + +``` +packages/shared/src/ +├── index.ts # Barrel +├── contexts/ +│ ├── SessionContext.tsx # Supabase session listener +│ └── ThemeContext.tsx +├── hooks/ +│ ├── auth.ts # useSignIn / useSignOut / useSession ... +│ ├── book.ts # Booking hooks +│ ├── public.ts # Public data hooks +│ └── useClickOutside.ts +├── lib/ +│ ├── api.ts # HTTP client w/ Bearer token +│ ├── supabase.ts # Supabase JS client +│ ├── toast.ts # toast.add() wrapper (sonner) +│ └── cn.ts # clsx + tailwind-merge +├── types/ # Re-exported domain types +└── utils/ + └── helpers.ts +``` + +### `packages/ui` (`@xtablo/ui`) + +Radix UI + Tailwind component library. Source-only. + +``` +packages/ui/src/ +├── components/ # button.tsx, dialog.tsx, select.tsx, +│ # popover.tsx, tabs.tsx, dropdown-menu.tsx, ... +├── hooks/ +└── styles/ +``` + +### `packages/shared-types` (`@xtablo/shared-types`) + +Zero-dependency TypeScript types — safe to import from API and frontend. + +``` +packages/shared-types/src/ +├── index.ts +├── database.types.ts # Auto-generated by supabase gen types +├── tablos.types.ts +├── tablo-data.types.ts +├── events.types.ts +├── kanban.types.ts +├── stripe.types.ts +├── admin.types.ts +└── utils.ts +``` + +### `packages/auth-ui` (`@xtablo/auth-ui`) + +Auth screens shared between apps: `AuthCardShell.tsx`, `AuthEmailPasswordForm.tsx`, `AuthInfoBanner.tsx`. + +### `packages/chat-ui` (`@xtablo/chat-ui`) + +Chat-specific UI (`components/`, `hooks.ts`, `security.ts`, `types.ts`, `chat-ui.css`). + +### `packages/tablo-views` (`@xtablo/tablo-views`) + +Tablo detail sections and shell: `TabloDetailsShell.tsx`, `TabloEventsSection.tsx`, `TabloFilesSection.tsx`, `TabloTasksSection.tsx`, `TabloDiscussionSection.tsx`, `TabloHeaderActions.tsx`, `EtapesSection.tsx`, `RoadmapSection.tsx`, plus `single-tablo/`, `components/`, `hooks/`, `styles/`. + +## Naming Conventions + +- **Modals**: `*Modal.tsx` — e.g. `apps/main/src/components/CreateTabloModal.tsx`, `EventDetailsModal.tsx`. +- **Sections**: `*Section.tsx` — e.g. `packages/tablo-views/src/TabloFilesSection.tsx`. +- **Cards**: `*Card.tsx` — e.g. `apps/main/src/components/EventTypeCard.tsx`, `AvailabilityCard.tsx`. +- **Pages**: live in `apps//src/pages/`, lowercased file names matching the route (`planning.tsx`, `events.tsx`, `login.tsx`). +- **Tests**: co-located as `*.test.tsx` / `*.test.ts` next to the source (`AvailabilityCard.test.tsx`, `auth.signup.test.ts`). +- **Hooks**: lowercase domain file in `hooks/` (`tablos.ts`, `events.ts`, `tasks.ts`); each exports multiple named hooks. +- **UI primitives**: lowercase in `packages/ui/src/components/` (`button.tsx`, `select.tsx`). + +## Key File Locations (Cheatsheet) + +| Concern | Path | +|-------------------------------|---------------------------------------------------------| +| Main app routes (protected) | `apps/main/src/lib/routes.tsx` | +| Main app public routes | `apps/main/src/lib/publicRoutes.tsx` | +| External app routes | `apps/external/src/routes.tsx` | +| Clients app routes | `apps/clients/src/routes.tsx` | +| Admin app routes | `apps/admin/src/routes.tsx` | +| Session context | `packages/shared/src/contexts/SessionContext.tsx` | +| User store (Zustand) | `apps/main/src/providers/UserStoreProvider.tsx` | +| HTTP API client | `packages/shared/src/lib/api.ts` | +| Supabase client (browser) | `packages/shared/src/lib/supabase.ts` | +| API entry | `apps/api/src/index.ts` | +| API router composition | `apps/api/src/routers/index.ts` | +| API middleware singleton | `apps/api/src/middlewares/middleware.ts` | +| API config | `apps/api/src/config.ts` | +| API secrets loader | `apps/api/src/secrets.ts` | +| Auto-generated DB types | `packages/shared-types/src/database.types.ts` | +| Shared barrel export | `packages/shared/src/index.ts` | +| UI component barrel | `packages/ui/src/components/index.ts` | +| Turborepo pipeline | `turbo.json` | +| Workspace definition | `pnpm-workspace.yaml` | +| Root Claude guidance | `CLAUDE.md` | + +## Adding New Features (5-Step Flow) + +From `CLAUDE.md`, the canonical workflow for a new feature is: + +1. **Define types** in `packages/shared-types/src/` (or run `npx supabase gen types typescript > packages/shared-types/src/database.types.ts` if you changed the DB schema). +2. **Add API endpoint** in `apps/api/src/routers/` — pick the right router (`public.ts`, `maybeAuthRouter.ts`, `authRouter.ts`, or a domain router like `tasks.ts`). Mount it in `apps/api/src/routers/index.ts` if it's new. +3. **Create a React Query hook** in either `packages/shared/src/hooks/` (cross-app) or `apps/main/src/hooks/` (app-local). Use the hierarchical query-key convention so mutations can invalidate predictably. +4. **Build the UI** using primitives from `@xtablo/ui` and patterns from `@xtablo/tablo-views` / `@xtablo/chat-ui` where applicable. Follow the `*Modal.tsx` / `*Section.tsx` / `*Card.tsx` naming convention and co-locate a `*.test.tsx`. +5. **Wire the route** in `apps/main/src/lib/routes.tsx` (or the equivalent in the relevant app) so the new page is reachable. + +## Testing Locations + +- **API tests**: `apps/api/src/__tests__/` (Vitest, mock Supabase). +- **Frontend tests**: co-located `*.test.tsx` next to source, run via `pnpm test` per app (`apps/main/src/setupTests.ts` configures happy-dom + Testing Library). +- **Docs on testing**: `docs/API_TESTS.md`, `docs/MIDDLEWARE_TESTS.md`. + +## Documentation Index (in `docs/`) + +- `DEVELOPMENT.md` — broad dev guide. +- `API_*.md` — API testing and integration. +- `STRIPE_*.md` — Stripe integration deep-dives. +- `AUTH_*.md` — Authentication patterns. +- `DOCKER_*.md` — Docker build optimization. +- `CLOUD_BUILD_*.md` — GCP Cloud Build setup. diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..22a6602 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,141 @@ +# Testing + +**Last updated:** 2026-05-14 +**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source` + +## Frameworks + +- **Vitest** is the only JS/TS test runner across every app and package (no Jest) +- **React Testing Library** (`@testing-library/react`) for component tests; `@testing-library/jest-dom` matchers loaded via setup files +- **happy-dom / jsdom** for DOM emulation in frontend tests (`apps/main/vite.config.ts` configures `environment: "jsdom"`) +- **Vitest with mocked Supabase** for `apps/api` — node environment, real Hono router exercised, Supabase client stubbed (`apps/api/src/__tests__/setup.ts` + `globalSetup.ts`) +- **pgTAP** SQL tests under `supabase/tests/database/*.test.sql` for schema/RLS/trigger correctness (run via the Supabase CLI, separate from Vitest) + +## Test Commands + +From the repo root (all dispatched by Turborepo via `turbo.json`): + +```bash +pnpm test # Run every package's `test` task +pnpm test:watch # Watch mode across the workspace +pnpm test:api # Run only @xtablo/api (turbo --filter) +cd apps/main && pnpm test # Run a single package directly +cd apps/api && pnpm test:watch +``` + +Per-package script conventions (see e.g. `apps/api/package.json`): +- `"test": "NODE_ENV=test vitest run"` +- `"test:watch": "NODE_ENV=test vitest"` +- Frontend apps similarly use `vitest run` / `vitest` (no `NODE_ENV=test` prefix required) + +## Test File Location + +Tests are **co-located** with the source they exercise: + +- React components: `Foo.tsx` + `Foo.test.tsx` in the same folder + - e.g. `apps/main/src/components/CustomModal.test.tsx`, `apps/main/src/components/NavigationBar.test.tsx` +- Hooks / contexts: `useFoo.ts` + `useFoo.test.ts(x)` + - e.g. `apps/main/src/contexts/UpgradeBlockContext.test.tsx` +- API: tests under `apps/api/src/__tests__//.test.ts` (grouped by router/concern), plus co-located helper tests like `apps/api/src/helpers/orgIcons.test.ts` +- Expo app sometimes uses `__tests__/` folders: `xtablo-expo/components/__tests__/BillingPaywall.test.tsx` + +## Vitest Configs + +Two distinct flavors live in the repo: + +1. **Frontend (Vite + Vitest)** — config embedded inside `vite.config.ts`. Example, `apps/main/vite.config.ts`: + ```ts + test: { + globals: true, + environment: "jsdom", + setupFiles: "./src/setupTests.ts", + } + ``` + The Cloudflare plugin is skipped when `process.env.VITEST === "true"`. `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` are stubbed via `define` when running under Vitest. + + Other apps with the same pattern: + - `apps/admin/vite.config.ts` + `apps/admin/src/setupTests.ts` + - `apps/external/vite.config.ts` + `apps/external/src/setupTests.ts` + - `apps/clients/vite.config.ts` + `apps/clients/src/setupTests.ts` + +2. **API (standalone Vitest)** — dedicated `apps/api/vitest.config.ts`: + ```ts + test: { + globals: true, + environment: "node", + setupFiles: ["./src/__tests__/setup.ts"], + globalSetup: ["./src/__tests__/globalSetup.ts"], + testTimeout: 30000, + hookTimeout: 60000, + include: ["src/__tests__/**/*.test.ts", "src/**/*.test.ts"], + pool: "forks", + fileParallelism: false, + } + ``` + `fileParallelism: false` is deliberate — tests share initialized middleware state and can't safely run concurrently across files. + +## Setup Files + +- `apps/main/src/setupTests.ts` — imports `@testing-library/jest-dom`, registers an `afterEach(cleanup)`, mocks `ResizeObserver`, `Element.prototype.scrollIntoView`, `Element.prototype.scrollTo`, and `window.matchMedia`. Also imports `./i18n.test` to bootstrap i18next for tests. +- `apps/admin/src/setupTests.ts`, `apps/external/src/setupTests.ts`, `apps/clients/src/setupTests.ts` — analogous per-app setup +- `apps/api/src/__tests__/setup.ts` — minimal per-test-file setup (DB init handled by `globalSetup.ts`) +- `apps/api/src/__tests__/globalSetup.ts` — boots the test middleware manager via `createConfig()` reading `.env.test` + +## Mocking Patterns + +- **Frontend component tests** import the component and render it with a `renderWithProviders` helper (see `apps/main/src/utils/testHelpers.tsx` and `apps/clients/src/test/testHelpers.test.tsx`) that wires QueryClient, router, i18n, and Zustand stores. +- **React Query mocking**: integration tests mock the hook return values directly with `vi.mock(...)` and inject `{ data, isLoading, error }` shapes. +- **Supabase client**: API tests stub `supabase.from(...)` chains via `vi.fn()` — fixtures live in `apps/api/src/__tests__/fixtures/`. +- **Auth**: API middleware tests bypass real Supabase auth by overriding the Bearer-token validator and asserting on 401 paths (see `docs/MIDDLEWARE_TESTS.md`). +- **Toast / window APIs**: stubbed globally in `setupTests.ts` so individual tests don't have to. +- **i18n**: each frontend test setup imports `./i18n.test` so `useTranslation()` works without network. + +## API Test Patterns + +Documented in `docs/API_TESTS.md` and `docs/MIDDLEWARE_TESTS.md`. Key conventions: + +- Tests live under `apps/api/src/__tests__//.test.ts` (e.g. `notes/notes.test.ts`, `tasks/tasks.test.ts`) +- Each router has at minimum a smoke test that verifies the endpoint is reachable and returns the right shape +- Middleware is tested independently in `apps/api/src/__tests__/middlewares/middlewares.test.ts` — auth header validation, Supabase client injection, R2/Stream/email middleware behavior +- Test mode is detected via `NODE_ENV=test` in `createConfig()` so secrets load from `.env.test` instead of Google Secret Manager — no GCP credentials required to run the suite +- Fixtures (sample DB rows, sample Stripe payloads) live in `apps/api/src/__tests__/fixtures/` +- Helpers (token mocking, app builders) live in `apps/api/src/__tests__/helpers/` +- The `__tests__/README.md` documents the suite layout in-tree + +Snapshot from `docs/MIDDLEWARE_TESTS.md`: 116 passing tests across the API suite at last documented run (2025-11-10), with explicit coverage for the Supabase, Auth, Stream, R2, and email middlewares. + +## Coverage + +- **No coverage threshold is enforced** in CI today — `vitest.config.ts` files do not declare `coverage` blocks and `package.json` has no `test:coverage` script at the root. +- Coverage can still be produced ad-hoc with `vitest run --coverage` in any individual package, but it's not part of the normal workflow. +- The repo relies on (a) Biome lint errors blocking CI, (b) `pnpm typecheck` (`tsc -b`) blocking CI, and (c) `pnpm test` blocking CI — coverage % is currently advisory only. + +## Existing Test Inventory + +Total Vitest/RTL test files (`*.test.ts(x)`, excluding `node_modules`, `dist`): **147** + +Breakdown by area (approximate): +- `apps/main/src/**/*.test.tsx` — ~64 (components, contexts, providers, hooks) +- `apps/api/src/**/*.test.ts` — ~31 (routers, middlewares, helpers) +- `apps/admin/src/**/*.test.tsx` — pages, components, lib (e.g. `AnalyticsStudioPage.test.tsx`, `PrivilegedGate.test.tsx`) +- `apps/clients/src/**/*.test.ts(x)` — env + Vite config sanity checks plus page tests +- `apps/external/src/viteConfig.test.ts`, `apps/main/src/viteConfig.test.ts` — Vite build config smoke tests +- `xtablo-expo/**/*.test.ts(x)` — React Native (Expo) tests (e.g. `auth.test.ts`, `BillingPaywall.test.tsx`) +- `supabase/tests/database/*.test.sql` — 13 pgTAP files covering schema, RLS, triggers, indexes, Stripe + Apple billing functions + +Representative examples worth reading first: +- `apps/main/src/components/CustomModal.test.tsx` — minimal RTL test +- `apps/main/src/components/NavigationBar.test.tsx` — uses `renderWithProviders` +- `apps/main/src/providers/UserStoreProvider.test.tsx` — Zustand + React Query store test +- `apps/main/src/contexts/UpgradeBlockContext.test.tsx` — context provider test +- `apps/api/src/helpers/orgIcons.test.ts` — pure-helper unit test +- `apps/api/src/__tests__/middlewares/middlewares.test.ts` — middleware integration suite +- `supabase/tests/database/02_rls_policies_core.test.sql` — pgTAP RLS coverage + +## Reference Documents + +- `docs/API_TESTS.md` — API router test catalog, env setup, known limitations +- `docs/MIDDLEWARE_TESTS.md` — middleware-by-middleware coverage notes +- `docs/ENV_TEST_SETUP.md` — `.env.test` structure +- `docs/TEST_FIXES.md`, `docs/TEST_ROUTER_REFACTOR.md` — historical notes on test infrastructure changes +- `docs/TESTING_WITH_FAKE_ACCOUNTS.md` — temporary-account flow for manual + integration testing