Overview
z3rno-server is a FastAPI application that exposes the Z3rno engine over HTTP. It imports z3rno-core for business logic and adds authentication, rate limiting, body-size limits, structured logging, request-ID propagation, and a Celery worker tier for async tasks (audit drain, lifecycle sweeps, Forge pipelines, refine scheduler).
Current version: z3rno-server 0.20.0. See changelogs/V0-20-CHANGELOG.md for what’s new.
Endpoints
Endpoints fall into four groups: always-on (registered on every deploy), conversation memory (always-on, Phase G), opt-in (registered when an*_ENABLED flag is true), and public (no auth required).
Always-on
| Method | Path | Purpose |
|---|---|---|
POST | /v1/memories | Store a memory |
POST | /v1/memories/recall | Semantic search (defaults to AUTO strategy router) |
POST | /v1/memories/recall/stream | Server-sent-events streaming recall (Phase G slice 5) |
POST | /v1/memories/forget | Soft-delete one or many memories |
POST | /v1/memories/batch | Batch store |
GET | /v1/memories/{memory_id} | Fetch one memory |
GET | /v1/memories/{memory_id}/history | SCD-2 temporal version history |
PATCH | /v1/memories/{memory_id} | Update content/metadata/importance |
GET | /v1/audit | Hash-chained audit log (paginated) |
GET | /v1/usage | Token / embedding / LLM-call counters (today + month-to-date) |
GET | /v1/tenants/me/budgets | Resolved budgets + raw per-tenant overrides (v0.20.3) |
PUT | /v1/tenants/me/budgets | Set per-tenant budget overrides (v0.20.3) |
GET | /v1/tenants/{org_id}/budgets | [superadmin] Read another tenant’s budgets (v0.22.1, opt-in) |
PUT | /v1/tenants/{org_id}/budgets | [superadmin] Replace another tenant’s budgets (v0.22.1, opt-in) |
POST | /v1/sessions | Start a session |
POST | /v1/sessions/{session_id}/end | End a session |
GET | /v1/sessions/{session_id} | Get session state |
POST | /v1/api-keys | Create an API key |
GET | /v1/api-keys | List the caller’s API keys |
DELETE | /v1/api-keys/{key_id} | Revoke a key |
GET | /v1/limits | Report the caller’s rate-limit posture |
GET | /v1/graph/data | Memo subgraph (nodes + edges) for the /graph viewer |
Conversation memory (Phase G slice 2 — always-on)
| Method | Path | Purpose |
|---|---|---|
POST | /v1/conversations | Open a conversation |
GET | /v1/conversations/{conversation_id} | Conversation metadata |
POST | /v1/conversations/{conversation_id}/turns | Append a turn (returns turn_index + needs_summary) |
GET | /v1/conversations/{conversation_id}/turns | List turns (paginated by after_turn + limit) |
DELETE | /v1/conversations/{conversation_id} | Soft-delete the conversation; turn Memos stay queryable |
Opt-in
Each block registers only when its gating flag is true. With the flag off, the routes are absent from the OpenAPI surface and the corresponding worker tasks self-reject.| Method | Path | Gate | Purpose |
|---|---|---|---|
POST | /v1/distill | DISTILL_ENABLED=true | Enqueue a Forge distillation job |
GET | /v1/distill/{job_id} | DISTILL_ENABLED=true | Poll job state |
POST | /v1/ingest | INGEST_ENABLED=true | Ingest text or URL |
POST | /v1/ingest/file | INGEST_ENABLED=true | Multipart file upload (PDF / DOCX / CSV / MD / code / text + image/audio when MULTIMODAL_ENABLED=true) |
POST | /v1/ingest/upload-url | INGEST_ENABLED=true + STORAGE_BACKEND=s3 | Presigned URL for direct-to-S3 upload |
POST | /v1/ingest/finalize/{job_id} | INGEST_ENABLED=true | Finalize a direct-upload ingest |
GET | /v1/ingest/loaders | INGEST_ENABLED=true | List registered loaders + MIME types |
GET | /v1/ingest/{job_id} | INGEST_ENABLED=true | Poll ingest job state |
POST | /v1/ingest/search | INGEST_ENABLED=true + TAVILY_API_KEY set | Tavily-driven web discovery → fan-out ingest |
GET | /v1/ingest/search/{batch_id} | INGEST_ENABLED=true + TAVILY_API_KEY set | Search batch status |
POST | /v1/datasets | INGEST_ENABLED=true | Create a dataset |
GET | /v1/datasets | INGEST_ENABLED=true | List datasets (paginated) |
GET | /v1/datasets/{dataset_id} | INGEST_ENABLED=true | Fetch one |
DELETE | /v1/datasets/{dataset_id} | INGEST_ENABLED=true | Soft-delete + detach memories |
POST | /v1/feedback | REFINE_ENABLED=true | Record -1/0/+1 signal on a memory or AGE edge |
POST | /v1/refine | REFINE_ENABLED=true | Enqueue a refine run |
GET | /v1/refine/{job_id} | REFINE_ENABLED=true | Poll refine job state |
GET | /v1/forget/{cert_id} | FORGET_PROOF_ENABLED=true | Fetch a Merkle-rooted, ed25519-signed forget certificate |
Public (no auth)
| Method | Path | Purpose |
|---|---|---|
GET | /v1/health | Liveness probe |
GET | /v1/ready | Readiness probe (DB connected, migrations applied) |
GET | /v1/worker/health | Celery worker pulse |
GET | /metrics | Prometheus metrics |
GET | /docs, /redoc, /openapi.json | OpenAPI surface |
Middleware chain
Requests pass through these layers, outermost first:| Layer | Notes |
|---|---|
| CORS | Origins controlled by Z3RNO_CORS_ORIGINS (comma-separated). |
| RequestId | Generates / preserves X-Request-Id for tracing. |
| Logging | Structured log line per request via structlog; includes request id, latency, status. |
| BodyLimit | Caps request body size; whitelists multipart/form-data for /v1/ingest/file (which enforces its own INGEST_MAX_FILE_BYTES cap). |
| Auth | Validates Authorization: Bearer <key> (or X-API-Key), resolves the tenant, sets app.current_org_id for PostgreSQL RLS. Public paths bypass. |
| RateLimit | Token-bucket per API key, backed by Valkey. Gated on RATE_LIMIT_ENABLED. |
| Route Handler | The endpoint logic. |
Authentication
API keys are passed via either header:z3rno_sk_* prefix. The dev default seeded on a fresh local server is z3rno_sk_test_localdev — change this for any non-localhost deploy. Production keys are generated via POST /v1/api-keys (returns the plaintext key once; only its hash is stored).
Rate limiting
Token bucket per API key, evaluated in theRateLimit middleware. Rate-limit response headers are returned on every authed call:
RATE_LIMIT_REQUESTS=100 per RATE_LIMIT_WINDOW=60 seconds with RATE_LIMIT_BURST=20. Self-hosters tune these per deploy; managed-cloud tiering lives ahead of v0.21.
Celery workers
Background tasks ride on Celery with Valkey as broker + result backend. Twelve shipped tasks:| Task | Trigger | Notes |
|---|---|---|
z3rno.audit_drain | Beat (every AUDIT_DRAIN_INTERVAL_SECONDS, default 1s) or NOTIFY/LISTEN wake (v0.20.2) | Drains audit_log_pending into audit_log with per-org advisory locks. |
z3rno.sweep_expired_memories | Beat (every 1 min) | Soft-delete memories past their TTL. |
z3rno.decay_importance | Beat (every 1 hr) | EMA decay on importance for unused memories. |
z3rno.enforce_retention_caps | Beat (every 6 hr) | Prune over-cap memories per agent. |
z3rno.ensure_audit_partitions | Beat (daily) | Pre-creates next-month audit log partitions. |
z3rno.refine_scheduler_tick | Beat (when REFINE_BEAT_INTERVAL_SECONDS > 0) | Picks opted-in tenants oldest-first → enqueues z3rno.refine_run. |
z3rno.refine_run | On-demand (API or scheduler) | One refine pipeline pass for a tenant. |
z3rno.ingest_watchdog | Beat (every 5 min) | Resurrects stuck ingest_jobs rows. |
z3rno.forge_distill | On-demand via POST /v1/distill | The Forge pipeline. Self-rejects if DISTILL_ENABLED=false. |
z3rno.ingest_run | On-demand via POST /v1/ingest* | Bridges to IngestPipeline.run(). Self-rejects if INGEST_ENABLED=false. |
z3rno.generate_embedding | On-demand | Embedding backfill. |
z3rno.worker_ping | On-demand (health check) | Drives GET /v1/worker/health. |
Z3RNO_AUDIT_LISTEN_ENABLED=true (v0.20.2), a dedicated z3rno-audit-listener console-script pod opens a Postgres LISTEN z3rno_audit_pending and fires z3rno.audit_drain on every write. Drains wake in ~50 ms instead of waiting for the next beat tick; the beat-driven poll stays as a fallback at a longer interval (60 s recommended).
Configuration
Required env vars:recall quality):
DISTILL_ENABLED, INGEST_ENABLED, REFINE_ENABLED, MEMORY_TIER_AUTO_ROUTE, RETRIEVAL_REDACTION_ENABLED, FORGET_PROOF_ENABLED, OTEL_ENABLED, DATABASE_READ_URL, Z3RNO_AUDIT_LISTEN_ENABLED, the USAGE_BUDGET_* family, multimodal, S3, Tavily, Playwright, codegraph, ontology, distributed-backends, etc.) all live alongside on the Self-hosting / Configuration page.
Running locally
Admin surface (v0.22.1, opt-in)
Cross-tenant management endpoints for managed-hosting providers. Sits alongside the regular tenant surface — not above it. Tenants still self-manage their own budgets via/v1/tenants/me/budgets;
this surface lets a hosting operator set budgets on behalf of a
tenant without holding that tenant’s auth.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/tenants/{org_id}/budgets | Read another tenant’s overrides + effective caps |
PUT | /v1/tenants/{org_id}/budgets | Replace another tenant’s budget overrides |
Enabling the surface
Both env vars must be set — an empty key disables the surface even when the flag istrue:
role="superadmin". This is the correct default for every deploy
that isn’t a managed-hosting control plane.
Authentication model
Authentication is the env-keyedSUPERADMIN_API_KEY directly — there
is no DB-stored superadmin role, no rotation API, no per-key auditing
yet. Any caller presenting that key in Authorization: Bearer ...
gets role="superadmin" attached to the request and is not
tenant-bound (org_id stays None). Handlers SET LOCAL app.current_org_id to the URL-path org_id so the underlying
SQL — same shape as /me/budgets — runs against the target tenant
under RLS.
Treat the key like a root password: deploy-time configured, kept out
of git, rotated by config push. A dedicated rotation playbook is on
the roadmap for v0.23+ (see Managed-hosting guide).
RBAC posture
The strictrequire_superadmin() dependency guards these routes —
it rejects role=None (the backward-compat path that the regular
require_role lets through for API-key callers). A misconfigured
client presenting a normal tenant key gets a 403, not silent
cross-tenant access.
| Status | When |
|---|---|
401 | No bearer presented |
403 | Bearer presented but isn’t the superadmin key |
404 | Target org_id doesn’t exist |
422 | Body fails Pydantic validation (e.g. negative budget) |
Using it from the SDKs
Both Python and TypeScript SDKs expose aclient.admin sub-namespace
since v0.9.0:
Error responses
Errors follow RFC 7807 (Problem Details):Reference
- Source: github.com/the-ai-project-co/z3rno-server
- Image:
ghcr.io/safayavatsal/z3rno-server:0.20.0(public mirror) — see Components → z3rno-helm for the chart that deploys it - Engine: Components → z3rno-core — the library this server imports
- Verbs: Concepts → The Z3rno Verbs — canonical seven-verb table
- Config knobs: Self-hosting → Configuration — every env var