Auth
Built-in auth for your deployed app: password, magic link, passkeys, social sign-in, two-factor and SAML, with your users in your own Postgres.
On this page
PrimDB Auth signs in the end-users of the app you deploy. You turn it on per project, and from then on sign-up, sign-in, sessions, organizations and enterprise SSO run on PrimDB, while the user records themselves sit in that project’s own Postgres. There is no separate auth vendor to hold an account with, and no user directory living outside your database.
Where your users live
Enabling auth bootstraps a set of auth_* tables in the project’s Postgres. They are ordinary tables in your database: you can read them, join them against your own schema, and they go into your backups with everything else.
| Table | Holds |
|---|---|
auth_users | The end-user accounts. |
auth_sessions | Live sessions, one per sign-in. |
auth_passkeys | Registered WebAuthn credentials. |
auth_user_mfa | Authenticator-app two-factor enrolment for a user. |
auth_oauth_accounts | Social provider accounts linked to a user. |
auth_orgs | The organizations your users create. |
auth_org_members | Who belongs to an org, and with what role. |
auth_org_invitations | Invitations sent but not yet accepted. |
auth_saml_config | The project’s SAML connection. |
Honest note. Disabling auth removes the control-plane config and the injected env, and leaves those tables in place: the rows in them are your data. Re-enabling reuses them.
Turning it on
Auth needs somewhere to put those tables, so the project must already have a ready Postgres service. With that in place, enable it from the dashboard, or let an agent call the enable_auth MCP tool. Enabling is idempotent: it creates the tables, mints the project’s signing key, and injects PRIMDB_PROJECT_ID and PRIMDB_AUTH_URL into your app’s environment.
Then install the backend SDK. @primdb/auth-node is published on npm at 0.2.0 and is framework-agnostic: Hono, Express, Fastify, Next.js route handlers.
import { createPrimDBAuth } from '@primdb/auth-node';
export const auth = createPrimDBAuth({
projectId: process.env.PRIMDB_PROJECT_ID!, // injected when you enable auth
baseUrl: process.env.PRIMDB_AUTH_URL, // injected too
});Sign-in methods
Six ways in. Which of them a project offers is a setting you change from the dashboard or over MCP, so you can ship one method now and add another later without touching the integration.
| Method | How you call it | Reach for it when |
|---|---|---|
| Email + password | signUp, then signIn.emailPassword | You want the familiar default that works on every device. |
| Magic link | signIn.magicLink mails the link, verifyMagicLink redeems the token | You would rather not store passwords at all. |
| Passkey (WebAuthn) | POST /passkey/register/start and /finish, then /signin/passkey/challenge and /signin/passkey/verify | Phishing resistance matters more than breadth of device support. |
| OAuth social | signIn.oauth returns an authorize URL, completeOAuth redeems the code | Your users already have an account with a provider you configured. |
| SAML | signIn.saml returns the IdP redirect, completeOAuth finishes it | A customer wants their whole company signing in through their own IdP. |
| Authenticator app (2FA) | setupMfa, enableMfa, then verifyMfa at sign-in | You need a second factor on top of any of the above. |
The two redirect-based methods share one shape. signIn.oauth and signIn.saml each take a redirectTo that has to be an origin registered for the project, and it is allowlist-checked, so the flow cannot be bent into an open redirect. The user comes back with a single-use primdb_oauth_code on the URL, and completeOAuth swaps that for a session.
Any interactive sign-in can return an MFA challenge instead of a session, so branch on it once and every method is covered.
import { isMfaChallenge } from '@primdb/auth-node';
const result = await auth.signIn.emailPassword({ email, password });
if (isMfaChallenge(result)) {
// No session yet. Prompt for a TOTP code or a single-use recovery code.
const { session } = await auth.verifyMfa({ mfaToken: result.mfaToken, code });
return session;
}
return result.session;Sessions, and validating a request
A completed sign-in gives you a user and a session: a short-lived access token plus a refresh token. The access token is an ES256 JWT signed with the project’s own keypair, and your backend verifies it against the matching public key. Nothing secret has to be copied into your app’s environment.
validateRequest accepts a Fetch Request, a Node request, or a plain { authorization } object, and returns the session or null. It pulls the project’s public key from GET /auth/v1/jwks/:projectId the first time it runs, then verifies locally, so the hot path costs no network call.
// Hono
app.get('/me', async (c) => {
const session = await auth.validateRequest(c.req.raw);
if (!session) return c.json({ error: 'unauthorized' }, 401);
return c.json({ userId: session.userId, orgId: session.orgId ?? null });
});
// Express
app.get('/me', async (req, res) => {
const session = await auth.validateRequest(req);
if (!session) return res.status(401).json({ error: 'unauthorized' });
res.json({ userId: session.userId });
});The rest of the session lifecycle is three calls. refreshSession trades a refresh token for a fresh session, signOut ends the current one, and signOut with all: true ends every session the user holds. From the admin side you can revoke one session or all of them, and rotating the project’s auth secret invalidates every live access token at once.
If your backend is not Node, verify the access token against that same JWKS endpoint with whatever JWT library your language already ships. That is the whole integration contract.
Organizations
Teams are built in, so a B2B app does not have to model them from scratch. auth.orgs.create makes an org with the caller as owner, invite mails a single-use accept link, and acceptInvite redeems it. members, setRole, removeMember, invitations and revokeInvitation cover the day-to-day; remove deletes the org.
auth.orgs.switch is the piece worth understanding. It returns a fresh session whose access token carries the active org, so validateRequest hands your route handler an org and a role next to the user id, with no extra lookup. Pass orgId: null to drop back to a personal session. Membership and role are re-checked server-side on every org call, so a stale claim in an old token never grants more than the user actually has.
SAML for your customers
App-level SAML lets a customer of yours sign in through their own identity provider. It needs the SSO add-on, a flat $30 a month, plus a connection you set from the dashboard or with the set_auth_saml MCP tool: the IdP entity id, its SSO URL, and its signing certificate. Your service-provider coordinates come from GET /auth/v1/saml/:projectId/metadata, assertions land on POST /auth/v1/saml/:projectId/acs, and sign-in starts at signIn.saml and finishes through the same completeOAuth exchange as social login.
Honest note. This is SAML for the end-users of the app you ship. Signing your own team in to PrimDB itself is a separate setting, covered in SSO / SAML.
Webhooks
Register an endpoint and PrimDB posts auth events to it, so your app can react to a sign-up or an account deletion without polling for it. Creating a webhook returns its signing secret exactly once, so store it then: only a hash is kept. An optional event filter narrows what gets delivered, the URL is checked against an SSRF guard whenever you set or change it, and you can rotate the secret or delete the endpoint later.
Running it day to day
Every operator action lives in the dashboard, and the same actions are MCP tools, which means an agent can do them for you:
- Enable or disable auth for a project, and update its settings.
- List end-users with search, filter and paging.
- Suspend a user, or delete one.
- Revoke a single session, or every session a user holds.
- Rotate the project’s auth secret.
- Configure OAuth providers and the SAML connection.
- Create, update, rotate and delete auth webhooks.
Suspending a user also revokes their live sessions, so they are locked out immediately rather than whenever their current token happens to expire.
Querying your users in SQL
Because the tables are yours, questions you would normally have to file a support ticket for are just SQL, and they join straight against your own schema.
-- Every org membership, most recently active people first
select o.name as org, m.role, u.email, u.last_sign_in_at
from auth_orgs o
join auth_org_members m on m.org_id = o.id
join auth_users u on u.id = m.user_id
where u.status = 'active'
order by u.last_sign_in_at desc nulls last;
-- Which social providers people actually use
select provider, count(*) as accounts
from auth_oauth_accounts
group by provider
order by accounts desc;The REST API
The SDK is a thin wrapper over a plain HTTP API, so a mobile client or a service written in another language can call it directly. The base path is /auth/v1 on the auth URL injected into your app, 42 routes in all, and every request names the project with an X-PrimDB-Project-Id header.
| Group | Routes |
|---|---|
| Sign up and sign in | POST /signup, /signin/password, /signin/magic-link, /signin/passkey/challenge, /signin/passkey/verify |
| Passkey registration | POST /passkey/register/start, /passkey/register/finish |
| OAuth | POST /oauth/:provider/start, GET /oauth/:provider/callback, POST /oauth/exchange |
| Two-factor | POST /mfa/setup, /mfa/enable, /mfa/verify, /mfa/disable, GET /mfa/status |
| Sessions | POST /session/refresh, /signout, /signout/all |
| Account | GET /user, PATCH /user, DELETE /user, POST /password/reset, /password/update, /verify-email |
| Organizations | GET /orgs, POST /orgs, DELETE /orgs/:orgId, POST /orgs/switch, GET /orgs/:orgId/members, PATCH + DELETE /orgs/:orgId/members/:userId, GET + POST /orgs/:orgId/invitations, DELETE /orgs/:orgId/invitations/:invitationId, POST /orgs/invitations/accept |
| SAML | POST /saml/start, POST /saml/:projectId/acs, GET /saml/:projectId/metadata |
| Service | GET /config, GET /health, GET /verify, GET /jwks/:projectId |
Honest note. What is not here yet. @primdb/auth-node is the only published SDK: a React package, a Python client and a Go client exist in the repo but are not on a registry, so you cannot install them today. The Node SDK carries no passkey helper either, because passkey registration and sign-in happen in the browser: drive those from the REST routes above. And all of this is auth for your app’s end-users, not for your own PrimDB login.