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.
SAPIX_AUTH_ENABLED=trueIncluded in the
enterprise build. No recompilation required.Enable
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
// 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}` },
});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 (Passwordless)
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.
// 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(); // JWTJWT 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.
{
"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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /v1/auth/register | — | Register new user, returns JWT |
| POST | /v1/auth/login | — | Password login, returns JWT |
| POST | /v1/auth/magic-link/send | — | Send or return magic link token |
| POST | /v1/auth/magic-link/verify | — | Exchange token for JWT |
| GET | /v1/auth/me | Bearer JWT | Return current user |
| GET | /v1/auth/users | Root key | List all users |
| DELETE | /v1/auth/users/:email | Root key | Delete user (strand tombstone written) |
| GET | /v1/auth/jwks | — | JWT 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 type | Trigger |
|---|---|
| auth/register | New user registered |
| auth/login | Successful password login |
| auth/magic_link_sent | Magic link requested |
| auth/user_deleted | User deleted via admin endpoint |
Configuration
| Environment variable | Default | Description |
|---|---|---|
| SAPIX_AUTH_ENABLED | false | Enable the auth add-on |
| SAPIX_AUTH_JWT_EXPIRY_SECS | 3600 | JWT lifetime in seconds (1 hour) |
| SAPIX_AUTH_MAGIC_LINK_EXPIRY_SECS | 900 | Magic 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_SECSshort. - 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.