docs: map existing codebase
This commit is contained in:
parent
d9bf94583b
commit
2c23906716
7 changed files with 1168 additions and 0 deletions
162
.planning/codebase/ARCHITECTURE.md
Normal file
162
.planning/codebase/ARCHITECTURE.md
Normal file
|
|
@ -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 <jwt>` 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<typeof getMainRouter>`) 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`).
|
||||
126
.planning/codebase/CONCERNS.md
Normal file
126
.planning/codebase/CONCERNS.md
Normal file
|
|
@ -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.
|
||||
161
.planning/codebase/CONVENTIONS.md
Normal file
161
.planning/codebase/CONVENTIONS.md
Normal file
|
|
@ -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/<app>/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/<pkg>/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)
|
||||
140
.planning/codebase/INTEGRATIONS.md
Normal file
140
.planning/codebase/INTEGRATIONS.md
Normal file
|
|
@ -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).
|
||||
142
.planning/codebase/STACK.md
Normal file
142
.planning/codebase/STACK.md
Normal file
|
|
@ -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`.
|
||||
296
.planning/codebase/STRUCTURE.md
Normal file
296
.planning/codebase/STRUCTURE.md
Normal file
|
|
@ -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/<app>/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.
|
||||
141
.planning/codebase/TESTING.md
Normal file
141
.planning/codebase/TESTING.md
Normal file
|
|
@ -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__/<area>/<area>.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__/<area>/<area>.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
|
||||
Loading…
Reference in a new issue