s-m-r-t
Open the s-m-r-t source on GitHub Switch to dark color scheme
← All packages
Foundation v0.57.40

@happyvertical/smrt-users

Users, sessions, memberships, hierarchical tenants, roles, fine-grained permissions, access requests, and Postgres RLS.

Source ↗

What it gives you

Users, sessions, memberships, hierarchical tenants, roles, fine-grained permissions, access requests, and Postgres RLS.

  1. 01Manifest-derived operation permissions
  2. 02Opt-in role inheritance through tenant trees
  3. 03OIDC, email and password, device PIN, and terminal device-code sign-in on one session model
  4. 04One shared login attempt budget across every credential flow
  5. 05Reusable mobile auth/session handlers and access-request graduation

Keep signup approval separate from login

AccessRequestService records a pre-User request for access. An approval flow can graduate that request into the user, membership, and tenant records it authorizes. MagicLinkService instead issues a single-use MagicLinkToken for email login; it is a login credential, not a waitlist approval.

Role inheritance is a separate opt-in

Tenant cascade and role inheritance answer different questions. A Role with inheritsToDescendants lets a membership resolve from the nearest flagged ancestor; a direct membership row still takes precedence and the result records inheritedFromTenantId. Seed or update roles with inheritsToDescendants deliberately—turning on tenant cascade alone does not enable this path.

OIDC login against your identity provider

Declare providers under packages.users.auth.oidc. Mount createOidcLoginHandler and createOidcCallbackHandler from the /sveltekit subpath. Each login creates an independent state, nonce, and PKCE verifier, and the challenge method is always S256. The callback checks state, the RFC 9207 authorization-response issuer, and provider errors. It also checks the JWKS-signed ID token and nonce before it reads claims. When the ID token omits email, the callback uses the UserInfo endpoint.

See the sign-in flows →

First identity binding fails closed

A new issuer and subject can resolve to a canonical global Person that already has an owning User. Provisioning then stops with OidcProvisioningError code profile_owned. It does not create a new User, OIDC identity, or session. An application with an invitation or approval workflow can supply authorizeProfileOwner to authorize that first binding. A return value of undefined keeps the fail-closed default. A null value rejects the login.

See the sign-in flows →

Terminal device-code sign-in

TerminalAuthService runs the device-code flow for command-line tools, and createRequest returns a device code, a short user code, and a verification URL. The CLI keeps the device code, and the person types the short user code. The service stores only a hash of the device code, and approval is idempotent. exchangeDeviceCode answers pending, expired, or approved. An approved answer includes a bearer token that resolves to the same session context as a browser cookie. Failed approvals have a per-user rate limit because user codes are short, and the handler reports the limit as 429.

See the sign-in flows →

Password sign-in without an identity provider

PasswordCredentialService signs in existing, active users with an email and password; it never creates a user. The password lives in its own sensitive object with no generated REST, MCP, or CLI surface. Hashing is scrypt with a per-hash salt, and weaker hashes are upgraded on the next sign-in. Every refusal returns the same 401, so timing does not reveal whether an account exists. Mount the flow with createPasswordCredentialHandlers from the /sveltekit subpath.

See the sign-in flows →

A PIN layered on an enrolled device

A shared tablet enrolled through the device-code grant holds a device-account bearer session. DeviceCredentialService lets a person sign in on that device with a short PIN. The result is a layered session with authMethod pin and a parentSessionId. The session is valid only while the device session is, never widens tenant scope, and signs out independently. Administering PINs needs users.pin.manage from a non-PIN session.

See the sign-in flows →

One login budget for every credential

Terminal approval, device PIN, and password sign-in reserve an attempt through LoginAttemptLimiter before any credential work. The budget is stored in the database, so it holds across replicas, and its keys are hashed. A rate limit answers 429 with Retry-After. Generated CLI commands on the identity models are list and get only, or closed.

Sync the permission catalog after migration

syncPermissionCatalog merges three sources: manifest permissions, custom entries from smrt.config.ts, and definitions added at runtime. It reconciles them into Permission rows. The operation reports created, updated, and unchanged slugs. You can run it on every deploy. It is additive and never deletes a stale permission, grants a role, or emits row-level-security SQL.

Authorization model →

Row-level security is generated, not hand-written

generatePostgresPermissionSql inspects tenant-scoped models whose scope is required and returns the statements for them; applyPostgresPermissionPolicies executes those statements. Each table gets row-level security enabled and forced, plus one policy per action guarded by the shared smrt_rls_bypass, smrt_current_tenant_id, and smrt_has_permission functions. Models it cannot target safely are reported as skipped with a reason rather than silently omitted.

Authorization model →

A pinnable discovery contract

The /app-contract subpath carries the versioned discovery artifact that a CLI or MCP consumer can validate and pin. createDiscoveryConformanceArtifact emits deterministically ordered JSON with a SHA-256 integrity digest, and validateDiscoveryConformanceArtifact checks structure, ordering, and that digest in one call.

Generated interfaces →