AbsoluteJS

Organizations & RBAC

First-class multi-tenancy: organizations, memberships, and email invitations, plus org-scoped roles and a turnkey permission check. This is the tenant spine the SSO, SCIM, and authorization blocks hang off.

#Organizations

The organizations block adds an Organization entity and user-to-org memberships, backed by one cohesive OrganizationStore (in-memory, Postgres, or Neon). Creating an org makes the caller its owner.

TS
import { auth, createNeonOrganizationStore } from '@absolutejs/auth';

await auth<User>({
  providersConfiguration: {},
  organizations: {
    organizationStore: createNeonOrganizationStore(process.env.DATABASE_URL),
    getUserId: (user) => user.sub,
    onSendInvitation: ({ email, token, organizationId }) =>
      sendInviteEmail(email, organizationId, token)
  }
});

// Mounts: GET  /auth/organizations            (the caller's orgs)
//         POST /auth/organizations            (create -> caller becomes owner)
//         POST /auth/organizations/:org/invitations
//         POST /auth/organizations/invitations/accept   { token }
//         GET/DELETE members + invitations    (gated to active members)

#Invitations

1
Invite by email
Invite teammates by email with a single-use, hashed-at-rest token.
2
Send the link
The plaintext token is returned once, for your email link.
3
Accept to join
Accepting turns the invite into an active membership.

The whole flow is also exposed as pure operations.

TS
// The same flow is available as pure, reusable operations (e.g. to seed an org
// during signup) — no HTTP layer required:
import {
  createOrganization,
  inviteToOrganization,
  acceptInvitation
} from '@absolutejs/auth';

const org = await createOrganization({
  organizationStore,
  name: 'Acme',
  ownerUserId: user.sub
});

const { token } = await inviteToOrganization({
  organizationStore,
  organizationId: org.organizationId,
  email: 'teammate@acme.com',
  roles: ['member']
});

// The invitee accepts (single-use, hashed-at-rest token) -> active membership:
await acceptInvitation({ organizationStore, token, userId: invitee.sub });

#JIT / domain assignment

autoAssignOrgsByEmail auto-joins every new user to the orgs their email domain maps to — call it from your OAuth-callback or register hook.

Idempotent (skips orgs the user already belongs to); returns the orgs newly added, so you can audit-log them.
TS
import { autoAssignOrgsByEmail } from '@absolutejs/auth';

// JIT / domain-based assignment: every new user automatically joins the orgs
// their email domain maps to. Call from your OAuth-callback / register hook —
// idempotent (skips orgs the user already belongs to), returns the orgs newly
// added so you can audit-log them.
const domains = {
  'acme.com': ['org_acme'],
  'eng.acme.com': ['org_acme', 'org_acme_eng']
};

await autoAssignOrgsByEmail({
  email: user.email,
  getOrgsForDomain: (domain) => domains[domain] ?? [],
  organizationStore,
  roles: ['member'],
  userId: user.sub
});

#Roles & Permissions

Define org-scoped (or global) roles as slugs mapped to permission slugs. createMembershipPermissionResolver turns a member's roles into a ready-made hasPermission hook, so RBAC is plug-and-play — the package stays schema-agnostic.

TS
import {
  auth,
  createNeonRoleStore,
  createMembershipPermissionResolver
} from '@absolutejs/auth';

const roleStore = createNeonRoleStore(process.env.DATABASE_URL);

await auth<User>({
  providersConfiguration: {},
  organizations: { organizationStore, getUserId },
  roles: { roleStore, organizationStore, getUserId },
  // Turnkey RBAC: a member's org-scoped roles resolve to permissions.
  authorization: {
    hasPermission: createMembershipPermissionResolver({
      getUserId,
      organizationStore,
      roleStore
    })
  }
});

// Define roles (org-scoped or global). A '*' permission grants everything:
await roleStore.saveRole({
  slug: 'admin',
  organizationId: 'acme',
  permissions: ['billing:read', 'billing:write', 'members:manage'],
  createdAt: Date.now(),
  updatedAt: Date.now()
});

#protectPermission

Gate any route on a permission. The decision is fully delegated to your hook, so it works with the built-in resolver or any custom RBAC/ABAC scheme.

TS
// auth() exposes a protectPermission derive alongside protectRoute. It delegates
// the decision to your hasPermission hook: 401 when unauthenticated, 403 when
// denied (denials emit an authorization_denied audit event).
app.get('/org/:organizationId/billing', ({ params, protectPermission }) =>
  protectPermission(
    { organizationId: params.organizationId, permission: 'billing:read' },
    (user) => getBilling(params.organizationId)
  )
);