Skip to content

MATRIZ — Architecture Overview

Repaired 2026-08-22 (doc reset, D:doc-reset). For what works right now see STATUS.md; for why decisions were made see DECISIONS.md; for open owner work see plans/OWNER-OPS.md.

The Clerk B2B2C pivot (D51) landed 2026-07-03: Clerk auth + Split-Glass (Projects/Targets) is the running system described below. The old pivot plans are archived under archive/2026-08-22-doc-reset/.

MATRIZ is a visual website builder: a browser editor where users compose pages from a tree of elements + reusable components bound to a CMS, then publish the result as a static site.

Editor SPA (frontend/) ──fetch /api/*──▶ Workers API (workers/, "skyfall-api")
React 19 + Vite + pnpm ◀─WS /api/yjs─── Hono router
Cloudflare Pages │
┌───────────────┬─────────┼───────────┬──────────────────┐
▼ ▼ ▼ ▼ ▼
Neon Postgres Durable Object R2 @matriz/site-render
(Drizzle) SiteDO / Y.Text assets compiler + tokens +
39 tables text island + pub html-serializer (shared)
Package Dir Role
frontend frontend/ Editor SPA (React 19 + Vite), deployed to Cloudflare Pages. Talks to the API over fetch /api/* and a Yjs WebSocket.
skyfall-api workers/ API + data plane: Hono routes, Drizzle/Neon access, the SiteDurableObject, R2 access, the publish pipeline. Deployed as a Cloudflare Worker.
@matriz/site-render packages/site-render/ Runtime-agnostic rendering core (component compiler + token resolver + static HTML serializer), shared by the editor preview and the publish pipeline so both render identically.
@matriz/schema packages/schema/ Canonical Drizzle table model + row DTOs ($inferSelect/$inferInsert) + slot/value contracts, shared by the Workers app and the import pipeline (D37).
@matriz/import packages/import/ Source-blind import adapters (CSV/JSON/XLSX/Webflow/Bondlayer/site-json) behind one wizard contract.
@matriz/email-render packages/email-render/ Email template rendering shared by the marketing designer and campaign send.
  • Neon Postgres via Drizzle ORM. The canonical data model is @matriz/schema (packages/schema/src/schema.ts, 39 tables) (D37) — projects/targets, dom_nodes, components, design systems/tokens, content types/items, site pages, assets, builds, orgs/users, …; workers/src/db/schema.ts is a thin re-export shim (the import path for the Workers app, drizzle.config.ts, and the db-guard drift check). The live DB also carries a few orphaned pre-pivot tables pending a drop (DECISIONS D17).
  • R2 for binary assets (ASSETS_BUCKET) and Yjs snapshots (YJS_SNAPSHOTS), and as the static host for published sites (published/<siteId>/…).
  • Durable Object SiteDurableObject (workers/src/do/site-do.ts): one per site, the Yjs CRDT working-state tier for collaborative text. Projects Y.Text → dom_nodes.slot_values (one-way) on flush.

There is no Supabase, no PostgREST, no Rust backend — all removed in the pivot. The Workers API (Hono) is the sole data layer (DECISIONS D25/D26).

Clerk is the identity provider (live since 2026-07-03; the Clerk org IS the project). Every tenant-scoped route runs authMiddleware (@clerk/backend verifyToken; sets userId/orgId) then projectAccessMiddleware (org-claim fast path, else a project_users join; sets projectId/targetId). Postgres RLS policies exist (db/migrations/20260703_clerk_rls_baseline.sql) and getScopedDB routes through the tenant role only when RLS_ENFORCED + the authenticated URL secret are both set — until that is verified in prod, app-layer middleware is the effective isolation (see db/RLS-ACTIVATION.md and the STATUS punchlist).

  1. Structural / property writes → the store-owned debounced save sink (frontend/src/lib/save-v2/) → POST /api/nodes/batch (Drizzle upsert-by-id + cascade-delete; slot_values per-key merge so the DO’s text projection is never clobbered — D27). This is the sole save path: the VITE_SAVE_V2 flag was flipped + retired and the legacy OperationQueue purged (D24).
  2. Text content → the Yjs Y.Text island: edits route into the CRDT, the DO owns the text and projects it to slot_values. Wire/protocol present; live WS session activation is a remaining Phase 3.5 task (see STATUS).

POST /api/publish/:siteId: force-flush the site DO → read published dom_nodes/site_pages/design_tokens/content_items → compile each page to static HTML via @matriz/site-render (component instances compiled; CMS repeaters expanded from the persisted data_binding — D29) with inlined token CSS → write the bundle to R2. Published sites serve publicly at GET /published/:siteId[/path]; with the PUBLISH_ROOT_DOMAIN var set, that path route instead 301s to the target’s canonical host (custom domain → platform subdomain).

Published pages ship a strict CSP whose per-page inputs travel as R2 customMetadata: cspScriptHash (hash-pinned interactions runtime; script-src is forever 'none'|hashes — CI-gated) and cspFrameSrc (embed-provider origins). Iframes come only from the 8-provider embed registry (embed-registry.ts); custom_code nodes publish as opaque-origin sandboxed srcdoc iframes (allow-scripts allow-forms hardcoded). The full third-party origin surface is pinned in packages/site-render/src/third-party-ledger.ts. servePublishedAsset also records cookieless page views to the optional PUBLISH_ANALYTICS Analytics Engine binding.

  • frontend-ci.yml — lint + typecheck + build + test (frontend).
  • workers-ci.yml — lint + typecheck + tests (unit + env-gated live-Neon integration).
  • packages-ci.yml — typecheck + lint + test for the @matriz/* workspace packages (schema, site-render).
  • db-guard.ymldb:check + Drizzle schema-drift gate.
  • deploy-editor.yml — build + wrangler pages deploy frontend/dist (Pages project skyfall, a frozen legacy id; live at app.matriz.design). Note it overlaps the Pages project’s own Git-integration deploy — see plans/OWNER-OPS.md about picking one.
  • deploy-api.ymlwrangler deploy the Worker (skyfall-api). Also overlaps the Cloudflare Workers Builds Git integration (root directory workers).
  • deploy-docs-product.yml / deploy-docs-dev.yml — build + deploy the Starlight doc sites (apps/docs-product → docs.matriz.design, apps/docs-dev → dev.matriz.design).
  • Toolchain pinned (packageManager: pnpm@10, .nvmrc 20) so Cloudflare’s build is reproducible.