# 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`).