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.
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
The whole flow is also exposed as pure operations.
// 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.
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.
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.
// 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)
)
);