Documentation
For coding agents — ZigBase
The ~2k-token entry point for AI agents working with ZigBase — what it is, the traps that bite, the response shapes you can rely on, and which guide to load next.
Start here, then load one or two of the linked guides. The full corpus is ~200k tokens; you almost never need it.
For interrupted client file transfers, load resumable uploads: opt-in, principal-bound and bounded. Default RAM sessions are process-local; explicit SQLite/local durability preserves sessions across process restarts. Neither mode provides streaming or cross-instance recovery. For image derivatives, load thumbnails: named compile-time profiles, an opt-in ImageMagick subprocess, and explicit deployment resource limits. Do not assume an embedded codec or arbitrary transformation URLs.
What ZigBase is
A single binary: REST API, WebSocket realtime, file storage, argon2id + JWT auth, OAuth2, an admin UI at /_/, and an embedded SQLite database (Postgres optionally). Linux and macOS; Windows is served by the Docker image.
You can use it two ways, and the answer changes which docs matter:
| Shape | You write | Read next |
|---|---|---|
| Backend in a box | No Zig. Define collections over REST or in the admin UI, talk to it with an SDK. | api.md, fields.md, your SDK guide |
| Zig framework | A Zig package that embeds ZigBase and adds a comptime schema, hooks, routes, and jobs. | framework.md, testing.md |
Get something running
npx zigbase init # backend in a box (docker-compose + schema + AGENTS.md)
npx zigbase init --framework # a Zig package embedding ZigBase
init never overwrites an existing file — it reports skips and exits 0, so it is safe to run in a directory that already has work in it. It also writes an AGENTS.md full of the traps below; zigbase agents-md writes it for a project that already exists — it never overwrites, so delete the old one first or diff against zigbase agents-md --stdout.
The development commands (init, agents-md, typegen, capabilities, routes, migrate preview, tune, diagnostics) are compiled in by default (-Ddev-tools); a custom-built binary may omit them (-Ddev-tools=false) — the official release, Docker image, and npm packages always have them.
The five things that bite
- Access rules default to LOCKED. A
nullor""rule means superusers only, not public."@public"is the only allow-all value; anything else is a filter expression evaluated per record. Rule parse errors fail closed (500). Every@publicrule is logged as a warning at startup — read them. - Plain-HTTP local dev needs
--insecure-cookies. Auth cookies areSecureby default, so a browser onhttp://127.0.0.1silently refuses to store them and the admin UI just bounces back to the login form. servebinds127.0.0.1. Inside a container that means unreachable —--http-host 0.0.0.0there, and only there.- One error envelope. Every endpoint — built-in and your own typed routes — answers
{"status": 404, "code": "not_found", "message": "…", "data": {}}.codeis a frozen machine-readable string: branch on it, not onmessage(human text, not contract). Per-field validation failures live underdata.<field>.{code,message}.zigbase explain-code CODEresolves a code to its meaning. - The data dir is a credential store. It holds the database, uploads, and
.jwt_secret(generated on first run). Losing it invalidates every issued token. Never commit it; in Docker, mount it.
Shapes you can rely on
- Every list endpoint returns an object, never a bare array:
{"items": [...], "page": 1, "perPage": 30, "totalItems": n, "totalPages": n}. Cursor pagination usescursor/limitand answersnextCursor/hasNext. - A successful side effect with no body is 204.
- URL segments are dash-case.
- Realtime re-applies each collection’s
viewrule per record per subscriber, so a subscriber does not necessarily see every write. GET /api/metais a public, unauthenticated capability probe:capabilities(booleans —oauth2,postgres,collectionsFrozen, …),endpoints, andlimits.maxUploadSizefor the running build.
The CLI
zigbase serve [--http-host H] [--http-port N] [--data-dir PATH] [--insecure-cookies]
[--background] [--ephemeral]
zigbase serve stop|status|wait|logs [--data-dir PATH] # manage a tracked session
zigbase doctor [--production] [--json] [--data-dir PATH]
zigbase migrate [status|preview|rollback N|dump]
zigbase schema dump [--out FILE] [--data-dir PATH]
zigbase schema apply FILE [--dry-run] [--allow-destructive] [--prune]
zigbase openapi [--data-dir PATH] [--out FILE] [--title TEXT] [--api-version VERSION] [--server URL]
zigbase superuser create --email … --password …
zigbase explain-code [CODE] [--json]
zigbase init [--box|--framework] [--dir PATH] [--name NAME]
zigbase agents-md [--box|--framework] [--dir PATH] [--stdout]
zigbase version
zigbase help # and `zigbase <command> --help`
In a detected AI-agent environment, serve backgrounds itself by default — use serve status/serve logs/serve stop to manage that session instead of waiting on a foreground process. zigbase help is the authoritative list — trust it over any document, including this one.
Machine-readable CLI discovery
With -Dfile-inventory=true, the catalog includes separate files-reconcile-preview (read_only) and files-reconcile-apply (may_write) operations. Apply requires explicit operator authorization and stopped writers; never turn a preview candidate or pagination cursor into automatic approval. Both operations support only built-in local storage with SQLite. See offline reconciliation for bounds, lease requirements and irreversible partial-failure semantics.
Run zigbase capabilities --json before choosing an inspection command. It returns one JSON object with protocol_version: 1 and a bounded operations catalog. Each entry has a stable id, an argv array (arguments after the executable, not a shell command), output format, effect, requires_database, and notes. Consumers should reject unsupported protocol versions and ignore unknown fields. The catalog covers selected development operations, not every CLI command or API route.
Discovery itself does not load or validate deployment settings, opens no database, and starts no server. CLI logging initialization still reads logging preferences. It is compiled out with -Ddev-tools=false; invoking it then exits nonzero with rebuild guidance. It does not execute the advertised operations.
Do not treat every diagnostic command as read-only. effect: "may_write" means an operation can create or modify supporting state: doctor probes writability and may initialize a ledger, while schema dump and migration status open the database pool. Use an isolated development database unless those effects are authorized. effect: "read_only" describes the supplied argument vector; adding output-file options can introduce writes. Commands requiring a database may need --data-dir; they also honor their existing environment configuration. Diagnostic/status commands can exit nonzero while emitting valid structured output.
For HTTP registration discovery without a database, use zigbase routes --json. For compiled consumer migration declarations, use zigbase migrate preview --json. It opens no database and invokes no callbacks; pending state, SQL, effects and runtime reversibility remain explicitly unknown. Declared reverse callbacks are not proof of safe rollback. See the preview contract. For request/response schemas and live collection metadata, use the advertised OpenAPI operation. For migration previews and test selection, consult their command documentation. Discovery does not execute operations or remediate issues.
Required inputs and structured diagnostics
zigbase capabilities [--json] emits a single catalog with runnable operations and a separate input_operations array. The diagnostics operation invokes the structured diagnostics verb. There is no protocol-selection flag.
Only operations[].argv is directly runnable. An input operation’s argv_prefix is not a complete invocation: append each supplied inputs[].flag and value as separate arguments, never concatenate shell commands or execute placeholders. The tune descriptor identifies its required JSON file, schema version, 1 MiB limit, scalar types/constraints, candidate bounds and optional nonnegative --as-of timestamp. The captured resources object is intentionally not expanded into a complete schema; its capture_argv obtains the real report. These are bounded descriptors, not JSON Schema. Consult their canonical reference for measurement semantics. Integer minimum_decimal/maximum_decimal bounds are inclusive base-10 strings: parse them losslessly (for example with JavaScript BigInt), not as floating-point numbers. They describe numeric JSON input fields, not string-valued fields; serialize large input integers without rounding. forbidden_byte_ranges contains objects with inclusive numeric minimum and maximum byte values, applied to UTF-8 bytes (0–31 and 127 for labels). Tune retains its existing output/error behavior.
For opt-in database timing evidence, -Dquery-workbench=true exposes bounded, superuser-bearer-only route/shape statistics without retaining SQL or values. SQLite measures prepared step calls; PostgreSQL measures client protocol exchanges including network/server wait and result buffering, not server CPU or pool wait. The separate bounded routes aggregates measure synchronous matched-dispatch elapsed time, including SQL-free handlers and in-scope pool waits, but not transport or detached work. They are not CPU or end-to-end request timings. PostgreSQL buffered rows add no per-row timestamps or telemetry copies. These telemetry budgets do not cap PostgreSQL result memory. Read each item’s backend and measurement label; PostgreSQL plan inspection remains 501. See the query-workbench contract before interpreting repeated shapes as evidence of an N+1 problem.
zigbase diagnostics [--json] [--production] [--data-dir PATH] adapts existing doctor checks into one JSON document (protocol_version: 1, scope: "development-diagnostics"). JSON is the default. A completed run has status: "complete", findings, the unchanged doctor summary, and exit_code: 0 clean, 1 errors, 2 warnings only. Completed does not mean healthy. Existing doctor NDJSON and frozen check identifiers/severities remain unchanged.
Argument, configuration or runtime failures instead have status: "error", exit_code: 1, and failure: {phase, code, subject, expected}. Codes are invalid_arguments, invalid_environment, and diagnostic_failed respectively; nullable subject/expected provide context without including supplied bad values. An option after --data-dir is a missing value; prefix a path beginning with - with ./ to pass it as a directory. Findings’ human messages are not stable identifiers and can include deployment paths or collection names: treat diagnostic reports as deployment information. Help is prose; disabled builds retain the existing nonzero stderr guidance, not a JSON error envelope. Output-device failures and process termination cannot guarantee a complete document. Keep stdout separate from stderr: merging log output into the same stream is not a JSON document contract. Diagnostics and capabilities respect the inherited stdout file offset when redirected; they do not overwrite earlier file content.
The adapter is not offline/read-only: it loads deployment configuration, probes filesystem writability, opens the database and may initialize a migration ledger. Use an authorized isolated development data directory. Report memory scales with the existing doctor’s deployment findings; buffering the document does not impose a global memory cap. The adapter and catalog compile out with -Ddev-tools=false; ordinary doctor remains available.
Focused repository tests
When working in the ZigBase source checkout, use the separate repository tool:
mise exec python@3.13 -- python tools/agent_tests.py inventory
mise exec python@3.13 -- python tools/agent_tests.py run \
--selector tests/admin/test_capabilities.py::test_capabilities_is_offline_and_explicit
These commands emit one JSON document (protocol_version: 1, scope: "repository-focused-tests"). Inventory statically reads an explicit set of pytest modules without importing tests or collecting them. items[].id is an exact module group or directly declared top-level function; a function runs all its parametrizations. Explicit clients/typescript::unit and clients/python::unit suites are also advertised without loading their runners or enumerating cases. Each item includes argv invoking the bounded wrapper and conservative machine-readable requirements (tool versions, Python packages, browser, binary override/build behavior). Class methods and dynamic case IDs are not individually inventoried. This is not a complete repository test inventory, a Zig test filter, or embedded-app discovery. No test executor or extra runtime cost is added to zigbase capabilities or a deployed binary. Inventory’s execution descriptor describes this checkout tool, not an embedded CLI verb.
The explicit allowlist includes five local Python tooling modules: binary resolver, performance-contract validation, parity replay, bounded test execution and changed-file selection. These need no Zig compilation, browser installation or live migration source. Replay tests do open loopback HTTP sockets; changed-file tests require Git; executor tests launch local subprocesses and a nested resolver check through mise. Their advertised effect remains conservative, not a promise of filesystem/network isolation. Performance-contract tests use synthetic artifacts and reports, not actual benchmark runs. Their unittest class runs through the module selector; class-method IDs are still not inventoried.
The TypeScript SDK selector runs the checked-in vitest.config.ts from clients/typescript using pinned Node 24, at most two workers, and the same time/output/process-group limits. Install dependencies explicitly first:
(cd clients/typescript && mise exec node@24 -- npm ci)
mise exec python@3.13 -- python tools/agent_tests.py run \
--selector clients/typescript::unit --timeout-seconds 180
The wrapper never installs dependencies or invokes npm lifecycle scripts. It runs the installed Vitest entrypoint directly; missing dependencies produce unsuccessful structured process evidence, not an installation attempt. Inventory marks this item kind: "suite", runner: "vitest", with a repository-relative cwd override; run reports its cwd (existing pytest items run from "."). The config excludes live integration tests. Type checking, package builds, unlisted SDK suites and individual Vitest case selection remain separate. NODE_OPTIONS is removed from child environments, alongside pytest argument/plugin overrides. Repository config, tests, dependencies and the remaining inherited environment are still trusted code.
The Python SDK selector runs serially from clients/python using pinned Python 3.13 and its checked-in pyproject.toml. Install its dependencies explicitly:
mise exec python@3.13 -- python -m pip install -e 'clients/python[dev,realtime]'
mise exec python@3.13 -- python tools/agent_tests.py run \
--selector clients/python::unit --timeout-seconds 180
Inventory marks this kind: "suite", runner: "pytest", cwd: "clients/python". The fixed command explicitly loads pytest_asyncio.plugin while other plugin autoload stays disabled, so async cases execute rather than silently skipping. It excludes the integration directory from collection and applies not integration; no server binaries are built or required. The runner prepends this checkout’s clients/python/src to child PYTHONPATH (preserving later caller entries) and sets pytest’s pythonpath=src, including for fresh interpreters launched by tests, so another installed SDK checkout is not used accidentally. Missing dependencies fail execution; the runner never installs them. Lint, type checking, packaging, live integration and other SDK suites still require separate validation. Results remain process-level evidence with test_counts: null, not inferred case counts.
mise exec python@3.13 -- python tools/agent_tests.py run \
--selector tests/tools/test_performance_contracts.py
mise exec python@3.13 -- python tools/agent_tests.py run \
--selector tests/tools/test_replay.py::test_subset_matches_extra_keys_but_not_missing_or_different
Run accepts one exact inventory ID, no extra pytest arguments, shell expressions or arbitrary paths. Unknown selectors fail before starting any process. The fixed argv runs the pinned Python toolchain and pytest from the checkout root, with stdin closed, PYTEST_ADDOPTS/PYTEST_PLUGINS removed, plugin autoload disabled, MISE_AUTO_INSTALL=false, and repository server processes pinned to foreground mode. Other environment variables, including explicit ZIGBASE_TEST_BINARY overrides, are inherited. Tests use the repository’s own Playwright fixtures, not auto-loaded plugins.
Run only trusted checkout code with authorized development resources. Tests, conftest, toolchain configuration and dependencies execute with your permissions: they can write files, build binaries, open sockets and access the network. The selector allowlist and shell=False prevent accidental arbitrary argv execution; they are not a sandbox or a security boundary against a modified checkout.
--timeout-seconds defaults to 120 (1–900); --output-limit-bytes defaults to 65,536 (1–1,048,576), shared across child stdout and stderr. Timeout or excess output terminates the child process group. Completion also cleans up ordinary descendants in that group; children deliberately escaping it are not contained. Reaping is bounded to an additional two seconds. These limits do not cap child RSS, files, network traffic, or the JSON envelope’s escaped size. A missing mise executable produces execution_error/spawn_failed; a nonzero toolchain or pytest exit is failed. Toolchain auto-install is disabled for child processes; tests and build scripts can still fetch their own dependencies under the caller’s authority.
Run reports outcome (passed, failed, timed_out, output_limit, execution_error, or cleanup_error), child_exit_code, elapsed milliseconds, captured output, its byte count, and truncation. Reports include cleanup_failed separately; test failures and timeout/output/execution errors retain their primary outcome when cleanup also fails. cleanup_error means cleanup alone failed. test_counts is null: outcomes reflect process exit, not inferred test counts; exit-zero suites may include skips. Use exit code 0 for passed/inventory/selection, 1 for unsuccessful execution, 2 for argument, selector, inventory or Git inspection errors. Help is prose. Captured text is untrusted and may contain deployment details. See Testing for setup and coverage limits. Focused success never replaces the full relevant CI suite.
Changed-file selection
mise exec python@3.13 -- python tools/agent_tests.py affected --base origin/main
affected is selection only: it never runs the selected tests. It resolves one commit-ish locally (no fetch), compares that commit directly to the working tree, and includes non-ignored untracked files. This is not a merge-base/triple-dot comparison: committed branch changes, staged changes, unstaged changes and deletions all count relative to the supplied commit. Renames count both old and new paths. Only net current content is compared: a staged edit canceled by an unstaged reversion to base is not selected. An unchanged checkout produces empty changes and items, not a test pass.
The version-1 envelope adds selection_version: 1, resolved base_commit, comparison, per-path changes with a reason and selected modules, and runnable inventory items. Paths are data, never shell commands. path_bytes_hex preserves the original Git filename bytes portably; path is a display/JSON representation with surrogate escapes for non-UTF-8 bytes. Spaces, tabs, newlines and leading dashes are not separators or options. Results are sorted and deduplicated.
Dependencies are explicit maintained rules, not inferred imports: changing an allowlisted test selects that module; src/, vendored Zig dependencies, build configuration or the admin harness selects all allowlisted admin modules; the shared Python harness or resolver selects all allowlisted pytest modules; toolchain configuration or selector changes select all groups, including the SDK suite. Tool-specific dependencies are narrower:
| Changed dependency | Selected module |
|---|---|
tools/performance_contracts.py or bench/contracts/ | tests/tools/test_performance_contracts.py |
tools/replay/ | tests/tools/test_replay.py |
clients/typescript/ | clients/typescript::unit (unit suite only) |
clients/python/ | clients/python::unit (unit suite only) |
These checks validate the tools; changed benchmark budgets still need actual benchmark runs separately. New tools are never discovered or executed implicitly. The allowlist-expansion regression driver deliberately stays unlisted to prevent recursive self-execution. Every unmapped path (including docs and unlisted tests) selects all allowlisted groups and sets fallback: true. For compatibility, changes[].modules contains both module and suite IDs. coverage_complete is always false and coverage_gaps remains present even for mapped changes. The fallback is not a full-suite run: Zig, other SDK suites, TypeScript/Python integration, typecheck/build/lint, other pytest and docs checks still need separate validation. This helps choose a first check, not decide that CI is unnecessary.
Each of the three Git subprocesses has a 10-second deadline and a 1 MiB combined output cap, using the same process-group cleanup as run. At most 4,096 distinct changed paths and 4,096 bytes per path are accepted. Exceeding limits, invalid revisions or Git/inventory errors fail closed with exit 2 and no partial selection. The allowlisted sources must still exist and be valid Python, including when a change deletes one of them. No automatic broader execution follows an error. Ignored untracked files are omitted; submodule contents are not enumerated. Git’s index flags (such as assume-unchanged/skip-worktree) can hide working-tree edits. Inventory and Git reads are not one atomic snapshot; rerun after concurrent edits. Git inspection inherits trusted checkout/configuration assumptions; this is not a sandbox or a guarantee against malicious repository configuration.
Offline compiled routes
zigbase routes [--json] emits one deterministic JSON object with protocol_version: 1, scope: "compiled-route-registrations", items, reserved_prefixes, coverage, and notes. It shares the discovery command’s offline/no-database behavior and -Ddev-tools gate. routes --help documents its arguments; server/deployment flags are not accepted.
Each item contains an uppercase method, router path (captures use :name), source (builtin, custom, realtime, or feature_state), optional name, and redacted declarative auth metadata. Built-ins come from this binary’s actual gated dispatch table, not a catalog of every feature ZigBase could support. The configured feature-state path is included for GET and HEAD; remapping or disabling it releases the old path. An admin-enabled build reports the /_ reserved prefix, not a fabricated list of admin endpoints.
declared_access: null means unknown, not public. For custom routes the value is the framework’s declared public, authed, or superuser level; authed_collection adds the declared principal-collection constraint, and path_secret describes only the submitted parameter/location/mismatch behavior. Neither configured secret values nor KV/settings secret keys are exported. A public level with path_secret still requires the secret. Built-in access labels are supplied only where the engine already owns an explicit declaration. Handlers, hooks, collection rules and deployment configuration can impose additional checks or return unavailable; this inventory cannot authorize a request.
Entries preserve declaration order within each source. The first matching custom route wins, so an earlier captured pattern can shadow a later literal. The array is not a cross-source dispatch priority list. Static assets/rewrites, individual admin endpoints and runtime authorization are explicitly excluded in coverage. Use the application’s compiled binary, not a stock ZigBase executable, when inspecting its custom routes. Reject unsupported protocol versions and tolerate unknown fields/source kinds when reading the inventory.
Which guide to load
| If you are… | Load |
|---|---|
| turning an idea into an app design | app-genesis.md |
| implementing collections and fields | fields.md, recipes.md |
| calling the API | api.md |
| generating or reviewing an HTTP contract | openapi.md |
| writing Zig hooks, routes, jobs, or a comptime schema | framework.md |
| writing tests | testing.md |
| pairing a Zigapagos frontend with a framework app | zigapagos-pairing.md |
| wiring a frontend | typescript-sdk.md |
| calling from Python / Dart / Kotlin | python-sdk.md, dart-sdk.md, kotlin-sdk.md |
| deploying | deployment.md, docker.md |
| evaluating an app-building agent | agent-evals.md |
| migrating PocketBase 0.39.11 | migrate-pocketbase.md |
| re-platforming a Node.js/Express service | migrate-express.md |
| re-platforming a Laravel application | migrate-laravel.md |
| re-platforming a Go web service | migrate-go.md |
| re-platforming a Rails API-only backend | migrate-rails-api.md |
| replacing a complete Rails application | migrate-rails-fullstack.md |
| doing per-row authorization | abilities.md, tenancy.md |
| adding search | search.md |
| sending mail, or running background work | email.md, jobs-and-webhooks.md |
| hitting something that does not work | known-limitations.md |
Machine-readable indexes: https://valthon.github.io/zigbase/llms.txt and https://valthon.github.io/zigbase/docs-index.json.
Conventions if you are contributing to ZigBase itself
Different job, different rules — those live in the repository’s CLAUDE.md and CONTRIBUTING.md. The short version: changelog entries go in changelog.d/ fragments and never in CHANGELOG.md; published docs under site/src/content/docs/ are generated and must never be hand-edited; and a green zig build test does not imply a green browser suite.