Skip to main content

Enable Auth

App Auth turns your app into a conformant OpenID Connect (OIDC) issuer without you running an identity service. The control plane is configuration only: it holds the issuer URL, signing-key references, branding, and status. Every end-user identity row (users, sessions, MFA secrets, refresh tokens, audit log) is written to the PostgreSQL database you attach, inside a managed _mdb_auth schema. Your data never leaves your database.

How the flow fits together

App Auth · managed OIDC, your database
TOKEN Issuer signed the JWT · App validated it against the JWKS
End Userbrowserlogin →OIDC Issuerauth-<id>⇢ verify _mdb_authYour PostgreSQL_mdb_auth← JWTYour Appvalidates
App / token (JWT)OIDC issuerYour PostgreSQL (_mdb_auth)Social login (optional)Control planeconfig only (dotted)

The diagram traces what happens once auth is Active: your app redirects the end user to the hosted issuer with an authorization code request and a PKCE challenge, the user authenticates on the hosted login page (magic link, or Google / GitHub), the issuer verifies the user against _mdb_auth in your own PostgreSQL, signs an ID and access token (JWT, RS256, with the per-app issuer key), and returns it to your app, which validates the token against the issuer's JWKS. The dotted edge to the control plane is the sovereignty boundary: the platform stores configuration only and never sees end-user identity rows.

The rest of this page is the control-plane side of that picture: provisioning the issuer, the _mdb_auth schema it manages in your database, and the lifecycle operations (key rotation, session revocation, erasure).

Prerequisites

Before enabling App Auth:

  • Your app must be in Running status.
  • Your app must have at least one PostgreSQL database attached and that attachment must be healthy.
  • You need an SMTP server you control. Magic-link delivery is mandatory; there is no platform relay and no silent fallback. SMTP credentials are accepted once, written to the platform secret store, and never returned by any API response.

The PostgreSQL attachment is where the _mdb_auth schema will be created. All end-user identity data (users, sessions, tokens, MFA secrets, audit log) will live there. Choose the attachment that should hold your users.

Find your attachment ID

Get the list of attachments for your app and note the id of the PostgreSQL attachment you want to use:

curl -u "$USER:$PASS" https://api.foundrydb.com/app-services/$APP_ID

The response includes an attachments array. Copy the id of the PostgreSQL entry.

Enable auth

curl -u "$USER:$PASS" -X POST \
https://api.foundrydb.com/app-services/$APP_ID/auth/enable \
-H "Content-Type: application/json" \
-d '{
"attachment_id": "'"$PG_ATTACHMENT_ID"'",
"issuer_domain_choice": "fallback",
"smtp": {
"host": "smtp.example.com",
"port": 587,
"username": "login@example.com",
"password": "smtp-password",
"from_address": "login@example.com",
"from_name": "Acme"
},
"theme": {
"display_name": "Acme",
"brand_color": "#4F46E5",
"logo_url": "https://acme.com/logo.svg"
}
}'

The theme object is optional. If omitted, the login page renders with generic FoundryDB branding.

The issuer_domain_choice is fixed at enable time because the issuer host becomes the iss claim baked into every JWT and the origin of the OIDC discovery document. Changing it later would invalidate tokens already in flight and break clients that cached discovery, so the choice cannot be edited in place. To change it, disable and re-enable auth.

ValueIssuer host
fallbackauth-<id>.foundrydb.com (platform-managed subdomain)
customYour own domain (requires a custom domain configured on the app)

Pick custom only when you have already attached and verified a custom domain on the app. A first-party issuer host (one your users recognize) reduces phishing surface and keeps the OAuth redirect URIs you register at Google and GitHub stable. The fallback subdomain works immediately and is a fine default for early integration.

A 201 Created response contains the auth configuration and signing key records:

{
"auth": {
"id": "...",
"app_service_id": "...",
"database_service_id": "...",
"attachment_id": "...",
"issuer_url": "https://auth-1a2b3c4d.foundrydb.com",
"fallback_domain": "auth-1a2b3c4d.foundrydb.com",
"status": "ProvisioningSchema",
"schema_version_applied": null,
"theme": { "display_name": "Acme", "brand_color": "#4F46E5", "logo_url": "..." },
"idp_providers": []
},
"signing_keys": [
{ "kid": "...", "algorithm": "RS256", "status": "active" }
]
}

Provisioning is asynchronous. Poll GET /app-services/$APP_ID/auth until status is Active. The issuer_url field contains the URL you need for OIDC integration.

Check auth status

curl -u "$USER:$PASS" https://api.foundrydb.com/app-services/$APP_ID/auth

