Getting Started with SapixDB
No prior database experience required. This manual walks you through everything from installation to writing your first intelligent, self-auditing record.
- A computer running macOS, Linux, or Windows
- Docker Desktop installed (free at docker.com)
- A terminal / command prompt
- Basic familiarity with JSON (key-value pairs like
{"name": "Alice"})
1. What is SapixDB?
Think of SapixDB as a database that never forgets, never lies, and never needs you to restructure it. Every piece of data you store is:
- Signed — cryptographically stamped so you always know who created it
- Linked — chained to the previous record, creating an unbreakable history
- Permanent — nothing is ever deleted or overwritten; old versions stay queryable
- Schema-free — add new fields any time; no ALTER TABLE, no migration scripts
The analogy SapixDB uses is biology. Your data is stored as nucleotides (individual records) strung together into a strand(a chain of records for one entity). Just like DNA, the chain can be read from any point in history and cannot be tampered with.
2. Installation
SapixDB runs as a Docker container. You do not need to install anything special — Docker handles all the dependencies.
sapixdb/agent:latest (or sapixdb/agent:enterprise) for the full feature set — Mutants, NL queries, HIPAA/SOX compliance, distributed mode, and more. Use sapixdb/agent:community for the open-source edition (Apache 2.0), which includes strands, SaQL, graph, blobs, crons, triggers, indexes, mesh, and the control-plane dashboard. The docker-compose.yml in Step 3 uses :latest — the enterprise image — and works with no changes.# macOS / Linux python3 -c "import secrets; print(secrets.token_hex(32))" # or with openssl openssl rand -hex 32
a3f8c201b7e94d0f2c6a1b3e5d7f9a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2
docker-compose.yml next. If you lose it your agent loses its cryptographic identity.docker-compose.yml inside it. Replace YOUR_SEED_HERE with the hex string you generated above:mkdir my-sapixdb && cd my-sapixdb mkdir -p data/strand data/graph data/blobs
services:
sapixdb:
image: sapixdb/agent:latest
container_name: sapixdb
restart: unless-stopped
ports:
- "7475:7475"
volumes:
- ./data/strand:/data/strand
- ./data/graph:/data/graph
- ./data/blobs:/data/blobs
environment:
SAPIX_AGENT_ID: my-first-agent
SAPIX_KEYPAIR_SEED_HEX: YOUR_SEED_HERE
SAPIX_STRAND_DIR: /data/strand
SAPIX_GRAPH_DIR: /data/graph
SAPIX_BLOB_DIR: /data/blobs
SAPIX_BIND_ADDR: 0.0.0.0:7475
# Enterprise features — omit for community mode, or add your license key:
# SAPIX_LICENSE_KEY: "your-license-key-here"
# Codios agent authorization — get API key from codios.midlantics.com → Settings → API Keys
# SAPIX_CODIOS_URL: "https://codios-api.midlantics.com"
# SAPIX_CODIOS_API_KEY: "codios_sk_..."
# A2A audit trail — get API key from a2a.midlantics.com → Settings → API Keys
# SAPIX_A2A_URL: "https://a2a-api.midlantics.com"
# SAPIX_A2A_API_KEY: "a2a_sk_..."./data/ subdirectories (bind mounts) means your data is visible on disk in your project folder and owned by you — no permission issues.Without a
SAPIX_LICENSE_KEY, the agent runs in community mode (core features). Add a valid license key to unlock enterprise features — see Section 2b (Licensing) below.docker compose up -d
curl http://localhost:7475/v1/health
{
"status": "ok",
"agent_id": "my-first-agent",
"record_count": 0,
"chain_head": "0000...0000"
}2b. Licensing — Community vs Enterprise
SapixDB uses a single Docker image for everyone. What features are available depends on whether you have a license key. No license key = community mode. Valid license key = full enterprise features, unlocked at startup.
How to get a license key
Activating your license key
Once you have a license key, add one line to your docker-compose.yml:
environment:
SAPIX_AGENT_ID: my-agent
SAPIX_KEYPAIR_SEED_HEX: YOUR_SEED_HERE
# ... other vars ...
SAPIX_LICENSE_KEY: "eyJvcmdfaWQiOiJhY21lLWNvcnAiLCJwbGFuIjoiZW50ZXJwcmlzZSJ9..."Then restart the container:
docker compose down && docker compose up -d
The agent logs its license status at startup. Check with:
docker logs sapixdb | grep -i license
INFO sapix_agent::license: Enterprise license verified org_id=acme-corp plan=enterprise permanent=true
INFO sapix_agent::license: No SAPIX_LICENSE_KEY set — running in community mode
2c. Codios — Agent Authorization
Codios is the agent contract security layer from Midlantics. When configured, SapixDB calls Codios before every write and read — Codios decides whether the operation is permitted. Without Codios, SapixDB runs in open mode: all operations are allowed and a warning is logged at startup.
Get your Codios API key
- Go to codios.midlantics.com
- Navigate to Settings → API Keys
- Click Generate new key — copy the
codios_sk_...value immediately (shown once)
Add to docker-compose.yml
environment: SAPIX_CODIOS_URL: "https://codios-api.midlantics.com" SAPIX_CODIOS_API_KEY: "codios_sk_..." # from your Codios dashboard # Optional: fail-closed mode — agent refuses to start without Codios # SAPIX_REQUIRE_CODIOS: "true"
What Codios authorizes on every operation:
POST https://codios-api.midlantics.com/authorize
Authorization: Bearer codios_sk_...
{
"agent_id": "orders",
"operation": "write",
"resource": "agents/orders/records",
"caller_identity": "orders",
"request_id": "req_a1b2c3d4...",
"timestamp": 1748304000000,
"ttl_ms": 5000
}Codios returns 200 (optionally with { "contract_id": "ctr_..." }) to allow, or 403 to deny. The contract_id is stamped on the write record for audit trail purposes.
Monitor Codios activity
curl http://localhost:7475/v1/codios/stats
{
"mode": "enforced",
"endpoint_configured": true,
"authorized": 1482,
"denied": 3,
"errors": 0
}SAPIX_CODIOS_URL is not set, SapixDB runs in open mode — all operations are permitted. Set SAPIX_REQUIRE_CODIOS=true in production to guarantee Codios is always present.2d. A2A — Audit Trail & Observability
A2A is the Midlantics observability layer. When configured, SapixDB emits a structured audit event to A2A after every write — giving you a live feed of all activity across all agents, with timing, status, and lineage in the A2A dashboard.
Get your A2A API key
- Go to a2a.midlantics.com
- Navigate to Settings → API Keys
- Click Generate new key — copy the
a2a_sk_...value immediately (shown once)
Add to docker-compose.yml
environment: SAPIX_A2A_URL: "https://a2a-api.midlantics.com" SAPIX_A2A_API_KEY: "a2a_sk_..." # from your A2A dashboard
What gets emitted — one event per write, fire-and-forget (A2A failure never delays or rolls back the write):
POST https://a2a-api.midlantics.com/events
Authorization: Bearer a2a_sk_...
{
"agent_id": "orders",
"event_type": "write",
"record_id": "01918d3e-7f3c-7a99-b2c8-e3a1f9d20b00",
"timestamp_ms": 1748304005123
}Event types: genesis, write, blob_write, graph_write, mutation_applied, codios_denied. View them all in the A2A dashboard under your workspace.
SAPIX_A2A_URL is not set, events are silently dropped. Safe to omit for local development.3. First Steps — Bootstrap the Strand
Before writing any data, initialize your strand with a genesis record. This is the root of the chain — every subsequent write links back to it. Run this once after starting for the first time:
curl -X POST http://localhost:7475/v1/genesis -H "Content-Type: application/json" -d '{"zone":"governed"}'{
"agent_id": "my-first-agent",
"public_key_hex": "a3f8c2...",
"genesis_record_id": "01960000-0000-7000-0000-000000000001",
"chain_head_hex": "3a7f2c...",
"zone": "governed"
}All endpoints in SapixDB are at http://localhost:7475/v1/. Named agents (created with POST /v1/agents) automatically get their genesis block — no separate genesis call needed. Your data structure lives inside the JSON payloads you write.
4. Writing Data
Use POST /v1/records/json to write any JSON object to the strand. SapixDB handles the encoding internally — no serialization needed from your side.
Write a record
curl -X POST http://localhost:7475/v1/records/json -H "Content-Type: application/json" -d '{"data":{"type":"user","name":"Alice","email":"[email protected]","role":"admin"}}'{
"record_id": "01960000000000000000000000000002",
"content_hash": "b4e91a3f...",
"chain_head": "b4e91a3f..."
}Save the content_hash — it is the permanent address of this record. You can retrieve it directly at any time using GET /v1/records/<hash>.
Write an update — the old version is preserved
curl -X POST http://localhost:7475/v1/records/json -H "Content-Type: application/json" -d '{"data":{"type":"user","name":"Alice","email":"[email protected]","role":"superadmin"}}'SapixDB never overwrites. Both versions exist on the strand forever. Alice was admin first, then superadmin — and you can prove it.
"_deleted": true field. Your application checks that field; the full history is always preserved for audit.5. Reading Data
Read everything in the strand
This is the most common query — get all records from genesis to the latest write, paginated. This is exactly what the Strand Browser in the Control Plane shows.
curl "http://localhost:7475/v1/strand/records?offset=0&limit=50"
{
"agent_id": "my-first-agent",
"total": 3,
"offset": 0,
"records": [
{
"record_id": "01960000000000000000000000000001",
"content_hash": "3a7f2c...",
"parent_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"timestamp_hlc": 1747000000000,
"payload_b64": "..."
},
{
"record_id": "01960000000000000000000000000002",
"content_hash": "b4e91a3f...",
"parent_hash": "3a7f2c...",
"timestamp_hlc": 1747000001000,
"payload_b64": "..."
}
]
}import base64, msgpack; msgpack.unpackb(base64.b64decode(payload_b64), raw=False)Get the N most recent records
curl -X POST http://localhost:7475/v1/query -H "Content-Type: application/json" -d '{"type":"latest","limit":10}'Read a specific record by its hash
curl http://localhost:7475/v1/records/b4e91a3f...
Check strand head and record count
curl http://localhost:7475/v1/strand/head
{
"chain_head": "b4e91a3f...",
"record_count": 3
}Read a record decoded (human-readable)
Every record's payload field is already decoded JSON — no extra tooling needed. The timestamp_ms field is wall-clock milliseconds so you can compare it to regular Unix timestamps.
curl http://localhost:7475/v1/records/<content_hash>
{
"record_id": "019e27a9-...",
"content_hash": "b4e91a3f...",
"timestamp_hlc": 116574249022717952,
"timestamp_ms": 1778781875957,
"payload": {
"type": "user",
"name": "Alice",
"email": "[email protected]",
"role": "admin"
},
"payload_b64": "...",
"flags": 0
}Query by time range
SapixDB uses HLC timestamps (Hybrid Logical Clock) — not Unix milliseconds. HLC packs wall-clock ms into bits 63–16 and a logical counter into bits 15–0. Use the timestamp_hlc values returned by previous queries as your range bounds. To get all records, use 0 and a large number:
curl -X POST http://localhost:7475/v1/query -H "Content-Type: application/json" -d '{"type":"time_range","from_ts":0,"to_ts":18446744073709551615}'curl -X POST http://localhost:7475/v1/query -H "Content-Type: application/json" -d '{"type":"time_range","from_ts":116574249022717952,"to_ts":116574350000000000}'Verify the entire chain is intact
curl http://localhost:7475/v1/strand/verify
{
"chain_intact": true,
"total_records": 3,
"content_hashes_valid": 3,
"parent_hashes_valid": 3,
"signatures_valid": 3
}6. Time Travel — Query the Past
Because SapixDB never overwrites data, you can ask "what did the strand look like at a specific moment?" Pass an HLC timestamp and get back every record that existed at or before that point.
curl "http://localhost:7475/v1/strand/as-of?timestamp_hlc=1747000001500&limit=50"
{
"agent_id": "my-first-agent",
"as_of_hlc": 1747000001500,
"total_at_time": 2,
"records": [
{ "record_id": "...", "content_hash": "3a7f2c...", "timestamp_hlc": 1747000000000 },
{ "record_id": "...", "content_hash": "b4e91a3f...", "timestamp_hlc": 1747000001000 }
]
}The third record (Alice's superadmin update, written at timestamp 1747000002000) is invisible here — it didn't exist at 1747000001500 yet.
7. Agents & Data Ownership
In SapixDB, every running container is one agent — a named identity with its own strand. The name you set in SAPIX_AGENT_IDis that agent's permanent identity. Every record it writes is signed with its Ed25519 keypair.
- Run one agent per microservice, domain, or AI process
- Each agent has its own isolated strand — no shared tables
- AI agents (LLMs, pipelines) can write directly to SapixDB via the same API
- You always know which agent signed which record
Log an AI decision permanently
curl -X POST http://localhost:7475/v1/records/json -H "Content-Type: application/json" -d '{"data":{"_type":"ai_decision","model":"gpt-4o","action":"approved_loan","reason":"Credit score 780, DTI 28%","confidence":0.94}}'Every AI decision is permanently logged, signed, and auditable. You can always prove what the model decided, when, and why.
8. Multiple Agents — One Port, Many Tables
In SapixDB an agent is a table. Just as a traditional database serves all its tables through one connection string and one port, SapixDB serves all its agents through one port (7475). You never need a separate port per agent — that would be like running a separate Postgres process per table.
Each agent owns a cryptographically isolated strand with its own Ed25519 keypair and hash-linked record chain. The data is separated; the network entry point is shared. Think of agents as named, intelligent, self-auditing tables.
Step 1 — Create your named agents
Use POST /v1/agents to register a named agent. Each agent gets its own strand (hash-linked chain) and Ed25519 keypair. Generate a unique seed for each one.
# Generate seeds (run once, save both outputs)
python3 -c "import secrets; print(secrets.token_hex(32))"
# → a3f8c201b7e94d0f2c6a1b3e5d7f9a0c... (use as seed for users-agent)
python3 -c "import secrets; print(secrets.token_hex(32))"
# → 7b2e4f91c3d85a06e1b2c4d6f8a0b3e5... (use as seed for orders-agent)
# Create the users agent
curl -X POST http://localhost:7475/v1/agents \
-H "Content-Type: application/json" \
-d '{
"agent_id": "users",
"seed_hex": "a3f8c201b7e94d0f2c6a1b3e5d7f9a0c...",
"zone": "governed"
}'
# Create the orders agent
curl -X POST http://localhost:7475/v1/agents \
-H "Content-Type: application/json" \
-d '{
"agent_id": "orders",
"seed_hex": "7b2e4f91c3d85a06e1b2c4d6f8a0b3e5...",
"zone": "governed"
}'Step 2 — Write to each agent by name
# Write to users agent
curl -X POST http://localhost:7475/v1/agents/users/records/json \
-H "Content-Type: application/json" \
-d '{"data":{"name":"Alice","role":"admin","email":"[email protected]"}}'
# Write to orders agent — same port, completely isolated strand
curl -X POST http://localhost:7475/v1/agents/orders/records/json \
-H "Content-Type: application/json" \
-d '{"data":{"item":"Widget","qty":3,"user":"Alice","status":"pending"}}'Step 3 — Query each agent independently
# Latest 10 records from the users agent
curl -X POST http://localhost:7475/v1/agents/users/query \
-H "Content-Type: application/json" \
-d '{"type":"latest","limit":10}'
# Latest 10 records from the orders agent
curl -X POST http://localhost:7475/v1/agents/orders/query \
-H "Content-Type: application/json" \
-d '{"type":"latest","limit":10}'
# Get the orders agent chain head
curl http://localhost:7475/v1/agents/orders/strand/head
# List all agents on this server
curl http://localhost:7475/v1/agents7475. Each has its own cryptographically isolated strand and keypair. The URL path (/v1/agents/orders/...) routes to the right agent automatically.Pre-load agents at startup with SAPIX_AGENTS
Instead of calling POST /v1/agents after every restart, define your agents in the environment. The server loads them automatically on boot.
services:
sapixdb:
image: sapixdb/agent:latest
ports: ["7475:7475"]
volumes:
- ./data/strand:/data/strand
- ./data/graph:/data/graph
- ./data/blobs:/data/blobs
environment:
SAPIX_AGENT_ID: primary
SAPIX_KEYPAIR_SEED_HEX: <primary-seed>
SAPIX_AGENTS: "users:<users-seed>,orders:<orders-seed>,payments:<payments-seed>"
SAPIX_STRAND_DIR: /data/strand
SAPIX_GRAPH_DIR: /data/graph
SAPIX_BLOB_DIR: /data/blobs
SAPIX_BIND_ADDR: 0.0.0.0:7475Each agent in SAPIX_AGENTS is loaded from /data/strand/agents/<id>/ — a subdirectory per agent, all inside one container, one port.
9. Graph Relationships
SapixDB has a built-in graph layer. Edges connect agent IDs (agent-level relationships like "orders depends on users") and record refs connect specific content hashes across agents (record-level cross-links).
Create an agent-level edge
curl -X POST http://localhost:7475/v1/graph/edges \
-H "Content-Type: application/json" \
-d '{"src":"users","edge_type":"has_orders","dst":"orders","weight":1.0}'Create a cross-agent record reference
A record ref links a specific record in one agent to a specific record in another — the fine-grained join between two nucleotides across strands.
curl -X POST http://localhost:7475/v1/graph/refs \
-H "Content-Type: application/json" \
-d '{
"src_agent": "users",
"src_content_hash": "<alice-content-hash>",
"edge_type": "placed_order",
"dst_agent": "orders",
"dst_content_hash": "<order-content-hash>"
}'Traverse the agent graph
Graph traversal walks agent-to-agent edges. Pass agent_id as the starting point — not a record hash. Use depth to control how many hops to follow (max 3). Filter by edge type with edge_type.
curl "http://localhost:7475/v1/graph/traverse?agent_id=users&depth=2"
{
"start_agent": "users",
"agents_visited": ["users", "orders"],
"edges": [
{ "src": "users", "dst": "orders", "edge_type": "has_orders", "weight": 1.0, "across_peer": false }
],
"depth_reached": 1
}Get all outbound edges for an agent
curl "http://localhost:7475/v1/graph/edges/users" # Inbound: curl "http://localhost:7475/v1/graph/edges/orders?direction=in"
Delete an edge
curl -X DELETE "http://localhost:7475/v1/graph/edges/users/has_orders/orders"
10. Multi-Agent Queries — Joining Tables
Each agent (table) is queried independently via its own endpoint. Cross-agent joins are done client-side by fetching results from each agent and merging on a shared field — the same pattern you'd use with any microservice database.
The graph layer lets you follow relationships between agents without a full table scan. Record refs (POST /v1/graph/refs) give you precise foreign-key-style links between records in different agents.
# 1. Get all users
curl -s -X POST http://localhost:7475/v1/agents/users/query \
-H "Content-Type: application/json" \
-d '{"type":"latest","limit":100}'
# 2. Get all orders
curl -s -X POST http://localhost:7475/v1/agents/orders/query \
-H "Content-Type: application/json" \
-d '{"type":"latest","limit":100}'
# 3. Traverse the graph to find all agents reachable from users
curl "http://localhost:7475/v1/graph/traverse?agent_id=users&depth=2"
# 4. Follow a specific cross-agent record ref (Alice → her order)
curl "http://localhost:7475/v1/graph/refs/users/<alice-hash>"POST /v1/graph/refs, then look it up with GET /v1/graph/refs/<agent>/<hash>. The link is content-hash-addressed — it tracks exactly which version of a record was linked, immutably.11. Saga Patterns — Distributed Transactions
A saga is a multi-step write transaction that spans multiple agents (or multiple SapixDB nodes). If any step fails, SapixDB automatically compensates — writing a tombstone record against every step that already committed, maintaining a complete audit trail of what happened and what was rolled back.
Unlike SQL transactions there is no lock. Each step writes independently. Compensation is a new append-only record (never a delete), so the compensation itself is auditable and cryptographically signed.
Create and execute a saga
Each step specifies a target_agent_url (the SapixDB instance to write to) and a payload_b64 (base64-encoded bytes to write). The saga executes all steps sequentially and returns the final state.
# Encode your payloads
PAYMENT=$(python3 -c "import base64,json; print(base64.b64encode(json.dumps({'event':'payment','order_id':'o1','amount':99.99}).encode()).decode())")
INVENTORY=$(python3 -c "import base64,json; print(base64.b64encode(json.dumps({'event':'deduct','item':'Widget','qty':1}).encode()).decode())")
curl -X POST http://localhost:7475/v1/saga \
-H "Content-Type: application/json" \
-d "{
\"steps\": [
{\"target_agent_url\": \"http://payments-svc:7475\", \"payload_b64\": \"$PAYMENT\"},
{\"target_agent_url\": \"http://inventory-svc:7475\", \"payload_b64\": \"$INVENTORY\"}
]
}"{
"id": "019e28550000...",
"coordinator_agent_id": "primary",
"state": "committed",
"steps": [
{
"step_id": "step_1",
"status": "applied",
"record_hash": "aaaa29cb...",
"compensation_hash": null,
"error": null
},
{
"step_id": "step_2",
"status": "applied",
"record_hash": "b4e91a3f...",
"compensation_hash": null,
"error": null
}
],
"created_at_ms": 1778793136028,
"completed_at_ms": 1778793136035
}What compensation looks like
If step 2 fails, step 1 receives a TOMBSTONE compensation write. The saga state becomes compensated — not deleted, not lost.
{
"state": "compensated",
"steps": [
{
"step_id": "step_1",
"status": "compensated",
"record_hash": "aaaa29cb...",
"compensation_hash": "3b79b8af...",
"error": null
},
{
"step_id": "step_2",
"status": "failed",
"record_hash": null,
"compensation_hash": null,
"error": "error sending request for url (http://payments-svc:7475/v1/records)"
}
]
}List and inspect sagas
# All sagas (newest first) curl http://localhost:7475/v1/saga # One saga by ID curl http://localhost:7475/v1/saga/<saga-id>
12. Mutants — Agent Governance
A Mutantis a governance proposal to change an agent's structure or behavior. Every structural change — bumping a schema version, changing a trust zone, adding or removing a field, patching behavior — goes through a proposal → approve → apply lifecycle that is fully audited on-chain.
Approval requirements depend on the target agent's zone:
Propose a mutation
curl -X POST http://localhost:7475/v1/mutations \
-H "Content-Type: application/json" \
-d '{
"target_agent_id": "orders",
"proposer_agent_id": "primary",
"kind": { "type": "SchemaVersion", "new_version": 2 }
}'{
"proposal_id": "019e2870911778e5...",
"status": "pending",
"policy": "TwoOfN"
}Approve and apply
# First approval
curl -X POST http://localhost:7475/v1/mutations/<proposal-id>/approve \
-H "Content-Type: application/json" \
-d '{"approver_agent_id": "admin-alice"}'
# Second approval — status flips to "approved"
curl -X POST http://localhost:7475/v1/mutations/<proposal-id>/approve \
-H "Content-Type: application/json" \
-d '{"approver_agent_id": "admin-bob"}'
# Apply the mutation
curl -X POST http://localhost:7475/v1/mutations/<proposal-id>/apply{
"proposal_id": "019e2870...",
"applied": true,
"target_agent_id": "orders",
"mutation": "schema_version→2"
}Other mutation kinds
# Bump schema version
{ "type": "SchemaVersion", "new_version": 3 }
# Move agent to a different trust zone
{ "type": "ZoneChange", "new_zone": 2 } // 0=immutable_core 1=protected 2=governed 3=free
# Propose adding a field (audit only — no storage-level enforcement in v1)
{ "type": "FieldAdd", "field_name": "phone", "field_type": "string" }
# Propose removing a field (audit only)
{ "type": "FieldRemove", "field_name": "legacy_id" }
# Propose a behavior change (audit only — for AI agent governance)
{ "type": "BehaviorPatch", "description": "rate-limit writes to 100/sec", "patch_b64": "..." }Reject and reinitialize
# Reject a pending proposal
curl -X POST http://localhost:7475/v1/mutations/<id>/reject \
-H "Content-Type: application/json" \
-d '{"rejector_agent_id": "admin-charlie"}'
# Re-open a rejected proposal (creates a fresh proposal with same params)
curl -X POST http://localhost:7475/v1/mutations/<id>/reinit
# List all proposals
curl http://localhost:7475/v1/mutations13. Agent Caging
Cagingmarks an agent as restricted. It sets a persistent flag in the agent's metadata store, broadcasts an agent_caged event over A2A to connected systems, and surfaces the caged: true field in GET /v1/status. The flag survives process restarts.
What caging does right now
- Sets
caged: truein the agent's metadata store (persisted to disk). - Emits an
agent_cagedA2A event — any connected orchestrator (Codios, custom agent) receives it and can react immediately. - The status endpoint returns
caged: trueso Control Plane dashboards, alerting, and monitoring tools can display it. - Write enforcement is the next step — blocked when Codios integration ships.
When to use it
- Incident response: An AI agent is producing anomalous records. Cage it immediately from the Control Plane while you investigate — the A2A event tells the orchestrator to stop dispatching new tasks to it.
- Audit hold: Legal or compliance requires you to freeze a specific agent's activity without deleting any data. Cage it; the immutable strand is untouched and fully readable.
- Maintenance window: You are migrating or reconfiguring an agent. Cage it to signal downstream consumers to pause before you make changes.
- Graceful drain: In a distributed deployment, cage a node you intend to remove. Connected peers see the A2A event and stop routing new writes to it.
HTTP API
# Cage (marks agent as restricted, broadcasts event) curl -X POST http://localhost:7475/v1/agent/cage # Uncage (resume normal operation) curl -X DELETE http://localhost:7475/v1/agent/cage
{ "caged": true, "agent_id": "orders" }
{ "caged": false, "agent_id": "orders" }Check cage state
curl http://localhost:7475/v1/status | jq .caged # true → agent is caged # false → agent is operating normally
The Control Plane's Overview page shows a cage badge next to any agent that is currently restricted, and the Cage / Uncage buttons are available in the top action bar. You can also cage an agent from the SDK via the admin HTTP API if your orchestration layer needs programmatic control.
Epigenetics — behavioral profiles
Epigenetics lets you switch an agent's behavioral profile at runtime without restarting. Profiles control things like whether writes are allowed and the agent's operational mode.
# See all available profiles
curl http://localhost:7475/v1/epigenetics/profiles
# Get active profile
curl http://localhost:7475/v1/epigenetics/profile
# Switch to analytics mode (read-only writes blocked)
curl -X PUT http://localhost:7475/v1/epigenetics/profile \
-H "Content-Type: application/json" \
-d '{"name": "analytics"}'
# View profile change history
curl http://localhost:7475/v1/epigenetics/history14. HIPAA & SOX Compliance
SapixDB's architecture is compliance-by-default. Here's why that matters and what it means in practice:
15. Troubleshooting
You must provide a 64-character hex seed for the agent's Ed25519 keypair. Generate one with python3 -c "import secrets; print(secrets.token_hex(32))" and add it to docker-compose.yml as SAPIX_KEYPAIR_SEED_HEX: your_hex_here. See the Installation section above.
The data directories need to be owned by your user, not root. Use bind mounts instead of named volumes — create the folders yourself first: mkdir -p data/strand data/graph data/blobs, then use ./data/strand:/data/strand (etc.) in your docker-compose volumes section. This way Docker uses directories you own. See the Installation section above for the complete setup.
Another process is using port 7475. Change the port mapping in docker-compose.yml: "7476:7475" and update your requests to use port 7476.
Install curl: on macOS run brew install curl, on Ubuntu run sudo apt install curl. Alternatively, use Postman (a free GUI tool) to send requests instead.
The container may still be starting. Wait 10 seconds and try again. Check container status with docker compose ps. If it says "unhealthy", check logs: docker compose logs sapixdb
Make sure you called POST /v1/genesis first (once on first startup), then write using POST /v1/records/json. The GET /v1/strand/records endpoint returns all records including genesis. Check GET /v1/strand/head to see the current record count.
Run docker compose down in your project folder. Your data is safely stored in Docker volumes and will be there when you start again with docker compose up -d.
For a full database backup, use the Disaster Recovery panel in the Control Plane (http://localhost:3781/agents) to create and download a .srpx bundle — one encrypted file that covers every agent. See Section 19 — Backup & Disaster Recovery below. For per-agent backups, see Section 18 — Export & Import.
Enterprise features require a license key. Add SAPIX_LICENSE_KEY: "your-key-here" to the environment: section of your docker-compose.yml and restart the container. To get a key: request a free 30-day trial at sapixdb.com or email [email protected] — no credit card needed. The agent logs its license status on startup: docker logs sapixdb | grep -i license.
Get an API key from your Codios dashboard at codios.midlantics.com → Settings → API Keys, then add two lines to your docker-compose.yml: SAPIX_CODIOS_URL: "https://codios-api.midlantics.com" and SAPIX_CODIOS_API_KEY: "codios_sk_...". Once set, every write and read is authorized by Codios before it reaches the strand. See Section 2c (Codios) for full details.
Get an API key from your A2A dashboard at a2a.midlantics.com → Settings → API Keys, then add two lines to your docker-compose.yml: SAPIX_A2A_URL: "https://a2a-api.midlantics.com" and SAPIX_A2A_API_KEY: "a2a_sk_...". SapixDB then emits a structured audit event after every write — visible in your A2A dashboard alongside BOBOYKA agent traces. See Section 2d (A2A) for full details.
sapixdb/agent:latest (also tagged :enterprise) is the full enterprise image — it includes every feature: Mutants, natural-language queries, HIPAA/SOX compliance, distributed cluster mode, Boboyka integration, connectors, and epigenetics. sapixdb/agent:community is the open-source edition (Apache 2.0) — it includes strands, SaQL, graph, blobs, crons, triggers, indexes, snapshots, mesh, policies, admin keys, capacity management, and the control-plane dashboard. Both images work with the same docker-compose.yml; just change the image: line. If you are building from source, pass --build-arg FEATURES=community or --build-arg FEATURES=enterprise to docker build.
16. The sapix CLI
Every SapixDB Docker image ships with a built-in operator CLI called sapix. It connects to the server over HTTP — no source code or Cargo required. You run it via docker exec against your running container.
Running commands
# Any sapix command runs inside the container docker exec sapixdb sapix status docker exec sapixdb sapix agents list docker exec sapixdb sapix mutant list
~/.zshrc or ~/.bashrc so you can drop the docker exec sapixdb prefix everywhere:alias sapix='docker exec sapixdb sapix' # Then just type: sapix status sapix agents list
Command reference
sapix statusPrimary agent health — uptime, record count, caged state, proposal countssapix agents listAll named agents with record count, schema version, zone, and cage statesapix agents status <id>Detail for one agentsapix agents create <id> [--zone governed]Create a new named agent (auto-generates seed)sapix agents cage <id>Cage an agentsapix agents uncage <id>Uncage an agentsapix strand records <id> [--limit 15]Show recent nucleotides for an agentsapix strand export <id> -o file.jsonlExport full strand to JSONLsapix strand import <id> -i file.jsonlImport a JSONL backup into an agentsapix chain verify <id>Verify the BLAKE3 parent-hash chainsapix mutant listAll proposals grouped by statussapix mutant propose --target <id> --proposer <id> --kind SchemaVersion --version 2Submit a proposalsapix mutant approve <proposal-id> --actor <id>Approve a pending proposalsapix mutant apply <proposal-id>Apply an approved proposalEvery command accepts --json to output raw JSON — useful for scripting or piping into jq. Use SAPIX_URL to point at a non-default server: export SAPIX_URL=http://myserver:7475.
17. Chain Verification
Every nucleotide in a SapixDB strand links to the previous one via a BLAKE3 hash — parent_hash of block N must equal content_hash of block N−1. The chain verifier walks the entire strand and checks every link, confirming that no record has been tampered with or silently dropped.
# Verify users agent — quiet mode (errors only) docker exec sapixdb sapix chain verify users # See every block as it is checked docker exec sapixdb sapix chain verify users --verbose # Machine-readable output for CI pipelines docker exec sapixdb sapix chain verify users --json
Output — intact chain
Verifying chain: users (42 records) ✓ Chain intact · 42/42 blocks verified
Output — broken chain
Verifying chain: users (42 records) ✗ block 15 REC hash=deadbeef1234… parent=99aabbcc… expected=7f3e12ab… ✗ Chain BROKEN · 1 error(s) across 42 blocks
1 if broken — safe to use in CI or shell scripts: docker exec sapixdb sapix chain verify users && echo "ok"content_hash values against an export.18. Export & Import — Backup and Restore
The sapix strand exportcommand streams an agent's full history to a JSONL file (one JSON object per line). sapix strand import replays that file into any agent — on the same node or a different one — writing the exact same payload bytes so content_hash values are preserved.
File format
The export file is plain JSONL. Line 1 is a header with metadata; every subsequent line is one nucleotide in strand order (genesis first, tip last):
{"sapix_export":true,"version":1,"agent_id":"users","total":42,"chain_head":"9aead8fb…","exported_at_ms":1747347600000}
{"record_id":"…","content_hash":"…","parent_hash":"…","timestamp_hlc":…,"flags":1,"schema_version":1,"payload_b64":"…"}Backup
# Export to a timestamped file docker exec sapixdb sapix strand export users | gzip > users-$(date +%Y%m%d).jsonl.gz # Or write directly to a mounted volume docker exec sapixdb sapix strand export users -o /data/backups/users.jsonl
Restore to the same agent
# From a gzip backup — pipe through stdin gunzip -c users-20260516.jsonl.gz | docker exec -i sapixdb sapix strand import users -i /dev/stdin
Copy to a different agent or node
# Export from users, import into users_v2 on the same node docker exec sapixdb sapix strand export users | docker exec -i sapixdb sapix strand import users_v2 --url http://localhost:7475 -i /dev/stdin # Export from one server, import into another docker exec node1 sapix strand export users | docker exec -i node2 sapix strand import users -i /dev/stdin
content_hash of each nucleotide is identical to the original. The import command verifies this automatically after each write — a hash mismatch causes a non-zero exit and is reported by record number.parent_hashchain is rebuilt from scratch with a fresh HLC timestamp sequence — it will differ from the original strand's chain structure. Run sapix chain verify <id> after any import to confirm the new chain is internally consistent.Recommended backup schedule
# Daily export of all agents at 02:00 0 2 * * * docker exec sapixdb sapix strand export users | gzip > /backups/users-$(date +%Y%m%d).jsonl.gz 0 2 * * * docker exec sapixdb sapix strand export orders | gzip > /backups/orders-$(date +%Y%m%d).jsonl.gz
19. Backup & Disaster Recovery
SapixDB has a built-in disaster recovery system called the Recovery Toolkit. It bundles your entire database — every agent, every record chain — into a single encrypted file called a .srpx bundle. If your server is destroyed, you can restore the complete cluster on a new node from that one file.
- .srpx bundle — one encrypted file for the whole database, recommended for disaster recovery
- NDJSON export — per-agent export for targeted restores (see Section 18)
What a .srpx bundle is
A .srpx file is a complete, point-in-time snapshot of all agents' record chains, compressed with zstd and then encrypted with AES-256-GCM. An Ed25519 signature is written over the entire file — wrong seed, tampered bytes, or a corrupted download all fail the seal check before any decryption is attempted. The file is useless without the master seed.
Prerequisites — master seed
The Recovery Toolkit requires a master seed: a 64-character hex string that derives both the encryption key and the signing keypair for the bundle. Set it when starting the node:
environment: SAPIX_MASTER_SEED: YOUR_64_HEX_MASTER_SEED # same seed used to bundle and unbundle
SAPIX_MASTER_SEED=<64-hex> docker compose up -d --build
Creating a bundle — Control Plane
The easiest way to create and download a bundle is the Disaster Recovery panel in the Control Plane at http://localhost:3781/agents (scroll to the bottom of the Agents page):
Creating a bundle — API
curl -X POST http://localhost:7475/v1/control/recovery/bundle
{
"created_at_ms": 1716000000000,
"total_records": 4485,
"total_agents": 8,
"size_bytes": 892416,
"duration_ms": 847
}curl http://localhost:7475/v1/control/recovery/status
{
"seed_available": true,
"last_bundle": {
"created_at_ms": 1716000000000,
"total_records": 4485,
"total_agents": 8,
"size_bytes": 892416,
"duration_ms": 847
},
"age_secs": 3600
}curl -O http://localhost:7475/v1/control/recovery/bundle/download # saves as sapixdb-recovery-YYYYMMDD.srpx
Creating a bundle — CLI (offline / no Control Plane)
# Bundle the whole database sapix-recover bundle \ --db-path /var/lib/sapixdb \ --out /backups/snapshot.srpx \ --seed $SAPIX_MASTER_SEED \ --primary-id primary # Or via docker exec docker exec sapixdb sapix-recover bundle \ --db-path /data/strand \ --out /data/backups/snapshot.srpx \ --seed $SAPIX_MASTER_SEED \ --primary-id primary
Recommended backup schedule
# POST to create, then download to local backup directory
0 3 * * * curl -s -X POST http://localhost:7475/v1/control/recovery/bundle \
&& curl -s -o /backups/sapixdb-$(date +%Y%m%d).srpx \
http://localhost:7475/v1/control/recovery/bundle/downloadRestoring from a bundle
On the new (or rebuilt) server, use sapix-recover unbundle to decrypt and reconstruct the complete database. The output directory must be empty — it becomes the new strand_dir for the restored node.
scp sapixdb-20260518.srpx user@newserver:/tmp/
sapix-recover unbundle \ --input /tmp/sapixdb-20260518.srpx \ --out-dir /var/lib/sapixdb-restored \ --seed $SAPIX_MASTER_SEED
Unbundling /tmp/sapixdb-20260518.srpx → /var/lib/sapixdb-restored ✓ Seal verified ✓ Decrypted (892 KB → 3.1 MB decompressed) ✓ Reconstructed 4485 records across 8 agents Done in 1.2s
sapix-recover verify --db-path /var/lib/sapixdb-restored
FULLY VERIFIED — 4485 records, 8 agents, all chains intact
environment: SAPIX_MASTER_SEED: <same-seed-used-to-bundle> SAPIX_STRAND_DIR: /var/lib/sapixdb-restored
SAPIX_MASTER_SEED=... docker compose up -d
Wrong seed — what happens
If you provide the wrong master seed, unbundle fails at the seal verification step — before any decryption is attempted:
Error: seal verification failed — wrong seed or tampered bundle
There is no partial decryption oracle. Either the seal matches (correct seed, intact file) or the operation stops immediately.
Explore the full developer reference for advanced queries, distributed mode, agent graph traversal, and SaQL — our semantic query language.