SapixDBSapixDB/Docs
Home
User Manual · v1.0

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.

What you'll need
  • 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.

Traditional Database
SapixDB
Rows that get overwritten
Nucleotides that accumulate
DELETE removes data forever
History stays — always queryable
Schema migrations required
Fields evolve naturally
Manual audit logs
Built-in cryptographic trail
You own the table
AI agents can own the data

2. Installation

SapixDB runs as a Docker container. You do not need to install anything special — Docker handles all the dependencies.

Two editions on Docker HubSapixDB publishes two images. Use 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.
1
Install Docker Desktop
Download and install Docker Desktop from docker.com/products/docker-desktop. After installation, open Docker Desktop and make sure it says "Engine running".
2
Generate your agent keypair seed
Each SapixDB agent has a permanent Ed25519 identity. Generate a secret seed — run this once and save the output:
terminal
# macOS / Linux
python3 -c "import secrets; print(secrets.token_hex(32))"

# or with openssl
openssl rand -hex 32
You will get a 64-character hex string like:
example output (do not use this one)
a3f8c201b7e94d0f2c6a1b3e5d7f9a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2
Save this value.It is your agent's private key. Treat it like a password — keep it in a password manager or secret store. You will put it in your docker-compose.yml next. If you lose it your agent loses its cryptographic identity.
One seed per agent — never reuseIf you run multiple agents, generate a separate seed for each one. The seed is the agent's permanent identity on the strand. Records signed by one agent cannot be re-signed by another.
3
Create a project folder and docker-compose.yml
Open your terminal, create a folder with the data directories, and create a file named docker-compose.yml inside it. Replace YOUR_SEED_HERE with the hex string you generated above:
terminal
mkdir my-sapixdb && cd my-sapixdb
mkdir -p data/strand data/graph data/blobs
docker-compose.yml
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_..."
Using ./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.
4
Start SapixDB
terminal
docker compose up -d
Docker will download the SapixDB image (first time only — takes about a minute) and start the agent in the background.
5
Verify it's running
terminal
curl http://localhost:7475/v1/health
You should see:
response
{
  "status": "ok",
  "agent_id": "my-first-agent",
  "record_count": 0,
  "chain_head": "0000...0000"
}
If you see that, SapixDB is live and ready.
No curl? Use your browserOpen http://localhost:7475/v1/healthdirectly in your browser. You'll see the same JSON response.

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.

Community (no license key)
Enterprise (with license key)
Strand engine + SaQL
All community features
Graph, blobs, mesh
Mutants — governed schema evolution
Policies, crons, triggers
Epigenetics — behavioral profiles
Indexes, snapshots, views
HIPAA + SOX compliance packs
Admin keys, capacity
Distributed cluster mode
Control-plane dashboard
NL natural-language queries
Apache 2.0 open source
Connectors, Boboyka, Cloud, Commons

How to get a license key

Community — Free forever
Self-hosted, no license key needed. Full SapixDB feature set with limits: up to 5 agents and 10 GB storage. No expiry. Great for evaluation, personal projects, and open-source work.
Free Trial — 30 days
Full Enterprise access for 30 days. No credit card required. Email [email protected] or request via sapixdb.com. You'll receive a license key by email within 24 hours. Converts to a paid plan or drops back to Community when the trial ends.
Starter — $299/mo
Up to 20 agents, 100 GB storage. Includes HIPAA and SOX compliance packages, time travel, chain verification, Mutants, and all SDK access. Annual billing available ($2,990/yr).
Professional — $999/mo
Unlimited agents, 1 TB storage. Everything in Starter plus distributed mode and multi-node replication. Ideal for teams running SapixDB across multiple regions or services. Annual billing available ($9,990/yr).
Enterprise — $3,999/mo
Unlimited agents, custom storage. Everything in Professional plus a dedicated SLA, priority support, custom compliance packages, and on-premises deployment options. License key issued on contract signing. Contact [email protected].

Activating your license key

Once you have a license key, add one line to your docker-compose.yml:

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:

terminal
docker compose down && docker compose up -d

The agent logs its license status at startup. Check with:

