Architecture
The source decisions are in Linear: V1 Architecture Decisions and the Implementation Blueprint. This page describes what’s built.
The system
Section titled “The system”
flowchart LR
browser["Browser
React app"]
scripts["Scripts and agents
(API key)"]
subgraph render["Render (Frankfurt)"]
app["TanStack Start server
routes · server functions
/api/v1 · server routes"]
docs["Docs site
(static)"]
cron["Daily purge cron"]
end
subgraph supabase["Supabase (EU)"]
auth["Auth"]
appschema[("app schema
orgs, members, maps,
views, sessions")]
network[("network schema
nodes, connections,
each map's catalogue")]
storage["Storage
map-imports"]
end
resend["Resend
(email)"]
browser -- "server functions (cookie session)" --> app
browser -- "direct upload (signed URL)" --> storage
scripts -- "/api/v1 (Bearer key)" --> app
cron -- "POST /api/maintenance/purge" --> app
app --> auth
app --> appschema
app --> network
app --> storage
app --> resend
| Concern | Choice |
|---|---|
| App framework | TanStack Start (Router and Query) with React 19 |
| Client graph state | TanStack DB collections for nodes and connections |
| Database and auth | Supabase (Postgres), EU region, two schemas: app and network |
| Edit-mode canvas | @xyflow/react (React Flow) |
| Visualise-mode canvas | react-force-graph-2d |
| Supabase Auth email, plus Resend for invitations | |
| Hosting | Render, Frankfurt, configured in render.yaml |
| Monorepo | pnpm workspaces: apps/* and packages/* |
| Lint and format | Biome, one config at the root |
| Docs | Astro Starlight (apps/docs) |
Hard rules
Section titled “Hard rules”These apply to every change.
- No cross-schema SQL joins between
appandnetwork. Need data from both? Run two queries and combine the results in application code.networkrefers toapponly through plainuuidmap_idcolumns with no foreign keys, so it can move to its own database later. - Guard every tenant-data server function twice: a route
beforeLoadguard for the user experience, andauthMiddleware/orgMiddlewareplus an authz helper for security. Validate sessions withsupabase.auth.getClaims(), nevergetSession(), on the server. See Auth and permissions. - Service-role writes must set
map_idon everynetworkrow (anduser_idwhere relevant), because the service role bypasses RLS. Nothing innetworkhas anorg_id. An organisation owns maps, members, invitations and API keys, and everything inside a map belongs to the map. - All data stays in the EU (Supabase and Render). Some users work in politically sensitive contexts. The region is fixed when a project is provisioned.
- Pin
@tanstack/*and the visualisation libraries exactly, with no^or~. CI installs with--frozen-lockfile. - pnpm only, Biome only.
- Use the new Supabase key format (
sb_publishable_…andsb_secret_…).
How the app is built
Section titled “How the app is built”- Server functions (
createServerFn) are the app’s own API: type-safe calls from the browser, grouped by area inapps/app/src/server/functions/. See Server functions and routes. - Server routes return raw HTTP responses. They serve map export and import, the
maintenance purge, the health check, and the public REST API under
/api/v1(see Public API). - Three modes, one workspace. A map page (
/maps/$mapId) has Edit, Visualise and Table modes, chosen by themodesearch param. All three stay mounted, and switching never reloads data. They share one pair of TanStack DB collections, filled from onegetGraphcall. Edits apply to the collections at once, and save in the background. See Editing internals. - Two renderers, one data shape. Edit mode draws with React Flow, and Visualise with
react-force-graph-2d. Both read theGraphDatatype insrc/types/graph.ts. - Centrality and layout run in the browser. At the expected scale (about 180k nodes and 400k connections across all maps), Postgres with good indexes is enough. There’s no graph database.
- Deploys are gated on releases. The production app deploys only when a GitHub release is published, after production migrations succeed. See Deployment.
Where each kind of workspace UI goes is covered in Workspace UI.