Perms ($perm) — Active

The server decides. ActivePerms and <Can /> are UX helpers only. Every protected read, write, channel join or command must call the server engine.

Overview

perm is the authorization artifact. It does not log users in, does not own sessions and does not replace auth. It answers one question: actor + action + resource + context -> decision.

The module combines RBAC, ABAC and ReBAC instead of forcing the app into one model: roles are actor attributes, request/session/risk values live in context, and ownership or membership is resolved through relation providers.

Mental model

Authorization is a server decision with a client mirror. The policy language in $libs/perm defines what can be said, $svrs/perm evaluates it against trusted actor/resource/context data, and $perm only mirrors decisions for UI responsiveness. If a route, action, WebSocket channel or job touches protected data, the server engine must decide again even if the button was hidden by <Can />.

Public surface

Factory / helper Layer Purpose
createEnginePerms(options)serverAuthoritative runtime with check, assert, explain, what, who and filter.
createPermHttpHandlers(engine, resolveActor)serverHTTP bridge for the active client: check, batch, what, explain and handle.
createActivePerms(options)clientRemote reflector with cache, snapshots, loading/error state and can().
defineActivePerm(options)clientService factory for the App schema. Registered as services.perm; the builder injects Http, Logger and Bus; endpoint remains explicit.
Can.svelteclient UIConditional rendering helper backed by Perms.can(...).

API reference

Server member Purpose Notes
check(input)Return a rich permission decision.Decision effect is allow, deny, not_applicable or indeterminate.
can(input)Boolean shortcut over check().True only for allow.
assert(input)Fail-closed enforcement.Throws when decision is not allow.
explain(input)Decision trace.Use for audit/debug panels and policy authoring.
what(input)Action matrix for one actor/resource.Useful for toolbars and menus.
who(input)Actor discovery query.Provider-backed where supported.
filter(action)Build a predicate or query plan.Can target SQL through createSqlCompiler().
dispose()Close runtime.Post-dispose calls throw PermDisposedError.
Active member Purpose Notes
currentSnapshotCurrent serializable decision cache.Can seed SSR or restore known checks.
decisionsCached decision map.Scoped by scopeKey to avoid actor leakage.
sizeNumber of cached decisions.Debug/UI surface.
loading / lastError / disposedActiveEngine state.Shared active contract.
check(), can(), batch()Remote permission checks.Calls server handlers and caches responses.
what(), explain()Remote inspection helpers.Still uses server truth.
hydrate(snapshot)Seed active cache.Usually SSR.
snapshot()Serialize active cache.Safe to send to client when actor-scoped correctly.
invalidate(scope?)Clear matching decision cache.Call after login/logout/role/resource changes.
subscribe(fn), onChange(fn)Observe cache/state changes.Use for panels and integrations.
decisionKey(input)Compute cache key.Includes scope key in the active client.
clearError(), dispose()Lifecycle.Dispose clears listeners and rejects new calls.

Vocabulary schema

The schema is not a database schema. It is the vocabulary that policies are allowed to speak: actor types, resources, actions, attributes, relations and context.

Policies

Policies are built as data. The fluent builder produces an IR that can be evaluated, explained, serialized and partially compiled.

Database integration

perm deliberately does not own the database schema. The application owns users, resources, relation tables and policy storage; perm owns the authorization language and the evaluation contract. This keeps it compatible with SQL, Prisma, Drizzle, Kysely, document databases or hand-written repositories.

The important split is: persist domain truth in your database, load trusted actor/resource data on the server, and expose unresolved facts through providers or query compilers.

{#each databasePersistenceRows as row (row.item)} {/each}
Thing Where it lives How perm uses it
{row.item} {row.persistedAs} {row.runtimeUse}

The repository includes a PostgreSQL reference migration at src/svrs/perm/sql/postgres.sql. It defines permission_policies, permission_relations and permission_decision_audit. Treat it as the supported starting point for a real database model, then adapt naming, migrations and ORM mappings to the application.

Persisting policies

The default and simplest pattern is to keep policies in TypeScript. If a product needs admin-managed or tenant-specific rules, store the produced PolicyIR as JSON, validate it when loading, version it, and then pass the resulting array to createEnginePerms(). Do not persist fluent builder calls or executable functions.

DB-backed providers

Relations and lazy attributes are where the engine talks to persistence. A relation name such as post.team.member is not automatically queried just because it appears in the schema; the provider is the adapter between policy vocabulary and database truth. Return 'unknown' when the provider cannot answer safely.

Querying lists with SQL filters

For detail routes, load the resource and call assert(). For list routes, avoid loading every row and filtering in memory. Use filter(...).toPlan('sql') to compile the portion of the policy graph that can run in the database. If the plan is partial, either fail closed or post-filter the returned rows with the engine.

Decision model

Effect Meaning Security posture
allowA matching allow policy granted access.May carry ttl, obligations and advice.
denyA matching deny policy rejected access.Deny overrides allow.
indeterminateThe runtime or a provider could not decide safely.Treat as denied unless a route deliberately chooses otherwise.
not_applicableNo policy matched.Fail closed on protected endpoints.

Server engine

Server code supplies providers for data that is not already present on actor, resource or context. Providers should return 'unknown' instead of guessing when they cannot resolve safely.

Server enforcement

Use assert() or inspect check() before touching protected data. Do this in route handlers, actions, command handlers, jobs and realtime servers.

Runtime flow

A safe permission check starts from server-trusted identity, loads the protected resource on the server, evaluates the decision, fails closed for anything other than allow, and then enforces any obligations attached to the decision.

HTTP bridge

The active client talks to the server through permission handlers. The built-in handle() dispatches POST requests whose path ends in /check, /batch, /what or /explain.

Active client

The active client caches remote decisions for first-paint UX and repeated toolbar/list checks. It is not trusted by the server.

Option Meaning
endpointBase endpoint used by the remote client. Required unless provided through App options.
initialSnapshotSSR/first-paint cache seed.
cacheTtlMsDefault TTL for allow decisions without a server TTL.
nonAllowCacheTtlMsShorter TTL for deny and not-applicable decisions.
remoteFailureBackoffMsBrief backoff for remote failures to avoid retry storms.
scopeKeyActor/session key used to avoid leaking cached decisions between users in one tab.

Snapshots and cache

Perms.snapshot() returns the serializable client cache. Use hydrate(snapshot) for SSR or to restore a known decision set, and call invalidate(scope?) after login, logout, role changes, session refreshes or resource mutations.

; global?: Record; expiresAt?: string; }`} />

Can component

<Can /> renders children only when Perms.can(...) resolves to true. It reads the active client from Svelte context via setPermsContext().

what(), explain() and filter()

Use what() when a screen needs an action matrix for one resource, use explain() for audit/debug panels, and use filter() to turn authorization into a list predicate or query plan.

Obligations and advice

An allow can carry obligations such as mask(), redact(), audit() or requireMfa(). The decision returns them; the caller must enforce them.

Integration rules

  • auth proves identity; perm decides access.
  • $session is where the actor comes from; never trust an actor sent by the browser.
  • http is the active client's transport when composed through App.
  • $cache and permission snapshots must be invalidated after actor or permission changes.
  • $connection servers should call the engine before channel joins or privileged messages.
  • $logger receives decision, deny and indeterminate diagnostics when injected.

Common mistakes

{#each commonMistakes as mistake (mistake.name)} {/each}
Mistake Why it hurts Correct pattern
{mistake.name} {mistake.why} {mistake.fix}

Testing

Unit-test policy behavior directly against createEnginePerms(), then test remote behavior through createPermHttpHandlers() and createActivePerms(). The interactive page is /test/perm.