SapixDBSapixDB/Docs
Home
Add-on

Auth Add-on

Built-in user authentication — registration, login, magic links, and JWT issuance — all self-hosted inside your SapixDB agent. No Supabase. No Auth0. No data leaving your server.

🔑 One env var to activate: SAPIX_AUTH_ENABLED=true
Included in the enterprise build. No recompilation required.

Enable

docker-compose.yml / Railway env
SAPIX_AUTH_ENABLED=true
SAPIX_AUTH_JWT_EXPIRY_SECS=3600          # optional — default 1 hour
SAPIX_AUTH_MAGIC_LINK_EXPIRY_SECS=900   # optional — default 15 min

The auth add-on stores users in the agent's GraphIndex meta column family and writes every authentication event to the strand as a signed nucleotide. There is no separate auth database.

Register & Login

TypeScript
// Register — creates a new user and returns a JWT
const res = await fetch("http://localhost:7475/v1/auth/register", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "[email protected]", password: "s3cr3t!" }),
});
const { user_id, email, token } = await res.json();

// Login — verify password, return JWT
const login = await fetch("http://localhost:7475/v1/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "[email protected]", password: "s3cr3t!" }),
});
const { token } = await login.json();

// All subsequent requests: attach the JWT
fetch("http://localhost:7475/v1/auth/me", {
  headers: { Authorization: `Bearer ${token}` },
});
Python
import httpx

client = httpx.AsyncClient(base_url="http://localhost:7475")

# Register
r = await client.post("/v1/auth/register",
    json={"email": "[email protected]", "password": "s3cr3t!"})
token = r.json()["token"]

# Verify current user
me = await client.get("/v1/auth/me",
    headers={"Authorization": f"Bearer {token}"})
print(me.json())  # {"user_id": "usr_...", "email": "[email protected]"}

Magic links provide passwordless sign-in. A one-time token is generated and sent via the Mail add-on if configured, or returned directly for testing.

TypeScript
// Step 1: Request a magic link
// If SAPIX_MAIL_ENABLED=true → token is emailed automatically
// If mail not configured → token is returned in the response for testing
const req = await fetch("http://localhost:7475/v1/auth/magic-link/send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "[email protected]" }),
});
const { sent, token: testToken } = await req.json();
// sent: true  → email delivered  (token not shown)
// sent: false → token: "a3f9..."  (testing mode)

// Step 2: Verify the token from the link (single-use, deleted on verify)
const verify = await fetch("http://localhost:7475/v1/auth/magic-link/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ token: tokenFromEmail }),
});
const { token } = await verify.json(); // JWT
How magic links work: A 32-byte random token is generated. Only its SHA-256 hash is stored in the agent — the raw token only travels via email or the API response. On verification the hash is recomputed, the expiry is checked, and the stored entry is deleted. One-time, tamper-evident, secure.

JWT Format

JWTs use HS256(HMAC-SHA256). The signing secret is derived deterministically from the agent's own Ed25519 keypair seed using HKDF — no additional secret to manage. The same agent always produces the same JWT secret and it survives process restarts.

JWT claims
{
  "sub":   "usr_a3f9b2c1...",   // user_id
  "email": "[email protected]",
  "iat":   1749427200,          // issued at (Unix seconds)
  "exp":   1749430800           // expires at (iat + SAPIX_AUTH_JWT_EXPIRY_SECS)
}

The secret derivation: HKDF(keypair_seed, salt="sapixdb-auth-jwt", info="jwt-hmac-secret"). Because it is symmetric, any service holding the same derived secret can verify tokens. For multi-service architectures, derive the shared secret once and distribute it as a scoped credential.

HTTP API Reference

MethodPathAuthDescription
POST/v1/auth/registerRegister new user, returns JWT
POST/v1/auth/loginPassword login, returns JWT
POST/v1/auth/magic-link/sendSend or return magic link token
POST/v1/auth/magic-link/verifyExchange token for JWT
GET/v1/auth/meBearer JWTReturn current user
GET/v1/auth/usersRoot keyList all users
DELETE/v1/auth/users/:emailRoot keyDelete user (strand tombstone written)
GET/v1/auth/jwksJWT algorithm metadata

Strand audit records

Every auth event is written to the strand as a signed nucleotide — the same cryptographic guarantee as any other record. This makes auth history tamper-evident and exportable via GET /v1/strand/export for compliance audits.

Strand record typeTrigger
auth/registerNew user registered
auth/loginSuccessful password login
auth/magic_link_sentMagic link requested
auth/user_deletedUser deleted via admin endpoint

Configuration

Environment variableDefaultDescription
SAPIX_AUTH_ENABLEDfalseEnable the auth add-on
SAPIX_AUTH_JWT_EXPIRY_SECS3600JWT lifetime in seconds (1 hour)
SAPIX_AUTH_MAGIC_LINK_EXPIRY_SECS900Magic link token lifetime (15 min)

Known Limitations

  • No token revocation. JWTs are stateless — a stolen token is valid until expiry. Keep SAPIX_AUTH_JWT_EXPIRY_SECS short.
  • No refresh tokens. Users must re-authenticate when the JWT expires.
  • Single agent scope. Users registered on one agent are not shared with peer mesh agents. Run a dedicated auth agent if auth must span a cluster.
  • No OAuth / OIDC / SAML. First-party auth only.
  • No rate limiting. Implement login rate limiting at the reverse proxy layer.
  • Magic links are single-use. If the link is lost, request a new one.
→ Mail Add-on→ Chat Add-on→ API Keys & Security