Architecture
How the pieces fit — the React app's five sections, the Fastify server's core and eight modules around its single query choke point, one native DuckDB engine per workspace, and the Drizzle metadata store.
┌──────────────── React 19 + Vite + Tailwind v4 (two themes, one kit) ────────────────┐
│ Home #/ Agent Home: one prompt, Needs you, Continue │
│ Library #/library dashboards · Mosaic dashboards · notebooks (Stories, │
│ Canvas) · saved queries · apps · briefs · missions │
│ editors SQL workbench #/library/queries · notebook · grid and │
│ Mosaic dashboard · data app — each an EditorPage │
│ Data #/data/tables tables, views and files; a table's page (overview, │
│ columns, checks, lineage & access); Prepare editor │
│ #/data/sources connections and folders · #/data/pipelines syncs, │
│ streams, reverse ETL · #/data/models dbt · │
│ #/data/metrics semantic layer and monitors │
│ Activity #/activity needs you (approvals, failing checks, insights) · runs │
│ · alerts and snapshots · comments │
│ Settings #/settings/<page> Profile · Workspace · People · AI · Developers · Admin │
│ ⌘K every command, object, table, page and setting; "Ask about this" in "..." │
└──────────────┬────────────────────────────────────────────────────────────────────────┘
│ REST · WebSocket (rows; one shared live-event stream) · SSE (missions, MCP) · Streamable HTTP
┌──────────────▼────────────────────────────────────────────────────────────────────────┐
│ Fastify 5 (TypeScript strict) — a core and eight modules (src/modules/<name>) │
│ core: config · metadata store · engines · cipher · auth (local, OIDC, SCIM, tokens) │
│ workspaces · QueryService (authz → SQL guard → access policies → HITL → audit) │
│ result cache · JobRunner (one jobs table) · RunStore (one runs table) │
│ Sources connections table + drivers · cloud · databases · lakehouse · connectors │
│ Pipelines syncs · streams · CDC · reverse ETL · orchestration │
│ Modelling lineage · dbt · semantic layer · quality · insights · Prepare │
│ Analysis workbench · saved queries · dashboards · Mosaic · notebooks · comments · │
│ Library · builder · Canvas · exports · search │
│ Delivery data apps (subprocess · docker · k8s, own origin) · alerts · snapshots · │
│ embeds · query endpoints · style guide │
│ AI model settings · inline actions (assist) · agent runtime (missions, │
│ scheduled missions, briefs) · A2A · CLI · OAuth · Slack · Teams │
│ Governance teams · access policies · approvals · Activity · audit export · SCIM · │
│ tags · PII · watches │
│ Platform cluster · notification channels · quotas · usage · templates · │
│ Postgres protocol · revisions · Git · live events │
│ Tool registry (94 tools) → MCP (100 with the agent's own · 4 resources · 6 prompts) │
│ + REST façade /api/agent/v1/tools + OpenAPI 3.0 │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ EngineManager ─ one DuckDB instance per workspace (LRU + idle TTL); cluster: a lease │
│ filesystem jail (Node) + allowed_directories / enable_external_access + lock_config │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Metadata store (Drizzle): SQLite by default · PostgreSQL via DATABASE_URL │
│ jobs · runs · connections · approvals · counters · exports · agent_* · ai_usage · … │
└───────────────────────────────────────────────────────────────────────────────────────┘The one path every query takes#
Whether it comes from the workbench, a WebSocket stream, a dashboard widget, an MCP tool or the REST façade, SQL goes through QueryService:
- Access —
WorkspaceService.get(principal, workspace, minRole)resolves the effective role (owner, direct grant, team grant; admins from the UI) —404for non-members,403for insufficient roles. - Authorization — scope (
read/write/admin), platform role, workspace role, access policies (row filters and column masks rewritten into the statement), and the human-in-the-loop gate for agents. - Cache — for read-only, deterministic statements a key built from the referenced files'
size:mtimeand the workspace data epoch is checked first. - Guard — the lexer classifies statements and rewrites path literals into the jail; extension and setting policies apply.
- Engine — a fresh DuckDB connection with a timeout (
interrupt()on cancel), row caps and cell truncation. - Audit — one row per execution with actor, workspace, SQL, duration, status; a mutation also moves the data epoch and notifies members over the live feed.
ZAANIX Agent#
The agent runtime lives in the server's AI module (modules/ai/agent-runtime): a decision engine that picks context and tools without a model, a context engine that ranks what the workspace holds within a token budget, and the model. It runs as the person — reads call the services with their identity, tools go through the same tool registry as MCP with the agent as the actor — so every call meets the QueryService choke point above, and changes wait for approval. Missions, tasks, briefs, memories and held calls live in the metadata store, so a restart picks up where it was. Inline AI actions (POST /api/agent/assist) are single model calls through the same governance. See ZAANIX Agent.
Jobs, runs and connections#
One JobRunner and one jobs table schedule everything — syncs, alerts, snapshots, dbt, quality suites, monitors, reverse syncs, watches, scheduled missions and the system's own housekeeping — and run each due job exactly once, also in a cluster. Every run of every kind is a row of one runs table, read by each feature in its own shape and by Activity › Runs all together. Every stored connection (secrets, cloud, lakehouse, database, connector) is a row of one connections table, its credentials encrypted with the row id as associated data. Nothing a feature needs is kept only in a process's memory: exports, rate counters, single-use tokens and orchestration waits are in the metadata store too.
Cluster mode#
Several nodes can share one PostgreSQL metadata store and a ReadWriteMany data directory. A lease names the node that opens each workspace's DuckDB file; other nodes forward that workspace's queries to it, and scheduled work is claimed by one node at a time. See Running ZAANIX for a team.
Engines#
EngineManager keeps one DuckDBInstance per workspace, LRU-bounded (max_cached_engines) and idle-evicted (engine_idle_ttl_seconds). Each engine starts with memory/thread/temp settings, loads allow-listed extensions, applies the owner's cloud and lakehouse secrets and ATTACHes catalogs, then locks the configuration. Secrets and attachments can be re-applied to a running engine without losing in-memory tables; a :memory: engine that restarts moves the workspace epoch so cached results over its (now gone) tables are not served.
Project layout#
packages/server/src
config/ db/ engine/ security/ loader, Drizzle schemas and migrations, the jail and SQL guard, crypto
context.ts · services/ the core: auth, workspaces, query, cache, jobs, runs, counters, audit
modules/<name>/ sources · pipelines · modelling · analysis · delivery · ai · governance · platform
each with its services, routes/ and tools.ts, registered through modules/kernel.ts
agent/ · mcp/ the tool registry (one list for MCP and REST), OpenAPI, the MCP server
observability/ pino, prom-client, OpenTelemetry, the live event bus
packages/web/src
app/ routes (five sections; every old hash redirects), settingsNav
components/ui · page · data the kit, the three page templates (List, Object, Editor), result grids
features/ agent · library · data · activity · settings · workspace · notebooks · dashboards · …
store/ · lib/ · theme/ zustand stores, one shared live-event socket, Light and Dark tokens
packages/agent-cli zaanix-agent, served by the server
packages/sdk-python the zaanix Python SDKTests#
More than 820 tests: the server's 649 boot real DuckDB engines and the MCP server over every transport, serve real Iceberg tables through a mock REST catalog, emulate Databricks, warehouses, SaaS and Google APIs, run migrations on SQLite and PostgreSQL, and pin every route and tool; the web app's 170 cover routes and redirects, the kit, the page templates and the themes' contrast. Browser checks drive the real UI (60+ scenarios, a visual QA of both themes, inline AI and no-model walks), and scripts/smoke.mjs verifies a running instance or the Docker image.