terminal
docker logs sapixdb | grep -i license
example output (enterprise)
INFO sapix_agent::license: Enterprise license verified org_id=acme-corp plan=enterprise permanent=true
example output (community)
INFO sapix_agent::license: No SAPIX_LICENSE_KEY set — running in community mode
License key safetyYour license key is verified locally inside the container — it is never sent over the network. An invalid or expired key falls back to community mode gracefully; your data is never locked or lost.

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

  1. Go to codios.midlantics.com
  2. Navigate to Settings → API Keys
  3. Click Generate new key — copy the codios_sk_... value immediately (shown once)

Add to docker-compose.yml

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:

authorization request (sent by SapixDB to Codios)
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

terminal
curl http://localhost:7475/v1/codios/stats
response
{
  "mode":                "enforced",
  "endpoint_configured": true,
  "authorized":          1482,
  "denied":              3,
  "errors":              0
}
Open mode is safe for local developmentIf 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

  1. Go to a2a.midlantics.com
  2. Navigate to Settings → API Keys
  3. Click Generate new key — copy the a2a_sk_... value immediately (shown once)

Add to docker-compose.yml

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):

audit event (sent by SapixDB to A2A)
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.

A2A is always fire-and-forgetA2A events are emitted in a background task — they never block the write that triggered them. If 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:

terminal
curl -X POST http://localhost:7475/v1/genesis -H "Content-Type: application/json" -d '{"zone":"governed"}'
response
{
  "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.

Control PlaneOpen the Control Plane at http://localhost:3781 and go to Strand Browser — every record you write will appear there in real time.

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

terminal
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"}}'
response
{
  "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

terminal
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.

No DELETE in SapixDBSapixDB is append-only. To mark a record deleted, write a new record with a "_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.

terminal — all records
curl "http://localhost:7475/v1/strand/records?offset=0&limit=50"
response
{
  "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": "..."
    }
  ]
}
Decoding payload_b64Payloads are base64-encoded MessagePack. To decode in Python: import base64, msgpack; msgpack.unpackb(base64.b64decode(payload_b64), raw=False)

Get the N most recent records

terminal — latest 10
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

terminal
curl http://localhost:7475/v1/records/b4e91a3f...

Check strand head and record count

