v0.1.0Read the changelog

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.

Get started

Node.js 22.12+ PostgreSQL, SQLite, or libSQL ESM with TypeScript types

01Authenticateauth

Sign in to Acme

Email
Continue with passkey

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.
Read the architecture overview
routes/invite.ts
// 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],
});
Pipeline trace Running
  1. CredentialThe cookie resolves to an identity and a session.
  2. Re-validateInside the transaction: identity active, session live, MFA and network rules hold.
  3. Authorizeiam:identities:create on iam/{tenantId} is allowed.
  4. ApplyThe invitation is written, then duty and invariant checks run.
  5. AuditAn event joins the tenant’s SHA-256 hash chain.
  6. Fan-outAfter commit: webhooks and subscribers.

Browser, server action, CLI, and SCIM calls all enter this same pipeline.

  1. 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.

  2. 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.

  3. 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
  4. 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
  5. 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.

  6. 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.
Start the quickstart
lib/iam.ts1 / 6

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.
Read the authentication guide
One sign-in, from method to session

Method

Checks

  1. Method allowedThe tenant policy is checked before any credential
  2. Credential verifiedRate-limited per tenant, and optionally per IP
  3. Second factorWhen required: TOTP, a passkey, or an emailed code
  4. Network allowedIP allowlists and blocked networks, at issuance

Session

waitingissued
kind'user'
method'password'
mfatrue
client{ ip: '203.0.113.7' }
expiresAtcapped by policy

Lifetime, idle timeout, and session count follow the tenant's policy.

Policies read the result as 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.
How a decision is made
alice invoices:approve on invoice/inv_1009

Request context

What the evaluator receives

principal.id
'usr_alice'
principal.mfa
false
resource.ownerId
'usr_alice'
request.sourceIp
'10.4.2.7'

Documents

allowApproveOwnInvoicesmatched

invoices:approve on invoice/*

StringEqualsresource.ownerId = ${principal.id}
IpAddressrequest.sourceIp in 10.0.0.0/8
denyRequireMfamatched

invoices:* on invoice/*

Boolprincipal.mfa = false
boundaryFinanceCeilingmatched

invoices:* on invoice/*

Deny statements
Grants
Boundaries
Deniedexplicit-deny
Evaluated in your browser by the same function the server uses.Open the playground

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.
Tenants and identities
Tenant tree
Platformroot
Acmeorganization
Globexorganization
Billingproject
Web appproject
Dataproject
Every tenant has its own directory and access model. Owning Acme grants nothing inside Billing: reaching another tenant takes platform-controlled role assumption or root authority.

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.
Just-in-time elevation
Just-in-time elevation · Production admin
  1. Eligible
  2. Request
  3. Approve
  4. Active
  5. Expired

Alice is eligible for Production admin

An eligible binding grants nothing until it is activated.

activation.activefalse
remaining30:00

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.
How the audit chain works
Audit chain · tenant acme

Every event stores the previous event’s hash, so the four form one chain.

  1. sequence 1

    binding:activation-requested

    actor usr_alice

    prev

    hash

  2. sequence 2

    binding:activation-approved

    actor usr_priya

    prev

    hash

  3. sequence 3

    binding:deactivate

    actor usr_alice

    prev

    hash

  4. sequence 4

    identity:offboard

    actor usr_olivia

    prev

    hash

Chain verified
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.
Federation overview
Federation · one instance, every protocol
Better IAMOne tenant model, one audit log
Corporate IdP. People sign in with their company’s identity provider. SAML connections are managed per tenant, with metadata import.