@happyvertical/smrt-users
Users, sessions, memberships, hierarchical tenants, roles, fine-grained permissions, access requests, and Postgres RLS.
What it gives you
Users, sessions, memberships, hierarchical tenants, roles, fine-grained permissions, access requests, and Postgres RLS.
- 01Manifest-derived operation permissions
- 02Opt-in role inheritance through tenant trees
- 03OIDC, email and password, device PIN, and terminal device-code sign-in on one session model
- 04One shared login attempt budget across every credential flow
- 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 →Package UI
Component catalog and public contracts
Browse by the job you are doing. Each group shows its focused import path; the Playground tab renders the working controls and composed patterns.
Account and directory UI
Render people, invite them, and give a signed-in person their own menu.
@happyvertical/smrt-users/svelteSessions and OIDC routes
Mount sign-in, callback, and session handling as SvelteKit routes instead of writing the protocol by hand.
@happyvertical/smrt-users/sveltekitPassword and device PIN routes
Mount email and password sign-in for an app without an identity provider, or a per-person PIN on an enrolled device.
@happyvertical/smrt-users/sveltekitTerminal device-code routes
Let a command-line tool start a login, have a person approve it in the browser, and exchange it for a bearer session.
@happyvertical/smrt-users/sveltekitIdentity services
The transport-neutral services the route handlers are built from.
@happyvertical/smrt-usersPermissions and row-level security
Build the permission catalog, resolve what a principal may do, and generate Postgres policies.
@happyvertical/smrt-usersDiscovery contract
Emit and validate the pinnable artifact a CLI or MCP consumer discovers.
@happyvertical/smrt-users/app-contractPackage previews
Try the working views
These previews are owned by the package and use its public components. Mock mode is intended for safe browsing; live mode appears only where the package supports it.
Generated interface
REST
The exact route set comes from each model's @smrt({ api }) policy
and the application manifest—not from a second controller definition.
GET /api/users
GET /api/users/:id
POST /api/users
PATCH /api/users/:idWhat stays consistent
- Tenant scope and operation permissions run before model access.
- OpenAPI and generated clients use the same manifest fields.
- Packages can expose read-only or custom action sets.
Generated interface
MCP
AI tools are generated from the methods you explicitly include. Their inputs are validated against the same model metadata used by REST and CLI.
users_list({ where, limit, orderBy })
users_get({ id })
users_create({ data })
users_update({ id, data })Agent-safe by construction
- Tool availability can be narrower than the HTTP API.
- Principal execution intersects RBAC, agent ceilings, and persona tools.
- Descriptions and schemas are emitted from source metadata.
Browser-native agent interface
WebMCP
The generated web definitions carry the same tool names, descriptions, and JSON Schema
inputs as MCP. smrt-web registers them with document.modelContext.
import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
import { registerWebMcpTools } from '@happyvertical/smrt-web';
// Omitting an exposure policy registers read-only tools only.
const dispose = registerWebMcpTools(webMcpToolDefinitions);The page keeps its security boundary
- Tools execute through generated REST fetchers as the signed-in page user.
- Authentication, tenant gates, permissions, and field policy stay server-side.
- Feature detection makes registration a safe no-op outside supported browsers.
Generated interface
CLI
The app CLI turns manifest resources into predictable operator commands while keeping package-specific actions close to their models.
smrt users list --limit 25 --order-by created_at:desc
smrt users get <id>
smrt users create --from-file ./record.jsonMade for automation
- Machine-readable output and file-based input.
- The same include/exclude policy as other surfaces.
- Migration, introspection, dispatch, and knowledge commands stay in smrt-cli.