terminal
curl http://localhost:7475/v1/strand/head
response
{
  "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.

terminal
curl http://localhost:7475/v1/records/<content_hash>
response — payload is decoded JSON
{
  "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:

terminal — all records via time range
curl -X POST http://localhost:7475/v1/query -H "Content-Type: application/json" -d '{"type":"time_range","from_ts":0,"to_ts":18446744073709551615}'
terminal — range using HLC values from previous responses
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

terminal
curl http://localhost:7475/v1/strand/verify
response
{
  "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.

terminal — strand as of a past timestamp
curl "http://localhost:7475/v1/strand/as-of?timestamp_hlc=1747000001500&limit=50"
response
{
  "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.

Why this mattersHealthcare, finance, and any regulated industry requires proving what your data looked like at a past moment. SapixDB satisfies this automatically for every record, forever — no snapshots, no extra tooling.

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

terminal
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.

terminal — create two agents
# 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

terminal — write to named agents on the same port
# 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

terminal — query named agents
# 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/agents
One port — all agentsAll agents live on port 7475. 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.

docker-compose.yml — one container, multiple agents
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:7475

Each 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

terminal — link two agents
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.

terminal — link Alice's user record to her order
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.

terminal — traverse from users, follow all edges up to depth 2
curl "http://localhost:7475/v1/graph/traverse?agent_id=users&depth=2"
response
{
  "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

terminal
curl "http://localhost:7475/v1/graph/edges/users"
# Inbound:
curl "http://localhost:7475/v1/graph/edges/orders?direction=in"

Delete an edge

terminal
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.

terminal — query users and orders separately, then join on user_id
# 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>"
Record refs are the foreign keyStore the cross-agent link once with 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.

terminal — two-step saga across agents
# 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\"}
  ]
}"
response — all steps committed
{
  "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.

response — step 2 failed, step 1 compensated
{
  "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

terminal
# 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:

Zone
Policy
immutable_core
Never — cannot be mutated
protected
Requires 3 approvals
governed
Requires 2 approvals
free
Auto-approved (applies after 4 hours)

Propose a mutation

terminal — propose a schema version bump
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 }
}'
response
{
  "proposal_id": "019e2870911778e5...",
  "status": "pending",
  "policy": "TwoOfN"
}

Approve and apply

terminal — governed zone: two admins must approve
# 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
response — mutation applied and written to the audit strand
{
  "proposal_id": "019e2870...",
  "applied": true,
  "target_agent_id": "orders",
  "mutation": "schema_version→2"
}

Other mutation kinds

mutation kind examples
# 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

terminal
# 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/mutations
Every mutation is written to the strandWhen a mutation is applied, SapixDB writes a MUTATION-flagged audit block to the primary agent's strand — cryptographically signed, hash-linked, permanent. You always have a tamper-evident record of who proposed what, who approved it, and when.

13. 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: true in the agent's metadata store (persisted to disk).
  • Emits an agent_caged A2A event — any connected orchestrator (Codios, custom agent) receives it and can react immediately.
  • The status endpoint returns caged: true so 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

terminal — cage an agent
# 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
response
{ "caged": true,  "agent_id": "orders" }
{ "caged": false, "agent_id": "orders" }

Check cage state

terminal
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.

terminal
# 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/history

14. HIPAA & SOX Compliance

SapixDB's architecture is compliance-by-default. Here's why that matters and what it means in practice:

HIPAA (Healthcare)
Every access to patient data is permanently logged with a cryptographic signature. Who read it, who wrote it, when — immutably. This satisfies the HIPAA audit trail requirement without any extra tooling.
SOX (Financial)
Financial records are append-only and hash-linked. No one can alter a past entry without breaking the chain — which is immediately detectable. SOX requires this kind of tamper-evident record keeping.
GDPR (Right to be Forgotten)
SapixDB stores data in encrypted blobs. To 'delete' under GDPR, you destroy the encryption key — the record remains in the chain but becomes unreadable. The cryptographic chain stays intact.

15. Troubleshooting

Q: Container crashes with: Configuration error: SAPIX_KEYPAIR_SEED_HEX is required

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.

Q: Container crashes with: Permission denied (os error 13)

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.

Q: SapixDB won't start / port already in use

Another process is using port 7475. Change the port mapping in docker-compose.yml: "7476:7475" and update your requests to use port 7476.

Q: curl: command not found

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.

Q: Health check returns connection refused

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

Q: I wrote data but GET /v1/strand/records returns only the genesis

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.

Q: How do I stop SapixDB?

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.

Q: How do I back up my data?

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.

Q: How do I activate enterprise features like Mutants, HIPAA, or NL queries?

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.

Q: How do I connect SapixDB to Codios for agent authorization?

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.

Q: How do I connect SapixDB to A2A for audit logging?

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.

Q: What is the difference between sapixdb/agent:latest and sapixdb/agent:community?

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

terminal
# Any sapix command runs inside the container
docker exec sapixdb sapix status
docker exec sapixdb sapix agents list
docker exec sapixdb sapix mutant list
Shell alias — feels nativeAdd this to your ~/.zshrc or ~/.bashrc so you can drop the docker exec sapixdb prefix everywhere:
~/.zshrc
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 counts
sapix agents listAll named agents with record count, schema version, zone, and cage state
sapix agents status <id>Detail for one agent
sapix agents create <id> [--zone governed]Create a new named agent (auto-generates seed)
sapix agents cage <id>Cage an agent
sapix agents uncage <id>Uncage an agent
sapix strand records <id> [--limit 15]Show recent nucleotides for an agent
sapix strand export <id> -o file.jsonlExport full strand to JSONL
sapix strand import <id> -i file.jsonlImport a JSONL backup into an agent
sapix chain verify <id>Verify the BLAKE3 parent-hash chain
sapix mutant listAll proposals grouped by status
sapix mutant propose --target <id> --proposer <id> --kind SchemaVersion --version 2Submit a proposal
sapix mutant approve <proposal-id> --actor <id>Approve a pending proposal
sapix mutant apply <proposal-id>Apply an approved proposal

Every 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.

terminal
# 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

sapix chain verify users
  Verifying chain: users  (42 records)

  ✓  Chain intact  ·  42/42 blocks verified

Output — broken chain

sapix chain verify users
  Verifying chain: users  (42 records)

  ✗  block     15  REC  hash=deadbeef1234…  parent=99aabbcc…  expected=7f3e12ab…

  ✗  Chain BROKEN  ·  1 error(s) across 42 blocks
When to run thisRun chain verify after a restore, after upgrading SapixDB, or periodically in a cron job as a data-integrity audit. The command exits with code 1 if broken — safe to use in CI or shell scripts: docker exec sapixdb sapix chain verify users && echo "ok"
What it does not verifyThe verifier checks hash-chain linkage — that no block was dropped or reordered. It does not decrypt or semantically validate payload content. For payload content integrity, compare 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):

users-backup.jsonl (first two lines)
{"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

terminal
# 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

terminal
# 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

terminal
# 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 hashes are preservedImport uses a raw-payload write endpoint that bypasses re-encoding. The 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.
Chain structure after restoreAfter import the 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

cron (host machine)
# 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.

Two backup approaches
  • .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:

docker-compose.yml
environment:
  SAPIX_MASTER_SEED: YOUR_64_HEX_MASTER_SEED   # same seed used to bundle and unbundle
or directly
SAPIX_MASTER_SEED=<64-hex> docker compose up -d --build
Keep the master seed safeThe master seed is the only key that can decrypt a bundle. Store it in a password manager or secrets vault completely separately from your server. Losing it means losing access to all bundles created with it.

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):

SEED READY badge
Node has the master seed in memory — bundling is enabled
Create Bundle button
Triggers bundle creation on the server, shows record count + size when done
Download .srpx link
Streams the latest bundle to your browser as a file download
Bundle age tile
Shows how old the latest bundle is — create a new one if it's stale

Creating a bundle — API

terminal — create bundle
curl -X POST http://localhost:7475/v1/control/recovery/bundle
response
{
  "created_at_ms": 1716000000000,
  "total_records": 4485,
  "total_agents": 8,
  "size_bytes": 892416,
  "duration_ms": 847
}
terminal — check status (seed ready? bundle age?)
curl http://localhost:7475/v1/control/recovery/status
response
{
  "seed_available": true,
  "last_bundle": {
    "created_at_ms": 1716000000000,
    "total_records": 4485,
    "total_agents": 8,
    "size_bytes": 892416,
    "duration_ms": 847
  },
  "age_secs": 3600
}
terminal — download the bundle
curl -O http://localhost:7475/v1/control/recovery/bundle/download
# saves as sapixdb-recovery-YYYYMMDD.srpx

Creating a bundle — CLI (offline / no Control Plane)

terminal
# 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

cron — daily bundle via API
# 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/download
Store the .srpx file off-serverCopy the bundle to S3, Backblaze, or any remote storage immediately after creation. A backup stored on the same server as the data it protects is not a backup.

Restoring 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.

1
Copy the .srpx bundle to the new server
terminal
scp sapixdb-20260518.srpx user@newserver:/tmp/
2
Unbundle into a fresh directory
terminal
sapix-recover unbundle \
  --input /tmp/sapixdb-20260518.srpx \
  --out-dir /var/lib/sapixdb-restored \
  --seed $SAPIX_MASTER_SEED
output
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
3
Verify chain integrity
terminal
sapix-recover verify --db-path /var/lib/sapixdb-restored
expected output
FULLY VERIFIED — 4485 records, 8 agents, all chains intact
4
Start the agent pointing at the restored directory
docker-compose.yml
environment:
  SAPIX_MASTER_SEED: <same-seed-used-to-bundle>
  SAPIX_STRAND_DIR: /var/lib/sapixdb-restored
terminal
SAPIX_MASTER_SEED=... docker compose up -d
What gets restoredThe primary agent's strand, all named agents' strands, all record chains, all payload bytes — everything needed to resume normal operation. Graph edges and blob objects are not included in the .srpx bundle; restore those separately if needed.

Wrong seed — what happens

If you provide the wrong master seed, unbundle fails at the seal verification step — before any decryption is attempted:

error output
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.

Ready to go deeper?

Explore the full developer reference for advanced queries, distributed mode, agent graph traversal, and SaQL — our semantic query language.