npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@happyvertical/smrt-users

v0.36.0

Published

Multi-tenant user management for the SMRT framework - users, tenants, roles, permissions, groups

Downloads

1,689

Readme

@happyvertical/smrt-users

Multi-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.

Installation

pnpm add @happyvertical/smrt-users

Usage

Roles and permissions

import {
  RoleCollection, MembershipCollection, PermissionResolver,
} from '@happyvertical/smrt-users';

const db = { db: { type: 'sqlite', url: 'app.db' } };

// Seed system roles (owner, admin, member, viewer) — required at app init
const roles = await RoleCollection.create(db);
await roles.seedSystemRoles();

// Assign a user to a tenant with the admin role
const memberships = await MembershipCollection.create(db);
const adminRole = await roles.findBySlug('admin');
await (await memberships.create({
  userId: user.id, tenantId: tenant.id, roleId: adminRole.id,
})).save();

// Check permissions
const resolver = await PermissionResolver.create(db);
await resolver.hasPermission(user.id, tenant.id, 'articles.create');

Manifest-derived permission catalog

SMRT objects now contribute permissions automatically based on their public surface area.

import { SmrtObject, smrt } from '@happyvertical/smrt-core';

@smrt({
  api: { include: ['list', 'create', 'publish'] },
  cli: { include: ['get', 'archive'] },
  collection: 'articles',
  mcp: { include: ['update'] },
  tenantScoped: { mode: 'required' },
})
class Article extends SmrtObject {
  tenantId: string = '';
  title: string = '';

  async publish(): Promise<boolean> {
    return true;
  }

  async archive(): Promise<boolean> {
    return true;
  }
}

This produces the following permission slugs:

  • articles.read from list or get
  • articles.create
  • articles.update
  • articles.publish
  • articles.archive

Non-public methods and actions that are not exposed through API, CLI, or MCP are not added to the catalog.

Sync the permission catalog

Use syncPermissionCatalog() during bootstrapping, migrations, or deploy hooks to upsert discovered permissions into the Permission table.

import { syncPermissionCatalog } from '@happyvertical/smrt-users';

const db = {
  db: {
    type: 'postgres' as const,
    url: process.env.DATABASE_URL!,
  },
};

const result = await syncPermissionCatalog(db);

console.log('created', result.created);
console.log('updated', result.updated);
console.log('unchanged', result.unchanged);

Catalog sync is additive and fail-closed:

  • it creates missing Permission rows
  • it updates name, description, and category by slug
  • it does not auto-grant permissions to roles
  • it does not delete stale permissions in v1

App-defined permissions in smrt.config.ts

Use package config for permissions that do not come from the manifest.

// smrt.config.ts
import { defineConfig } from '@happyvertical/smrt-config';

export default defineConfig({
  packages: {
    users: {
      permissions: {
        custom: [
          {
            category: 'app',
            description: 'Allows access to the operations dashboard',
            name: 'View Operations Dashboard',
            slug: 'operations.dashboard',
          },
          {
            category: 'audits',
            name: 'Inspect Audit Rows',
            postgres: {
              bindings: [
                {
                  action: 'select',
                  tableName: 'audit_logs',
                },
                {
                  action: 'insert',
                  tableName: 'audit_logs',
                },
              ],
            },
            slug: 'audits.inspect',
          },
        ],
        postgres: {
          enabled: true,
        },
      },
    },
  },
});

Custom permissions merge with manifest-derived permissions by slug. If the same slug is registered with conflicting metadata, SMRT throws so the mismatch is visible early.

Runtime permission registration

Use registerPermissionDefinitions() when a package or integration needs to declare permissions at runtime.

import {
  registerPermissionDefinitions,
  syncPermissionCatalog,
} from '@happyvertical/smrt-users';

const unregister = registerPermissionDefinitions([
  {
    category: 'billing',
    description: 'Allows exporting invoices',
    name: 'Export Invoices',
    slug: 'invoices.export',
  },
]);

try {
  await syncPermissionCatalog({
    db: { type: 'sqlite', url: 'app.db' },
  });
} finally {
  unregister();
}

Postgres RLS enforcement

For Postgres, SMRT can generate and apply row-level security policies directly from the permission catalog.

import {
  applyPostgresPermissionPolicies,
  generatePostgresPermissionSql,
  syncPermissionCatalog,
} from '@happyvertical/smrt-users';

const db = {
  db: {
    type: 'postgres' as const,
    url: process.env.DATABASE_URL!,
  },
};

await syncPermissionCatalog(db);

const preview = generatePostgresPermissionSql(db);
console.log(preview.targets);
console.log(preview.skipped);

await applyPostgresPermissionPolicies(db);