When status is Failed, the response includes a failure_reason string describing what went wrong.

The _mdb_auth schema

Once auth reaches Active, your PostgreSQL database contains a _mdb_auth schema managed by the platform. It holds:

  • users: one row per registered end user, with UUID subject identifier and email
  • identities: identity rows linking users to their login method (magic link, Google, GitHub)
  • sessions: active browser sessions
  • refresh_tokens: long-lived tokens for session renewal
  • mfa_totp: encrypted TOTP secrets for users who have enrolled two-factor
  • mfa_recovery_codes: one-time recovery codes
  • login_tokens: short-lived magic-link tokens
  • oauth_codes: authorization codes in-flight during the OIDC flow
  • events: audit log rows

The schema is applied and migrated by the issuer, not by you. The status field moves through ProvisioningSchema while the migrations run; schema_version_applied reports the version once they complete. Because the control plane is config only, schema upgrades ship with the issuer: re-enabling auth after a platform update applies any pending migrations transparently.

Your application code can read from _mdb_auth using a read-only database user. Writes are locked to the low-privilege mdb_auth role owned by the platform. Do not grant write access to _mdb_auth to application users; the schema is managed by the issuer and the platform only. The cleanest setup is a dedicated app role with USAGE on the schema and SELECT on its tables, nothing more. This keeps identity data tamper-resistant: even a compromised application user cannot forge a session row or alter the audit log.

Rotate the JWT signing key

The signing key is a per-app RSA keypair held in the platform secret store. Key material is never returned by any API response.

Rotation is dual-key: when you rotate, the newly minted key is published in the issuer's JWKS alongside the outgoing key. Tokens signed by the previous key keep validating until that key retires. Live sessions do not see any disruption.

curl -u "$USER:$PASS" -X POST \
https://api.foundrydb.com/app-services/$APP_ID/auth/rotate-key

The response contains the new signing key record (not the key material):

{
"signing_key": {
"kid": "new-key-id",
"algorithm": "RS256",
"status": "active",
"activated_at": "2026-06-16T10:00:00Z"
}
}

The previous key enters retiring status and stays in the JWKS until it reaches retired.

Rotate signing keys regularly and whenever you suspect the key material may have been compromised.

Disable auth

Disabling auth tears down the managed issuer and sets the configuration to Disabled. It does not touch the _mdb_auth schema or your end-user data. The identity rows, sessions, and audit log remain in your database unchanged.

curl -u "$USER:$PASS" -X POST \
https://api.foundrydb.com/app-services/$APP_ID/auth/disable

You can re-enable auth on the same app. The platform will provision a new issuer and apply any pending schema migrations.

Revoke a session

To force-sign-out a specific user session, post to the revoke endpoint with the session ID from _mdb_auth.sessions:

curl -u "$USER:$PASS" -X POST \
https://api.foundrydb.com/app-services/$APP_ID/auth/sessions/$SESSION_ID/revoke

The revocation is dispatched asynchronously to the backing database's primary VM. The response returns a task ID:

{ "task_id": "..." }

Erase an end user (GDPR right to erasure)

To delete an end user and all their identity data from your database under GDPR Art. 17, address them by email or by their subject UUID:

# By email
curl -u "$USER:$PASS" -X POST \
https://api.foundrydb.com/app-services/$APP_ID/auth/users/delete \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'

# By subject UUID
curl -u "$USER:$PASS" -X POST \
https://api.foundrydb.com/app-services/$APP_ID/auth/users/delete \
-H "Content-Type: application/json" \
-d '{"user_id": "00000000-0000-0000-0000-000000000000"}'

You can also use the path-based form:

# By email (path contains @)
curl -u "$USER:$PASS" -X DELETE \
"https://api.foundrydb.com/app-services/$APP_ID/auth/users/user@example.com"

# By subject UUID
curl -u "$USER:$PASS" -X DELETE \
"https://api.foundrydb.com/app-services/$APP_ID/auth/users/00000000-0000-0000-0000-000000000000"

The erasure cascades across all identity tables: users, identities, sessions, refresh_tokens, mfa_totp, mfa_recovery_codes, login_tokens, and oauth_codes. Audit rows in _mdb_auth.events are additionally scrubbed, since their JSONB metadata can carry personal data.

Erasing a user that does not exist is a no-op and returns 202 Accepted. The platform emits an auth.user.deleted webhook event referencing the user by UUID only; when the request addressed the user by email, the event uses a keyed HMAC digest, not the plaintext email.

The erasure is dispatched asynchronously and returns a task ID.

Next steps