xtablo-source/.planning/codebase/ARCHITECTURE.md
2026-05-14 16:01:31 +02:00

11 KiB

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:

["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 AppConfigapps/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).