SapixDBSapixDB/Docs
Home

Security

Security

✓ Shipped

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.

ℹ API keys authenticate services, not end usersSapixDB has no "client-safe, publishable" key type — unlike, say, Supabase's anon key, every scoped key here is meant to be held by a trusted backend, never embedded in a browser or mobile app. If your application needs to know which end user is calling, use the Auth add-on to issue that user a JWT, and have your backend forward it alongside its own service key. This is also what makes row-level policies meaningful: a scoped key presenting no JWT is treated as a trusted service caller and bypasses row-policy filtering, by design — safe only because keys are never end-user-facing.

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
admin:controlView raw operator/audit views — Control Plane dashboard, strand records dump, cluster/replica management. A plain read:* key cannot see these even though it can read everything else.
admin:hipaaPHI field classification (read/write) and the PHI access audit log
admin:soxDesignate/remove a financial agent's SOX tier and the SOX audit report
admin:clusterRegister/remove replica nodes, switch CP/AP consistency (Distributed Mode)
admin:organismsCreate/delete organisms and manage their member agents
admin:graphCreate/list/delete graph edges directly (not the row-policy-filtered `refs` routes)
admin:publicationsPublication + subscriber-slot CRUD (a publication can cover every agent on the instance)
admin:connectorsConnector CRUD and inbound REST-bridge pushes (connector configs can hold webhook secrets)
admin:blobsChange the instance-wide blob upload policy, or delete a blob outright
admin:epigeneticsSwitch the instance's epigenetic profile (read/write/audit-on-read posture)
admin:commonsOpt this instance in or out of anonymized Commons data sharing
ℹ 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.
⚠ admin:<resource> gates raw audit/debug views and cluster-wide configA few classes of endpoint require an explicit admin:<resource> scope even if the key already holds read:* or write:*: (1) chronological dumps across everycaller by design — the raw strand-records dump, the Control Plane operator views, the HIPAA/SOX audit reports — which row policies can't meaningfully filter without becoming misleading; and (2) cluster-wide or instance-wide configuration that affects every tenant at once — replica/consistency management, organisms, publications, connectors, the blob upload policy, the epigenetic profile, Commons opt-in. See Row-Level Policies for the full per-endpoint breakdown and reasoning.
ℹ A flat resource scope doesn't imply access to any agent named in the bodyA handful of endpoints — creating a scheduled cron (POST /v1/crons), defining a field index (POST /v1/indexes), subscribing to a publication stream — classify to a flat resource (crons, indexes, publications) that's decoupled from the specific agent named in the request body or query. Holding write:crons is not enough on its own: SapixDB additionally checks read:agents/<source> / write:agents/<output> for the exact agent the request targets, the same way an ordinary per-tenant request is checked. A key scoped only to write:crons cannot create a cron that reads an agent it has no relationship to.

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.

Global IP Rate Limiting

In addition to the optional per-key limit above, SapixDB applies a global IP-based token bucket as the outermost request layer — before API-key auth, before license checks, and before any route handler. This protects open-to-internet endpoints (auth registration, magic-link, OAuth, passkeys) that deliberately accept unauthenticated requests.

Env varDefaultApplies to
SAPIX_RATE_LIMIT_RPS100 req/sAll routes
SAPIX_RATE_LIMIT_BURST200 tokensMax bucket capacity — new IPs start at rps tokens (not burst) to limit VPN-rotating flood attacks
SAPIX_AUTH_RATE_LIMIT_RPS10 req/s/v1/auth/* routes only (stricter)
SAPIX_AUTH_RATE_LIMIT_BURST20 tokensMax auth bucket capacity — new IPs start at auth_rps tokens, not burst

Blocked requests receive 429 Too Many Requests with a Retry-After: 1 header. Rate-limit violations are recorded by the Alabay security watchdog (warn at any violation, alert at ≥ 50 per minute) and surface in the Control Plane Security Findings tab.

ℹ Auth routes get a stricter limit by design/v1/auth/paths accept requests without an API key — that's how end-user registration and login work. The per-email lockout (5 failures → 15-min block) handles single-account brute force. The stricter IP limit (10 rps default) catches cross-email enumeration where an attacker spreads attempts across many target addresses to stay under the per-email threshold.

The IP limit is bypassed for requests with no identifiable source IP (no X-Forwarded-For or X-Real-IP header), which covers Docker health probes and local development without a reverse proxy. In production, set these headers on your load balancer or Cloudflare.

This is not a replacement for a WAF or CDN-level DDoS protection. For traffic at volumetric scale, put Cloudflare or a similar CDN in front.

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.