System technical documentation
An overview document for audits and code review. It describes how the system is built, which patterns apply, and where the boundaries of responsibility lie. The conceptual specification ("what and why") is in System Architecture, operational instructions are in the Knowledge Base, and call examples are in the API Cookbook.
The source of truth for the state of the code is always the repository — this document links to files rather than repeating their content.
---
1. Guiding principle
> The schema definition is the single source of truth. Persistence, validation, the panel, REST, JSON:API, GraphQL, OpenAPI, permissions, events, and the SDK are all derived from it, never written by hand.
Consequence for review: if a feature requires a change in two places (e.g. adding a field in the database *and* in the API contract), that's an architectural bug — a derivation is missing.
---
2. Technology stack
| Layer | Technology | Notes |
|---|---|---|
| Framework | TanStack Start v1 (SSR + server functions) | src/router.tsx, src/routes/__root.tsx, src/start.ts |
| Routing | TanStack Router, file-based | src/routes/, tree generated into src/routeTree.gen.ts (not edited) |
| Server runtime | Cloudflare Workers (workerd) via Nitro | no Node host — see §9 |
| UI | React 19 + TypeScript 5.8 | strict mode, 0 tsc/tsgo errors |
| Styling | Tailwind CSS v4 (src/styles.css, @theme) | semantic tokens, no hardcoded colors |
| Components | shadcn/ui on Radix UI | src/components/ui/ |
| Server state | TanStack Query v5 | loader ensureQueryData + useSuspenseQuery |
| Forms | react-hook-form + zod | schemas generated from the model definition |
| Database / auth / storage | Postgres (Lovable Cloud) | RLS, public and cms schemas |
| GraphQL | graphql 17 (executable schema built at runtime) | src/platform/api/graphql.ts |
| Maps / charts | Leaflet, Recharts (lazy) | loaded after hydration |
| Build | Vite 8 + @lovable.dev/vite-tanstack-config | PWA via vite-plugin-pwa |
| Tests | Vitest 4 | 45 files / 413 tests |
---
3. Repository map
src/
routes/ # file-based routing (URL = file name)
index.tsx, blog.*, docs.*, use-cases.* # marketing + portal (public, SSR/SEO)
_authenticated/ # /admin/* panel behind a session gate (route.tsx)
api/public/ # public HTTP: REST v1, JSON:API, GraphQL, webhooks, cron
mcp/ # MCP servers (content + management), OAuth 2.1
platform/ # DOMAIN — pure functions, no React and no I/O
schema/ # field definitions, DDL compiler, validation, conditions, graph
api/ # surface, OpenAPI, GraphQL SDL, scopes, tokens
access/, permissions.ts, policy.ts # RBAC + permission sets + ABAC/FLS
tenancy.ts, tenant-scope.ts # multi-tenancy and isolation
publishing/, channels/, view/, i18n/ # lifecycle, channels, layouts, text
server/ # database access lives EXCLUSIVELY here (*.server.ts)
__tests__/ # domain tests (fast, no DOM)
lib/ # *.functions.ts — typed RPC (createServerFn)
components/admin/ # heavy panel views
components/ui/ # shadcn primitives
integrations/supabase/ # generated clients — NOT edited
supabase/migrations/ # the only way to change the database schema
docs/ # documentation = source of the /docs portalDirectory rule (to verify during review): src/platform/** (outside server/) may not import anything from server/, from @/integrations/supabase/*, or from React. This is the foundation for fast tests and for keeping secrets out of the client bundle.
---
4. Frontend
4.1 Routing patterns
- Every content section has its own route (SEO, SSR, og:*), not a hash anchor.
- The panel sits under the pathless
_authenticated/layout — the gate insrc/routes/_authenticated/route.tsxredirects unauthenticated users to/authbefore loaders run. That's why loaders under this tree can call protected functions. - A public route never calls a protected server function in its loader (prerendering has no session) — such reads go through
useServerFn+useQueryin the component. head()per route: a unique title/description/og — no metadata inherited from the root for leaves.
4.2 Data
The default read shape:
loader: ({ context }) => context.queryClient.ensureQueryData(opts)
component: useSuspenseQuery(opts)Forbidden: useEffect + fetch, manual isLoading for first-render data.
4.3 Performance and UX
pendingComponent+ skeletons on every heavier route.- Lazy import for Leaflet, Recharts, editors, and import dialogs (Drive).
- List virtualization (
@tanstack/react-virtual) in content and media views. - Service worker / PWA (
vite-plugin-pwa), precaching 128 entries. - Deterministic view cache (
platform_view_cache+ generations) — invalidated by bumping the generation, not TTL.
4.4 Design system
Colors, shadows, and gradients only as semantic tokens in src/styles.css. Components contain no hex values or classes like text-white. Direction: "Slate editorial" (IBM Plex, slate + amber).
---
5. Backend — boundaries
| Need | Mechanism | Location |
|---|---|---|
| Internal application logic (panel) | createServerFn (typed RPC) | src/lib/*.functions.ts |
| Public API for clients and integrations | server routes | src/routes/api/public/** |
| Outgoing webhooks, cron | server routes + queue | api/public/hooks/* |
| Agents / LLMs | MCP (content + management) with OAuth 2.1 | src/routes/mcp/** |
| Database access | only *.server.ts | src/platform/server/ |
Rules:
- Secrets (
process.env) are read inside the handler, never at module scope. - Protected functions use the
requireSupabaseAuthmiddleware; the token is attached byfunctionMiddlewareinsrc/start.ts. - The
/api/public/*prefix bypasses platform auth — every such handler verifies the caller itself (API token, HMAC signature for webhooks, scope). createServerFnlives in thin wrappers: only imports, types, and function declarations at module scope. All logic lives in imported modules (a bundler-splitting requirement).
---
6. Data model
6.1 Two schemas
- **
public.platform_*** — platform metadata: schemas and their versions, accounts and memberships, groups, roles, permission sets, API tokens, webhooks, events, audit, revisions, environments, promotions, locales, channels, assets, cache, jobs. - `cms.<model_key>` — tables generated by the compiler from the model definition, plus join tables
<model>__<relation>formanyToManyandcms.entry_translationsforlocalizedfields.
System columns on every content table: id, status, locale, account_id, created_at, updated_at.
6.2 Migrations
Database schema changes always go through a file in supabase/migrations/. Every create table in public requires, in the same migration: GRANT → enable row level security → policies. cms.* tables deliberately have no grants for anon/authenticated — they are not exposed via the Data API; all access goes through the server layer, which enforces scopes and FLS.
---
7. Security and multi-tenancy
Order of access resolution (a single place, src/platform/permissions.ts + access/permission-sets.ts):
role baseline → grant sets → restrict sets → schema policies (deny always wins)- Field-Level Security exclusively via
readableFieldsFor/writableFieldsFor. No handler returns a raw row. - Multi-tenancy: an account tree (org → company → brand → project),
account_idon every row, thex-accountheader, rules insrc/platform/tenant-scope.ts, query filtering and a row guard insrc/platform/server/tenant-filter.server.ts. - A row belonging to another tenant returns 404, not 403 — no existence oracle. Rows with
account_id = nullare shared platform defaults, read-only for tenants. - A write with multiple scopes and no pinned
accountIdis rejected (no "orphaned" data). - Event fan-out (webhooks, SSE, GraphQL subscriptions) matches only events with an identical
account_id. - Relation expansion (
include=), revisions, and layouts pass through the same guard — history is projected through FLS too. - API tokens have a scope grammar narrowing models, operations, fields, and channels; MCP and REST use the same enforcement path.
- A client with service-level privileges (
supabaseAdmin) bypasses RLS — it may only be used after verifying the caller, inside the handler, via dynamic import. The role is never resolved with this client.
For review: see src/platform/__tests__/tenant-isolation.test.ts and runtime-isolation.test.ts — 11 cross-tenant scenarios plus an import guard.
---
8. Content lifecycle and delivery
- Statuses and allowed transitions are defined only by
src/platform/publishing/lifecycle.ts:draft → review → scheduled → published(+archived). No component compares status strings on its own. - Publish scheduler: cron →
api/public/hooks/publish-scheduled. - Versioning: immutable JSONB snapshots in
platform_entry_revisions, rollback as a new write (history is never edited). - Environments: Dev → Staging → Prod with review gates and a promotion chain; Production serves only
published, Preview also serves drafts. - Delivery channels: a field without tags is universal, a field with tags is delivered only to the listed channels. Projection lives in
src/platform/channels/channels.ts, selection via?channel=. - Events:
platform_eventsoutbox → webhook delivery with HMAC and retry → SSE for live clients.
---
9. Runtime constraints (Workers)
The server runs on workerd, not Node. Not allowed: child_process, sharp/canvas/puppeteer, fs.watch, os.cpus(), packages requiring native binaries or a real filesystem. Everything must be fully bundled at build time — there is no module resolution at runtime. Symptoms of incompatibility: [unenv] X is not implemented yet!, __dirname is not defined, "works in dev, fails in prod".
---
10. Code conventions
- Domain = pure functions. I/O =
*.server.ts. UI = components with no business logic. - No duplicating a rule in two places — one rule, one location (lifecycle, channels, scope, FLS, field conditions).
- Panel text goes through the
src/platform/i18n/catalog.tscatalog (EN), never strings in components. Documentation and communication are in Polish, identifiers in English. - A new field/type/interface = an entry in the registry (
schema/field-types.ts,field-variants.ts,api/surface.ts) + a test + documentation. - Files above ~600 lines are candidates for splitting (current exceptions are listed in the audit).
---
11. Testing and quality
bunx vitest run— 45 files, 413 tests, ~4 s. They test domain rules, not I/O mocks.- Every new feature and every fixed bug gets a test (a regression test for the exact scenario).
- Gates before a task counts as done:
tsgoclean,bun run buildpasses, the whole suite green. - The interface registry (
/admin/interfaces) has a regression guard — a new endpoint without a catalog entry fails a test.
Quality elements currently missing (a conscious backlog item): route E2E tests in CI, alerting on the webhook queue.
---
12. Build and deployment
bun run build→ client + SSR worker + service worker; artifacts indist/, worker configuration generated automatically.- Stable public URLs for cron jobs and integrations:
project--<id>.lovable.app(prod) andproject--<id>-dev.lovable.app(preview). - Pre-production-release checklist:
DEMO_LOGIN_ENABLEDdisabled, storage policies for the asset bucket, narrowed SELECT onprofilesanduser_roles. Full list and priorities:docs/CODE-AUDIT-2026-08-11.md.
---
13. How to review this system
The order that most quickly surfaces real problems:
src/platform/schema/compiler.ts+validate.ts— do DDL and validation really originate from a single definition.src/platform/permissions.ts,access/permission-sets.ts,tenant-scope.ts— is the deny/allow order and FLS exceptionless.src/platform/server/entries.server.ts— does every read and write pass through the guard and field projection (especiallyexpand()).src/routes/api/public/**— does every handler verify the caller before doing anything.supabase/migrations/— does every new table have GRANT + RLS + policies.src/platform/__tests__/— do the tests cover the rules, not just the happy path.docs/CODE-AUDIT-2026-08-11.md— known gaps and priorities, so they aren't reported a second time.