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 componentapps/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 inapps/clients/src/routes.tsx. - Admin app (
apps/admin): internal admin tools. Entry:apps/admin/src/main.tsx, routes inapps/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:
- 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 inpackages/shared/src/lib/supabase.ts(re-exported viaapps/main/src/lib/supabase.ts). - 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 inpackages/shared/src/lib/api.ts(re-exported viaapps/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 tosupabase.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
supabasemiddleware 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:
loadSecrets()pulls secrets (locally from env, in staging/prod from Google Secret Manager) —apps/api/src/secrets.ts.createConfig(secrets)produces the typedAppConfig—apps/api/src/config.ts.MiddlewareManager.initialize(config)constructs the middleware singleton —apps/api/src/middlewares/middleware.ts.- The root Hono app applies
logger()and a CORS middleware that only accepts*.xtablo.comandlocalhostorigins. - All routes mount under
/api/v1viagetMainRouter(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:
/public— unauthenticated (public.ts)./tasks— task router (tasks.ts)./revenuecat-webhook,/stripe-webhook— webhook receivers./admin— admin-only routes (admin.ts,adminAuth.ts, ...)./client-auth,/client-portal,/client-invites— client portal stack./—maybeAuthRouter.ts(optional auth — must come before authed to allow public booking)./—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/sharedis 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 inpackages/shared/src/index.ts.packages/ui— Radix + Tailwind component library. Source-only. Components inpackages/ui/src/components/(button.tsx,dialog.tsx,select.tsx, ...).packages/shared-types— zero-runtime-dependency TypeScript types. Auto-generateddatabase.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.jsonat repo root). apps/mainand other Vite apps deploy to Cloudflare Workers viawrangler.tomland the bundledworker/folder.apps/apicompiles TypeScript and deploys to Google Cloud Run; secrets resolved via Google Secret Manager.apps/chat-workerdeploys as a Cloudflare Worker with Durable Objects.- Observability: Datadog RUM on frontends (
apps/main/src/lib/rum.ts),dd-traceon the API (initialized at the top ofapps/api/src/index.ts).