Authenticate users, issue tokens, and manage sessions — deployed inside your network, owned by you.
Each project gets its own deployment, its own signing keys, and its own auth schema within the project's existing Postgres database. No shared user namespace. Forking a project's database volume forks its auth state automatically — users, sessions, signing keys, all of it.
- Sessions — opaque bearer tokens validated in one SQL query; JWT exchange opt-in for stateless edge verification
- Auth methods — password, magic links, TOTP (2FA with recovery codes), passkeys (WebAuthn/FIDO2), OAuth (GitHub, Google, Apple, Microsoft, generic OIDC)
- Multi-email — users can attach and verify multiple email addresses
- API keys — server-to-server authentication with
key_tokens - Organizations — create orgs, manage members and roles, send and accept invitations
- Authorization — opt-in Zanzibar-style relation engine; define schemas, write relation tuples, check permissions in one query
- Stateless — no in-process state; scale to zero and restart cleanly against the existing DB
The service runs migrations automatically on startup. To migrate only:
beyond-auth migrate --database-url "postgres://..."mise run build:release
./target/release/beyond-auth serve \
--database-url "postgres://user:pass@host:5432/dbname" \
--signing-key-encryption-key "$(openssl rand -base64 32)" \
--admin-secret "$(openssl rand -hex 32)" \
--webauthn-rp-id "example.com" \
--webauthn-rp-origin "https://example.com"| Flag / Env | Default | Description |
|---|---|---|
--mmds-endpoint / MMDS_ENDPOINT |
— | Firecracker MMDS endpoint (e.g. http://169.254.169.254). When set, secrets (DATABASE_URL, SIGNING_KEY_ENCRYPTION_KEY, ADMIN_SECRET) are fetched from MMDS instead of env vars; env vars remain as per-key fallbacks. |
--database-url / DATABASE_URL |
— | Postgres connection string. The service operates within the auth schema. |
--address / ADDRESS |
0.0.0.0:8080 |
Bind address |
--signing-key-encryption-key / SIGNING_KEY_ENCRYPTION_KEY |
— | Base64url-encoded 32-byte AES-256-GCM key for signing key encryption at rest |
--signing-key-encryption-key-old / SIGNING_KEY_ENCRYPTION_KEY_OLD |
— | Comma-separated old KEK values for zero-downtime key rotation |
--admin-secret / ADMIN_SECRET |
— | Bearer token required for admin endpoints |
--webauthn-rp-id / WEBAUTHN_RP_ID |
— | WebAuthn relying party ID (e.g. example.com) |
--webauthn-rp-origin / WEBAUTHN_RP_ORIGIN |
— | WebAuthn origin (e.g. https://example.com) |
--public-url / PUBLIC_URL |
— | Public base URL for OAuth callbacks |
--oauth-allowed-redirect-origins / OAUTH_ALLOWED_REDIRECT_ORIGINS |
— | Comma-separated origins allowed as OAuth redirect targets |
--authz-cache-size / AUTHZ_CACHE_SIZE |
100000 |
Max cached authz check entries |
--authz-cache-ttl-secs / AUTHZ_CACHE_TTL_SECS |
1800 |
Authz cache TTL in seconds |
--log-level / LOG_LEVEL |
info |
Log verbosity |
--otlp-enabled / OTLP_ENABLED |
false |
Enable OpenTelemetry export |
--otlp-endpoint / OTLP_ENDPOINT |
http://localhost:4317 |
OTLP collector endpoint |
Set ENVIRONMENT=development for human-readable logs.
openssl rand -base64 32This key protects signing key material at rest in Postgres. Keep it out of the database. Loss of this key requires key rotation; compromise of the DB alone is not sufficient to forge JWTs.
The service connects to an existing Postgres database and operates entirely within the auth schema. It does not touch other schemas. Run it against your app's existing database, or a dedicated one — either works.
The auth schema is part of the public contract. Migrations are additive-only; no column is ever removed or renamed in a way that breaks existing data.
| Token | Format | Used for | Revoke |
|---|---|---|---|
| Session | session_{id}_{secret} |
End-user sessions | Delete row |
| Refresh | rt_{id}_{secret} |
Long-lived SDK credential | Soft-delete row |
| API key | key_{id}_{secret} |
Server-to-server | Soft-delete row |
| Magic link | ml_{id}_{secret} |
Passwordless login | Expiry |
| Password reset | pwr_{id}_{secret} |
Password recovery | Expiry |
| Email verification | ev_{id}_{secret} |
Email confirmation | Expiry |
| Invitation | inv_{id}_{secret} |
Org invitations | Expiry |
| JWT (EdDSA, opt-in) | Standard JWT | Stateless edge verification | Key rotation |
Wire format: {prefix}_{uuid_v7_hex}_{32_random_bytes_b64url}. The DB stores only SHA-256(secret) — the raw secret is never persisted.
Configure providers via the admin API (PUT /v1/admin/oauth-providers): GitHub, Google, Apple, Microsoft, and generic OpenID Connect. Provider credentials are encrypted at rest.
Projects that opt in to JWT mode publish their public keys at /v1/jwks.json. Tokens are signed with Ed25519 (EdDSA). Verify against the JWKS:
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://auth.yourproject.beyond.dev/v1/jwks.json"),
);
const { payload } = await jwtVerify(token, JWKS);JWKS responses are cache-controlled (public, max-age=3600). Key rotation is additive — old keys remain in the set until all valid tokens issued under them have expired.
The authz engine is off unless you define a schema. No rows in authz_relations, no CPU cost.
Define resource types, roles, and permissions as JSON:
{
"version": 1,
"resources": [
{
"name": "document",
"roles": ["owner", "viewer"],
"permissions": {
"edit": ["owner"],
"view": ["owner", "viewer"]
}
}
]
}Check a single permission (defaults to the current session user):
GET /v1/authz/decisions?resource_type=document&resource_id=123&permission=edit
GET /v1/authz/decisions?resource_type=document&resource_id=123&permission=edit&user=456
Batch-check multiple permissions in one request:
POST /v1/authz/checks
{
"checks": [
{ "resource_type": "document", "resource_id": "123", "permission": "edit" },
{ "resource_type": "document", "resource_id": "123", "permission": "view", "user": "789" }
]
}Other authz endpoints: POST/DELETE/PATCH /v1/authz/relations (write tuples), GET/PUT /v1/authz/schema, GET /v1/authz/subjects, GET /v1/authz/objects, GET /v1/authz/traces (decision audit).
npm install @beyond.dev/authimport { createAuthzClient, createSessionVerifier } from "@beyond.dev/auth";
// Verify an opaque session token
const verifier = createSessionVerifier({
baseUrl: "https://auth.yourproject.beyond.dev",
});
const session = await verifier.verify(bearerToken); // null if invalid/expired
// Check a permission
const authz = createAuthzClient({
baseUrl: "https://auth.yourproject.beyond.dev",
adminSecret: process.env.AUTH_ADMIN_SECRET!,
});
const allowed = await authz.check({
resource: "document",
id: "123",
permission: "edit",
subject: session?.userId,
});Next.js integration ships in @beyond.dev/auth/next: middleware, RSC helpers, and cookie utilities.
mise run format # format all source filesIntegration tests use testcontainers and spin up a real Postgres instance.
To move off Beyond (or any managed host) to your own infrastructure:
- Export your database:
pg_dump --schema=auth -f auth.sql "$DATABASE_URL" - Stand up
beyond-authanywhere that can reach a Postgres instance. pg_restorethe auth schema into your target database.- Set
DATABASE_URL,SIGNING_KEY_ENCRYPTION_KEY,ADMIN_SECRET, and WebAuthn config, then start the service.
Existing sessions and JWTs remain valid. The same binary, the same schema, the same keys.
AGPLv3. Self-host for any purpose, including commercial use. If you offer this software as a service, you must release your modifications under AGPLv3.