Identity and access management that lives in your codebase
Better IAM is an embeddable TypeScript platform for multi-tenant authentication, fine-grained authorization, access governance, and enterprise federation. It runs in your process, on your database, behind one typed API.
Node.js 22.12+ PostgreSQL, SQLite, or libSQL ESM with TypeScript types
Sign in to Acme
01 · Pipeline
Every call runs the same pipeline
Whether a call comes from a browser, a server action, the CLI, or a SCIM connector, it resolves its credential, re-validates inside a serialized transaction, is authorized, applies its change, and appends an audit event.
- Nothing skips it. A permission check, a revocation, or an audit rule in the pipeline cannot be bypassed by calling the API another way.
- No permission cache. Tokens, roles, and policies are checked on every use, so a revocation applies to the very next request.
- All or nothing. A change that breaks a rule rolls back whole, and nothing is emitted for it.
// The caller's session cookie or bearer token.
const credential = { headers: request.headers };
await iam.api.identities.invite(credential, {
tenantId,
email: 'alice@acme.test',
roleIds: [editorRoleId],
});- CredentialThe cookie resolves to an identity and a session.
- Re-validateInside the transaction: identity active, session live, MFA and network rules hold.
- Authorize
iam:identities:createoniam/{tenantId}is allowed. - ApplyThe invitation is written, then duty and invariant checks run.
- AuditAn event joins the tenant’s SHA-256 hash chain.
- Fan-outAfter commit: webhooks and subscribers.
Browser, server action, CLI, and SCIM calls all enter this same pipeline.
- 01 · Credential
Resolve the credential
The session cookie, bearer token, or API key becomes an identity and a session. When the call carries request headers, the client’s address comes from them too, so network rules judge the address that actually presented the credential.
- 02 · Re-validate
Re-validate inside the transaction
An administrator may have disabled the person or ended the session a moment ago. So before anything is used, the transaction re-reads the identity and session and checks every revocation condition again: status, expiry, tenant ancestry, idle timeout, MFA, and network rules.
- 03 · Authorize
Authorize the operation
Every provisioning operation is itself an action on a resource, evaluated like any product action against roles, policies, boundaries, and grant authorities. A denial commits only a deny audit event, and the caller receives a typed error.
ACCESS_DENIED · 403 - 04 · Apply
Apply the change
Plugin hooks run around the change itself, then separation-of-duties rules and access invariants are checked. Any failure rolls the whole transaction back, so a change that breaks a rule never becomes visible.
INVARIANT_VIOLATION · 409 - 05 · Audit
Append the audit event
One event joins the tenant’s hash chain in the same transaction. Each event includes the hash of the one before, so altering, reordering, or removing a record is detectable.
- 06 · Fan-out
Fan out after commit
Subscribers, plugin afterAudit hooks, and signed webhooks are queued in the transaction and dispatched once it commits. Nothing is emitted for a change that rolled back.
02 · Build
One typed API, from the database to the button
Configure one instance, enforce decisions on the server, and guard pages, routes, and UI in the framework you already use. Every call is typed end to end from that same instance.
- Typed end to end. Inputs, results, and errors come from one instance, on the server and in the browser client.
- Stable errors. Every failure carries one of 144 documented codes with its HTTP status.
- Your framework. Guards for Next.js, Nuxt, SvelteKit, React Router, and NestJS; middleware for Express, Hono, and Fastify.
Storage, the deployment secret, and the resource types your product protects.
import { betterIam } from 'better-iam';
import { postgresAdapter } from 'better-iam/adapter-postgres';
export const iam = betterIam({
database: postgresAdapter({ connectionString: process.env.DATABASE_URL! }),
secret: process.env.BETTER_IAM_SECRET!,
baseURL: 'https://identity.example.com',
permissions: {
resourceTypes: {
invoice: {
actions: ['invoices:read', 'invoices:approve'],
attributes: { amount: 'number' },
},
},
},
});03 · Sign-in
Every sign-in method, one session model
Passwords with policy and history, passkeys, magic links, email and SMS codes, TOTP with recovery codes, and federated sign-in all end in the same kind of session, checked against each organization's own rules.
- Per-organization policy. Allowed methods, required MFA, session lifetime, idle timeout, and IP allowlists, set by each tenant.
- Trusted devices. A remembered device can stand in for the second factor for as many days as the tenant allows.
- Visible to policies. How someone signed in reaches every decision as principal.authMethod and principal.mfa.
Method
Checks
- Method allowedThe tenant policy is checked before any credential
- Credential verifiedRate-limited per tenant, and optionally per IP
- Second factorWhen required: TOTP, a passkey, or an emailed code
- Network allowedIP allowlists and blocked networks, at issuance
Session
Lifetime, idle timeout, and session count follow the tenant's policy.
principal.authMethod and principal.mfa, so a rule can demand a passkey for sensitive actions.04 · Decisions
Decisions you can explain
Roles, versioned JSON policies, and boundaries meet in one evaluator. Flip the switches: this is the real evaluator from @better-iam/core, running in your browser.
- Deny wins. A matching deny statement overrides every allow.
- Grants are a union. One matching allow from any role or policy is enough.
- Boundaries only take away. Each boundary is an independent ceiling. None of them ever grants.
- 21 condition operators. With variables such as ${principal.id} and request context such as the source IP.
Request context
What the evaluator receives
- principal.id
- 'usr_alice'
- principal.mfa
- false
- resource.ownerId
- 'usr_alice'
- request.sourceIp
- '10.4.2.7'
Documents
invoices:approve on invoice/*
invoices:* on invoice/*
invoices:* on invoice/*
05 · Tenancy
Tenants all the way down
Mirror how your customers are organized: the platform at the root, organizations below it, and projects or workspaces below those. Every level is a full tenant with its own members and access model.
- Isolated directories. People belong to one tenant, and membership in a parent grants nothing in a child.
- Your hierarchy. Define your own tenant types and depth, eight levels by default.
- Suspension cascades. Suspend a tenant and its whole subtree stops, sessions included.
- Domains and limits. Verified domains route people to their organization; plan limits cap usage.
06 · Elevation
Privileged access that expires on its own
Make powerful roles eligible instead of standing. People activate them for a bounded time, with a reason, MFA, and approval when you require it, and the access ends by itself.
- Two-person control. An approver group or the requester’s manager decides. Nobody approves their own request.
- Access packages. Bundle roles and groups into packages people request, or that rules assign automatically.
- Configuration as code. Export, plan, and apply roles, policies, and bindings, with drift detection in CI.
- Eligible
- Request
- Approve
- Active
- Expired
Alice is eligible for Production admin
An eligible binding grants nothing until it is activated.
Can Alice run deploy:production?time →
07 · Audit
A record nobody can quietly rewrite
Every operation, allowed or denied, appends an event to its tenant's SHA-256 hash chain. Try to change history below: the real verifier from @better-iam/core catches every attempt.
- Verifiable anywhere. verifyAuditChain runs on exported events, outside the server that wrote them.
- Export and archive. JSON Lines export, and continuous archiving to storage you control.
- Signed webhooks. After commit, events fan out to signed webhooks and in-process subscribers.
Every event stores the previous event’s hash, so the four form one chain.
- sequence 1
binding:activation-requested
actor usr_alice
prev…
hash…
- sequence 2
binding:activation-approved
actor usr_priya
prev…
hash…
- sequence 3
binding:deactivate
actor usr_alice
prev…
hash…
- sequence 4
identity:offboard
actor usr_olivia
prev…
hash…
verifyAuditChain(events, { head }) → …08 · Federation
Fluent in every enterprise identity protocol
Sign people in with any OIDC or SAML identity provider, act as the OAuth provider for your own apps and MCP servers, provision users in and out with SCIM, and transmit Shared Signals.
- Standards, not adapters. OIDC, OAuth 2.0, SAML 2.0, SCIM 2.0, WebAuthn, DPoP, PAR, and RFC 8693 token exchange.
- Ready for MCP. Dynamic client registration and protected resource metadata (RFC 9728) for AI agents.
- Enterprise onboarding. Verified domains send people to their own company’s identity provider.
And more
The rest of the platform
Governance, operations, and integrations share the same tenant model, the same pipeline, and the same audit log as everything above.
09 · Reference
Generated from the source
The reference is extracted from the repository every time this site is built, so every count and signature on it matches the code.
Own your identity layer, with the guarantees of a platform
Start with SQLite on your laptop, ship on PostgreSQL, and keep every decision, session, and audit event in your own database.
New to identity and access management? Start with the concepts