Skip to content
DezfilDocsv0.1.0

Architecture

Find where each part of the Dezfil container lives in the source, and keep the rules that hold the parts together.

Dezfil is one Node.js server in one container. It keeps everything it stores in one data folder:

One container
browser AI agent
React UI, deck.gl over MapLibre GL any MCP client
| |
| HTTP /api/... | HTTP /api/mcp
+-----|-----------------------------------------|---------------+
| v Next.js server, port 3000 v |
| pages and API routes <------ HTTP ------ MCP endpoint |
| | |
| v |
| service modules |
| | | | | |
| v v v v |
| SQLite DuckDB DuckDB picture renderer |
| metadata trusted: hardened: (Chromium) and |
| store imports and user SQL, tile builder |
| inspection read-only (tippecanoe) |
| | | | |
+-----|-------------|-----------------------------|-------------+
v v v
metadata.sqlite tables/ (GeoParquet) tiles/ (PMTiles)
profile-pictures/ (WebP) thumbnails/
(all in the data folder)

Both DuckDB instances run in memory, and the metadata store is a SQLite file. What must outlive a restart is a file in the data folder, so there is no database server to run.

The data folder is DEZFIL_DATA_DIR, with its default in Storage and programs. Back up and restore lists what it holds.

PartIn the sourceIts job
Pagessrc/app/(workspace)/, src/app/(entry)/, src/app/v/, src/components/The React screens: the workspace, the entry pages (sign-in, setup, accepting a link) and the published viewer at /v/<token>
Map canvassrc/components/map/deck.gl layers drawn over a MapLibre GL basemap
API routessrc/app/api/Every HTTP operation, one route.ts per path. The sign-in gate in src/proxy.ts refuses a request that needs a sign-in and has none. A handler checks what the caller may reach and calls the service modules.
Service modulessrc/lib/The work behind the routes: imports, layers, tiles, operations, publishing, sharing
API descriptionsrc/lib/api/openapi.ts and the openapi-*.ts files beside itThe OpenAPI description, served at GET /api/openapi.json and written to docs/api/openapi.json
Metadata storesrc/lib/metadata/Accounts, maps, layers, projects, workflows and settings in metadata.sqlite, with the migrations that build it
Profile picturessrc/lib/profile-pictures/Turns a person’s upload into the server’s own WebP in profile-pictures/, one per account, and serves it
Trusted DuckDBsrc/lib/duckdb.ts, src/lib/trusted-*.ts, src/lib/ingest/Reads imported files and writes each dataset as one GeoParquet file in tables/
Hardened DuckDBsrc/lib/duckdb.ts, src/lib/query/Runs user SQL and every layer’s query, fenced to tables/ and the tile scratch folder
Tile buildersrc/lib/tiles/Builds a PMTiles archive with tippecanoe for a large point layer, and serves its tiles from the archive
Picture renderersrc/lib/export/, src/lib/pictures/, src/app/render/Headless Chromium opens the app’s own render page and takes a screenshot, for image exports and the pictures on map cards
MCP endpointsrc/app/api/mcp/, src/lib/mcp/, src/lib/tools/The documented operations as MCP tools. Each tool call is an HTTP request to the API.
Assistantsrc/lib/assistant/The in-app assistant, which calls the same tools as an MCP client
Startupsrc/instrumentation.tsChecks that the data folder is writable, opens the metadata store, removes leftover files, settles interrupted runs and starts the map picture work

The Dockerfile builds the image in stages on node:24-slim. Its last stage holds everything the server runs, so the server starts, and its engine loads every extension, with no network. What leaves the instance lists the requests a running instance can make.

In the imageWhereWhat it is for
The production build, as the Next.js standalone server/appThe pages, the API and the MCP endpoint
tippecanoe 2.79.0, compiled in a build stage of its own/usr/local/bin/tippecanoeThe tiles of large layers
Chromium, a certificate bundle and the Liberation fonts/usr/bin/chromium, named by DEZFIL_CHROMIUM_PATHImage exports, the pictures on map cards and the look at a map the assistant or an agent asks for, drawn with software GL
The engine’s extensions: spatial, h3, httpfs, json, postgres, azure and aws/app/duckdb-extensions, named by DUCKDB_EXTENSION_DIRInstalled at build time by scripts/install-duckdb-extensions.mjs from the one list in scripts/duckdb-extensions.mjs, which the offline check reads too
The native packages the standalone build leaves out: @duckdb, better-sqlite3, bindings and file-uri-to-path/app/node_modules/The engine’s and the metadata store’s native code, copied whole
The picture library sharp, the binary and libvips of the image’s own architecture under @img, and detect-libc and semver, which it requires/app/node_modules/Profile pictures: read, cropped and written as WebP. It loads on first use, so without it only pictures are refused
public/, the model catalogue and the shipped templatesAt their paths in the sourceRead at run time where the source keeps them
LICENSE and NOTICE/appThe licences of the code and the components the image carries
The ingress preload and the account recovery script, with the modules they import/app/scripts/, /app/src/lib/auth/The preload starts before the server; an operator runs the recovery script

The image runs as the unprivileged node user, listens on port 3000 and keeps its data in the /data volume. Development setup builds it and runs its two checks.

  1. The builder asks for each layer’s data with GET /api/layers/<id>/render, in src/components/builder/render-data.ts.
  2. The route, src/app/api/layers/[id]/render/route.ts, checks that the caller can reach the layer and passes the request’s abort signal on.
  3. src/lib/layer-render.ts builds the effective query with src/lib/query/compile.ts: the layer’s SQL, refined by its filters, then aggregated by its visualization.
  4. src/lib/query/bounded.ts runs it on the hardened instance within a time budget. It stops the engine when the budget runs out or the caller leaves.
  5. The answer carries GeoJSON for deck.gl. A point layer over the render limits answers with tiles instead, which src/lib/tiles/build.ts builds in the background.

The SQL workspace takes the same road through POST /api/query, and a published map through GET /api/published/<token>. The browser reaches the engine only through the API.

  • One container, one data folder: a feature that needs a second service, such as a database server, a cache or a job runner, does not fit. Durable state is a file in the data folder, never inside an engine.
  • DuckDB on the server only: there is no query engine in the browser. The client calls the API, and the server’s engine works on data larger than browser memory.
  • User SQL on the hardened instance: at its start it loads its extensions, fences the filesystem, sets a memory limit with no spilling to disk, turns off network access and extension loading, then locks its configuration. Never widen it to make a feature easier.
  • One read-only statement: src/lib/query/guard.ts admits exactly one SELECT, classified by DuckDB’s own parser rather than a list of keywords. src/lib/query/guard.test.ts holds it.
  • SQL is the source of truth for a layer: a layer is its SQL, a name and a style. Style draws the query’s result and never becomes a second query. Rendering, classification, field statistics and publishing all build the effective query in src/lib/query/compile.ts.
  • Every capability is an API the UI calls: new behavior starts as a documented operation with a route behind it. src/lib/api/parity.test.ts fails on a route without an operation, an operation without a route, or a call from src/components/ to an undocumented path.
  • The API description is a published contract: a renamed path, field or error code is a breaking change. If an agent would need a warning, change the API until the mistake is impossible or harmless.
  • MCP tools come from the API description: src/lib/tools/tools.ts makes one tool of each documented operation that names one in its x-tool extension. src/lib/tools/tools.test.ts fails when the two drift apart.

An operation declares its tool in its openapi-*.ts file: tool names the tool, or false leaves the operation out, and a tool needs display, the words the assistant shows for a call. Then regenerate docs/api/openapi.json as Development setup shows.

Dezfil · Apache-2.0docs from 7439201b