s-m-r-t
Open the s-m-r-t source on GitHub Switch to dark color scheme
← Foundations

Foundation 04

Connect each user to a tenant and a role

Memberships answer where someone has access. Roles collect the exact operations they may perform in that tenant.

Membership
UserProfileTenantRolePermission
Authentication, product identity, organization, and access remain separate.

Derive common permissions from the model

Public model operations contribute permission names. Sync the catalog after migration, then seed or assign roles with the permissions your app needs.

  • items.read covers list and get.
  • items.create, items.update, and items.delete stay separate.
  • Custom methods can add names such as articles.publish.
seed-access.ts
typescript
await syncPermissionCatalog(getSmrtConfig('Permission'));

const roles = await RoleCollection.create(getSmrtConfig('Role'));
await roles.seedSystemRoles({ seedPermissions: true });

Enforce the same decision at every entry point

Generated routes are authentication-gated and tenant-scoped. Custom SvelteKit actions, jobs, and in-process writes must also check the principal permission snapshot. assertOperationPermission derives the same names the catalog holds and refuses an operation whose name is not in it.

src/routes/articles/+page.server.ts
typescript
import { assertOperationPermission } from '@happyvertical/smrt-users';

// Throws OperationPermissionError (status: 403) unless the caller holds
// articles.publish in the tenant that owns this article, or is running in
// system or super-admin context. Map error.status to a response yourself.
await assertOperationPermission({
  ...getSmrtConfig('Permission'),
  collection: 'articles',
  action: 'publish',
  userId: locals.user.id,
  tenantId: article.tenantId
});

Postgres can check the same names on every row

A Postgres application can generate row-level security policies that check the same names. The request context publishes the resolved permissions to the database session. Each policy requires a tenant match and the applicable permission before a row operation. System context and super-admin sessions bypass these checks. Policy generation and application are deliberate steps. SQLite applications keep the catalog and guard but get no data-layer check.

Some packages contribute their own permissions

A framework package can add named permissions to the same catalog. Field policy contributes two: fields.policy.manage for organization-wide administration and fields.policy.personalize for a principal maintaining only their own preferences.