Automatic policy generation currently applies only to objects that are:

  • tenant-scoped with tenantScoped: { mode: 'required' }
  • backed by a real Postgres table
  • mapped to a single tenant field

Automatic CRUD policy mapping is fixed in v1:

  • SELECT -> <collection>.read
  • INSERT -> <collection>.create
  • UPDATE -> <collection>.update
  • DELETE -> <collection>.delete

Optional-tenancy and global tables are skipped and returned in result.skipped instead of generating unsafe policies. Custom permissions can participate in RLS by adding explicit Postgres bindings as shown above.

SvelteKit hooks

// hooks.server.ts
import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';

export const handle = createSessionHandler({
  db: { type: 'postgres', url: process.env.DATABASE_URL },
  enterTenantContext: true,
  postgresRls: true,
  ttl: 7 * 24 * 60 * 60, // 7 days in seconds
  skipPaths: ['/api/health'],
});
// Populates event.locals: { user, permissions, tenantId, sessionId }

// +page.server.ts
import { createSessionCookie, destroySessionCookie } from '@happyvertical/smrt-users/sveltekit';

await createSessionCookie(event, userId, tenantId, { db }); // login
await destroySessionCookie(event, { db });                   // logout

OIDC login with Kanidm or Dex

Kanidm and Dex both work through the generic SMRT OIDC flow. Configure one or more providers under packages.users.auth.oidc.providers, then add login and callback route handlers.

// smrt.config.ts
import { defineConfig } from '@happyvertical/smrt-config';

export default defineConfig({
  packages: {
    users: {
      auth: {
        oidc: {
          defaultProvider: 'kanidm',
          providers: {
            kanidm: {
              kind: 'kanidm',
              issuer: process.env.KANIDM_ISSUER!,
              clientId: process.env.KANIDM_CLIENT_ID!,
              clientSecret: process.env.KANIDM_CLIENT_SECRET,
              redirectUri: 'http://localhost:5173/auth/kanidm/callback',
            },
            dex: {
              kind: 'dex',
              issuer: process.env.DEX_ISSUER!,
              clientId: process.env.DEX_CLIENT_ID!,
              clientSecret: process.env.DEX_CLIENT_SECRET,
              redirectUri: 'http://localhost:5173/auth/dex/callback',
            },
          },
        },
      },
    },
  },
});
// src/routes/auth/[provider]/login/+server.ts
import { createOidcLoginHandler } from '@happyvertical/smrt-users/sveltekit';

export const GET = createOidcLoginHandler({
  db: { type: 'postgres', url: process.env.DATABASE_URL! },
});
// src/routes/auth/[provider]/callback/+server.ts
import { createOidcCallbackHandler } from '@happyvertical/smrt-users/sveltekit';

export const GET = createOidcCallbackHandler({
  db: { type: 'postgres', url: process.env.DATABASE_URL! },
  successRedirect: '/dashboard',
});

The callback verifies state, PKCE, issuer, audience, nonce, and the provider JWKS-signed ID token, falling back to the OIDC UserInfo endpoint when the ID token omits required profile claims like email. Temporary transaction cookies are HMAC-signed with the provider clientSecret when present; public clients can pass transactionCookieSecret to the route helpers. On success it creates or reuses a SMRT Profile, links an OidcIdentity, creates or reuses a User, records lastLoginAt, and sets the standard SMRT session cookie.

With postgresRls: true, SMRT opens a request-scoped Postgres transaction, loads the session, resolves permissions, and sets session variables used by the generated RLS helpers:

  • smrt.tenant_id
  • smrt.user_id
  • smrt.session_id
  • smrt.permissions
  • smrt.super_admin_bypass
  • smrt.system_context

With enterTenantContext: true, the same request also enters @happyvertical/smrt-tenancy context so regular collection access is scoped to the current tenant in application code.

Request-scoped database access

Generated SvelteKit helpers and custom server code can read the current request-scoped database, which is especially useful when Postgres RLS is enabled and you want collection operations to use the active transaction.

import {
  getRequestScopedDatabase,
  withSessionPermissionContext,
} from '@happyvertical/smrt-users';

const response = await withSessionPermissionContext(
  {
    db: { type: 'postgres', url: process.env.DATABASE_URL! },
    enterTenantContext: true,
    postgresRls: true,
    sessionId,
  },
  async (context) => {
    const database = getRequestScopedDatabase();

    console.log(context.permissions);
    console.log(database === context.database); // true

    return new Response('ok');
  },
);

Key Concepts

Permission cascade (4 levels)

PermissionResolver evaluates permissions in order, where each level can add or remove grants:

  1. Tenant hierarchy -- walk ancestors, apply TenantPermissionOverride at each level
  2. Membership role -- base permissions from the user's role in the tenant
  3. Group roles -- permissions from all groups the user belongs to in that tenant
  4. Membership overrides -- per-user GRANT/DENY (DENY always wins)

