SapixDBSapixDB/Docs
Home

Security

Security

SapixDB is a self-contained database — no external auth service required. Record payloads are encrypted at rest with AES-256-GCM. A root key bootstraps your deployment. Scoped API keys give each service exactly the access it needs. Codios adds per-operation receipts on top.

The Three Layers

┌──────────────────────────────────────────────────────────┐
Layer 3 — CODIOS CONTRACTS (agent-to-agent only) │
│ Signed receipt per operation. TTL-scoped. Per-agent. │
├──────────────────────────────────────────────────────────┤
Layer 2 — SCOPED API KEYS (every caller) │
│ Named, revocable, expirable, rate-limited, scope-checked. │
├──────────────────────────────────────────────────────────┤
Layer 1 — ROOT KEY (operator only) │
│ SAPIX_ROOT_KEY — creates all other keys. Never in app. │
└──────────────────────────────────────────────────────────┘

These layers stack. An agent calling SapixDB goes through all three. A human admin in the Control Plane goes through Layer 2. An external application goes through Layer 2. Every layer is optional — SapixDB starts in open mode if nothing is configured — but all three should be active in production.

Layer 1 — Root Key

The root key is your operator credential. It grants unconditional access to everything including /v1/admin/*. Generate it once, store it in a password manager, use it only to create scoped keys.

Generate and configure
# Generate
openssl rand -hex 32
# → a3f9c2e1b4d7f8a2c5e0b1d3f6a9c4e7b2d5f8a1c4e7b0d3f6a9c2e5b8d1f4

# Set on SapixDB service (Railway, Docker, k8s)
SAPIX_ROOT_KEY=a3f9c2e1b4d7...
Never embed SAPIX_ROOT_KEY in application codeThe root key is like a database superuser password. Use it once to create service keys, then leave it in the password manager. Your running services should only ever hold scoped sapix_sk_... keys.

Layer 2 — Scoped API Keys

Every service, app, or integration gets its own named API key. Keys are stored hashed in the _api_keys system agent and loaded into memory at startup for O(1) lookup on every request.

Key format

Keys are formatted as sapix_sk_ followed by 64 hex characters (32 random bytes, 256 bits of entropy). The secret is shown once at creation — only its SHA-256 hash is stored. Save it immediately.

Scopes

Every key has a list of scope strings. Each scope is verb:resource-glob. A request is allowed when any scope in the key's list matches the request's required verb and resource.

ScopeWhat it grants
admin:*Everything — all routes including key management (root key only)
admin:api-keysCreate / revoke / list API keys (for key-issuing services)
write:*Write to any agent, strand, blob, graph edge
read:*Read from anything — strand, query, NL query, graph, blobs
write:agents/orders/*Write only to the orders agent
read:agents/boboyka::*Read from any agent in the boboyka organism
write:agents/hire::*Write to any hire organism agent
write:X does NOT imply read:XUnlike many auth systems, SapixDB scopes are explicit. A service that needs both read and write should list both: ["write:agents/orders/*", "read:agents/orders/*"].admin:* implies everything and is the only superscope.

Creating a key

curl
curl -X POST https://<sapixdb-url>/v1/admin/api-keys \
  -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "agent-database-boboyka-prod",
    "scopes": ["write:agents/boboyka::*", "read:agents/boboyka::*"],
    "caller_id": "agt_database_prod",
    "rate_limit_rps": 500
  }'
Response — save the key field immediately
{
  "key_id": "kid_a3f9c2e1b4d7f8a2",
  "key": "sapix_sk_a3f9c2e1b4d7f8a2c5e0b1d3f6a9c4e7...",
  "key_prefix": "sapix_sk_a3f9…",
  "label": "agent-database-boboyka-prod",
  "scopes": ["write:agents/boboyka::*", "read:agents/boboyka::*"],
  "created_at_ms": 1748304000000,
  "expires_at_ms": null,
  "caller_id": "agt_database_prod",
  "rate_limit_rps": 500,
  "warning": "Save this key immediately — it will not be shown again."
}

Using a key

Send every request with Authorization: Bearer sapix_sk_.... Optionally include X-Sapix-Caller to override the Codios caller identity.

TypeScript SDK
import { SapixClient } from "@sapixdb/sdk";

const db = new SapixClient({
  url: process.env.SAPIX_API_URL,
  apiKey: process.env.SAPIX_API_KEY,   // sapix_sk_... scoped key
  callerId: "my-service",              // optional — forwarded to Codios
});
Python SDK
from sapixdb import SapixClient

db = SapixClient(
    url=os.environ["SAPIX_API_URL"],
    agent="my-app",
    api_key=os.environ["SAPIX_API_KEY"],
    caller_id="my-service",
)
curl
curl -H "Authorization: Bearer sapix_sk_..." \
     -H "X-Sapix-Caller: my-service" \
     https://<sapixdb-url>/v1/agents/orders/records/json \
     -d '{"data": {"order_id": "123", "status": "paid"}}'

Managing keys

List all keys
curl -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  https://<sapixdb-url>/v1/admin/api-keys
Revoke a key
curl -X DELETE \
  -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  https://<sapixdb-url>/v1/admin/api-keys/kid_a3f9c2
Update scopes or rate limit
curl -X PATCH \
  -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scopes": ["read:*"], "rate_limit_rps": 100}' \
  https://<sapixdb-url>/v1/admin/api-keys/kid_a3f9c2
View usage stats (since last restart)
curl -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  https://<sapixdb-url>/v1/admin/api-keys/kid_a3f9c2/usage

# → { "request_count_since_restart": 14203, "last_used_at_ms": 1748394512000 }

Rate Limiting

Set rate_limit_rps on any key to enforce a per-second request limit. SapixDB uses a token bucket per key — each request consumes one token, tokens refill atrate_limit_rps per second. Excess requests get a 429 response.

Response when rate limit exceeded
HTTP 429 Too Many Requests
{
  "error": "rate_limited",
  "message": "Key 'external-app' exceeded its rate limit of 100 req/s."
}

rate_limit_rps: null (the default) means unlimited — appropriate for trusted internal services on a private network.

Key Delegation — Multi-Tenant Apps

If you're building an application on SapixDB and need per-user or per-tenant data isolation, your backend can issue scoped keys for each user. Give your backend service the admin:api-keys scope and it can create child keys — but only with scopes it already has.

Backend issues a per-user key (TypeScript)
// Your backend server has: ["write:agents/myapp::*", "read:agents/myapp::*", "admin:api-keys"]
const res = await fetch(`${SAPIX_URL}/v1/admin/api-keys`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SAPIX_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    label: `user-${userId}`,
    scopes: [
      `write:agents/myapp::${userId}/*`,
      `read:agents/myapp::${userId}/*`,
    ],
    expires_at_ms: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days
    rate_limit_rps: 50,
  }),
});
const { key } = await res.json();
// Return 'key' to the user — they now access only their own data
Scope delegation is enforced by SapixDBA key with admin:api-keysscope cannot grant scopes it doesn't have. If your backend has write:agents/myapp::*, it cannot issue a child key withwrite:agents/other-app::*. SapixDB enforces the boundary — no trust required.

HTTP API Reference

MethodPathRequired scopeDescription
POST/v1/admin/api-keysroot OR admin:api-keysCreate key — secret shown once
GET/v1/admin/api-keysroot OR admin:*List all keys
GET/v1/admin/api-keys/:idroot OR admin:*Get one key
DELETE/v1/admin/api-keys/:idroot OR admin:*Revoke key
PATCH/v1/admin/api-keys/:idroot OR admin:*Update label / scopes / expiry / rate
GET/v1/admin/api-keys/:id/usageroot OR admin:*Usage stats since last restart

At-Rest Encryption

SapixDB encrypts every record payload in segment files with AES-256-GCMso that raw filesystem access — stolen disk, leaked backup, cloud storage exposure — yields no readable data. The WAL (write-ahead log) stays plaintext for cold-start resilience but is automatically deleted after each server restart or checkpoint.

Enabling encryption

Set SAPIX_MASTER_SEED to a 64-hex-character string (32 bytes). This single secret drives all key derivation — keypairs, per-agent encryption keys, and organism agent seeds. Generate it once:

Generate a master seed
openssl rand -hex 32
# → deadbeefdeadbeef...  (64 hex chars — exactly this format)

# Set on your SapixDB service
SAPIX_MASTER_SEED=deadbeef...64chars...
SAPIX_MASTER_SEED must be exactly 64 hex charactersA plain English passphrase like mysecretkey is silently ignored — the server starts in unencrypted mode with no warning to the caller. Always use openssl rand -hex 32 to generate the seed.

How it works

One AES-256-GCM key is derived per agent using HKDF-SHA256:

payload_key = HKDF-SHA256(IKM=MASTER_SEED, salt=agent_id, info="strand-payload-encryption-v1")
— domain-separated from Ed25519 identity derivation
— deterministic: same seed + same agent_id = same key on every node
— replication works unchanged: all nodes share the same seed

Each record gets a fresh 12-byte random nonce. On-disk layout inside the segment:

Encrypted payload region (inside .sapx segment block)
nonce (12 bytes, random per record)
ciphertext (plaintext_len bytes, AES-256-GCM)
auth tag (16 bytes, GCM authentication)

The content_hash header field is always BLAKE3(plaintext) — chain integrity and Ed25519 signatures are unaffected by encryption. API callers always receive decrypted payloads; the ENCRYPTED flag is cleared before the response is sent.

What is and isn't encrypted

LocationEncrypted?Why
Segment files (.sapx) — user agentsYesAll application record payloads
WAL file (strand.wal)No — deleted after useCold-start resilience; deleted after replay or checkpoint
System agents (_api_keys, _crons, etc.)NoOperational metadata only; no user payload data
Block header (content_hash, timestamps, signatures)NoNeeded for chain verification and indexing

Flushing plaintext from disk

After writing records, a small plaintext WAL window exists until the next restart or explicit checkpoint. Call this endpoint to seal and delete all WAL files immediately:

Remove all plaintext WAL files
curl -X POST https://<sapixdb-url>/v1/control/checkpoint
# → { "status": "ok", "agents_checkpointed": 12 }

Behavior with wrong or missing seed

ScenarioOutcome
Correct SAPIX_MASTER_SEEDRecords decrypt normally on every read
Wrong SAPIX_MASTER_SEED (right format)HTTP 500 — AES-GCM authentication fails; no data exposed
No SAPIX_MASTER_SEED, no SAPIX_KEYPAIR_SEED_HEXServer refuses to start
No SAPIX_MASTER_SEED, explicit keypair providedServer boots; encrypted records return HTTP 500 on read
SAPIX_MASTER_SEED in wrong format (not hex)Silently ignored; data stored as plaintext — use openssl rand -hex 32

Open Mode (Development)

When neither SAPIX_ROOT_KEY is set nor any scoped keys exist, SapixDB runs in open mode — all requests pass through without authentication. This is appropriate for local development on a private machine.

Startup warning in open mode
WARN SapixDB starting in OPEN MODE — no SAPIX_ROOT_KEY and no scoped API keys.
     Any caller that can reach this port can read and write data.
     Set SAPIX_ROOT_KEY then use POST /v1/admin/api-keys to issue scoped keys.
Never run open mode in productionSet SAPIX_ROOT_KEY and issue scoped keys before exposing SapixDB to any network traffic. Private networking (Railway internal, k8s ClusterIP) reduces blast radius but does not replace auth.

Layer 3 — Codios Contracts (Agent Operations)

Codios is optional but recommended for agent workloads. When SAPIX_CODIOS_URL is set, every write, read, query, cron execution, trigger fire, and Mutant operation calls Codios for a signed contract before executing. The contract includes:

  • The SapixDB agent ID performing the operation
  • The operation verb and resource
  • The caller identity (key label from Layer 2, or X-Sapix-Caller header)
  • A per-request unique ID (prevents replay)
  • A 5-second TTL

Codios denials return 403 Forbidden and emit a codios_denied A2A event. Set SAPIX_REQUIRE_CODIOS=true to make Codios mandatory — startup fails ifSAPIX_CODIOS_URL is absent.

Codios contract payload (sent by SapixDB internally)
{
  "agent_id": "sapixdb-primary",
  "operation": "write",
  "resource": "agents/orders/records",
  "caller_identity": "agent-database-boboyka-prod",  // from API key label
  "request_id": "req_a3f9c2e1",
  "timestamp": 1748304512000,
  "ttl_ms": 5000
}

Production Quick Start

1. Set root key on SapixDB
SAPIX_ROOT_KEY=$(openssl rand -hex 32)
# Store this in your password manager — it's your master credential
2. Issue a scoped key for each service
BASE=https://<sapixdb-url>

# For agent-database
curl -s -X POST $BASE/v1/admin/api-keys \
  -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "agent-database-prod",
    "scopes": ["write:agents/boboyka::*", "read:agents/boboyka::*"],
    "caller_id": "<your Codios agent ID>"
  }' | jq '{key_id, key}'  # save the key value

# For Control Plane (admin GUI — server-side only, never in browser)
curl -s -X POST $BASE/v1/admin/api-keys \
  -H "Authorization: Bearer $SAPIX_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "control-plane-prod", "scopes": ["admin:*"]}' | jq '{key_id, key}'
3. Set the returned keys on your services
# Each service gets its own key in SAPIX_API_KEY env var
# agent-database Railway env:
SAPIX_API_KEY=sapix_sk_<key from step 2>

# Control Plane Vercel env (server-side):
SAPIX_API_KEY=sapix_sk_<control-plane key from step 2>
That's it — no external auth service, no JWT library, no SSOSapixDB handles authentication itself. Your services authenticate with scoped keys. Codios adds the per-operation audit layer on top for compliance workloads. No Supabase, no Auth0, no dependency.