xtablo-source/.planning/codebase/ARCHITECTURE.md

163 lines
11 KiB
Markdown
Raw Permalink Normal View History

2026-05-14 14:01:31 +00:00
# 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`).