Tenant-level inherited permissions are part of the effective permission set returned by resolvePermissions() and SessionService.loadSessionContext().

Hierarchical tenants

Tenants support parent-child trees (max depth 10). Two flags control inheritance: cascadePermissions (parent pushes down) and inheritPermissions (child accepts). Both must be true for permissions to flow.

Tenant policies

TenantService supports three modes: flexible (no auto-create), personal (auto-create on first login, deletable), required (auto-create, must keep at least one).

API

Models

| Export | Description | |--------|-------------| | User | Auth identity. Email auto-lowercased. profileId links to smrt-profiles (plain string). | | Tenant | Organizational boundary. STI. Hierarchical via parentTenantId/hierarchyPath. | | Role | Permission template. tenantId = null for system roles. isSystem blocks deletion. | | Permission | Named capability. Slug format: resource.action. | | Session | Server-side session. Secure UUID. TTL in seconds. | | Group | Team within a tenant. Gains permissions via GroupRole. | | Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). | | MembershipOverride | Per-user permission grant/deny on a membership. | | TenantPermissionOverride | Tenant-level permission override (INHERIT/GRANT/DENY). | | GroupMember, GroupRole, RolePermission | Junction tables for groups and role-permission assignments. |

Collections

| Export | Description | |--------|-------------| | UserCollection, TenantCollection, RoleCollection | Core CRUD. TenantCollection adds createChild(), getTree(). RoleCollection adds seedSystemRoles(). | | PermissionCollection, SessionCollection | Permission CRUD with findByIds(). Session CRUD with findValidSession(), deleteExpired(). | | MembershipCollection | Membership CRUD, findByUserAndTenant() | | MembershipOverrideCollection, TenantPermissionOverrideCollection | Override management at membership and tenant levels | | GroupCollection, GroupMemberCollection, GroupRoleCollection, RolePermissionCollection | Group and role-permission junction management |

Services

| Export | Description | |--------|-------------| | PermissionResolver | Resolves effective permissions via 4-level cascade. hasPermission(), resolvePermissions(). | | PermissionCatalogService, syncPermissionCatalog() | Discovers manifest/config/runtime permissions and upserts them into Permission rows. | | registerPermissionDefinitions() | Register app or integration permissions at runtime and receive an unregister cleanup function. | | generatePostgresPermissionSql(), applyPostgresPermissionPolicies() | Preview or apply Postgres RLS helper functions and table policies. | | SessionService | High-level session management. createSession(), loadSessionContext(), destroySession(). | | OidcLoginService | Generic OIDC authorization-code login with PKCE for Kanidm, Dex, and other standards-compliant providers. | | withSessionPermissionContext() | Loads a session, optionally enters tenancy context, and exposes a request-scoped database/permission context. | | getCurrentSessionPermissionContext(), getRequestScopedDatabase() | Read the active request/session context inside app code. | | TenantService | Policy-driven tenant lifecycle. ensureTenantForUser(), createTenantWithOwnership(). |

SvelteKit (@happyvertical/smrt-users/sveltekit)

| Export | Description | |--------|-------------| | createSessionHandler | SvelteKit handle hook that populates event.locals, and can also enter tenancy context and Postgres RLS request transactions | | createSessionCookie | Set session cookie after login | | destroySessionCookie | Clear session cookie on logout | | switchSessionTenant | Change tenant context for current session | | beginOidcLogin, completeOidcLogin | Low-level SvelteKit helpers for custom OIDC login routes | | createOidcLoginHandler, createOidcCallbackHandler | Ready-to-use SvelteKit route handlers for OIDC login and callback | | SessionLocals | Type for event.locals (extend in app.d.ts) |

Types & Constants

| Export | Description | |--------|-------------| | UserStatus, TenantStatus, SessionStatus, MembershipStatus | Status enums | | OverrideEffect, TenantPermissionEffect | Override effect enums | | DEFAULT_ROLE_SLUGS, DEFAULT_ROLES, DEFAULT_TENANT_POLICY | System role slugs, role configs, default tenant policy | | DEFAULT_SESSION_TTL, MAX_TENANT_HIERARCHY_DEPTH | 604800 (7 days in seconds), 10 | | TenantHierarchyError | Thrown when hierarchy depth limit is exceeded |

Dependencies

  • @happyvertical/smrt-core -- ORM, @smrt() decorator, SmrtObject/SmrtCollection
  • @happyvertical/smrt-types -- shared enums (UserStatus, SessionStatus, etc.)
  • @happyvertical/smrt-profiles -- optional peer dependency for profile linking
  • jose -- JWT/JWKS verification for OIDC and magic-link tokens
  • svelte -- optional peer dependency for Svelte components

License

MIT