MATRIZ — Architecture Overview
Repaired 2026-08-22 (doc reset,
D:doc-reset). For what works right now seeSTATUS.md; for why decisions were made seeDECISIONS.md; for open owner work seeplans/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.
The stack (post-pivot)
Section titled “The stack (post-pivot)” 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)Packages (pnpm workspace)
Section titled “Packages (pnpm workspace)”| 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. |
Data plane
Section titled “Data plane”- 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.tsis a thin re-export shim (the import path for the Workers app,drizzle.config.ts, and thedb-guarddrift 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. ProjectsY.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).
Request security
Section titled “Request security”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).
The two editor write paths
Section titled “The two editor write paths”- 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_valuesper-key merge so the DO’s text projection is never clobbered — D27). This is the sole save path: theVITE_SAVE_V2flag was flipped + retired and the legacyOperationQueuepurged (D24). - 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).
Publish pipeline (live; D28)
Section titled “Publish pipeline (live; D28)”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.
CI / deploy
Section titled “CI / deploy”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.yml—db:check+ Drizzle schema-drift gate.deploy-editor.yml— build +wrangler pages deploy frontend/dist(Pages projectskyfall, a frozen legacy id; live at app.matriz.design). Note it overlaps the Pages project’s own Git-integration deploy — seeplans/OWNER-OPS.mdabout picking one.deploy-api.yml—wrangler deploythe Worker (skyfall-api). Also overlaps the Cloudflare Workers Builds Git integration (root directoryworkers).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,.nvmrc20) so Cloudflare’s build is reproducible.