Core
RBAC
Role-based access control - configuration, permission mapping, and guards.
RBAC
Keyloom supports optional RBAC for multi-organization apps. Enable it in config and use helpers to protect routes and actions.
Config
export default defineKeyloom({
// ...
rbac: {
enabled: true,
roles: {
admin: { permissions: ["manage:org", "manage:users", "read"] },
member: { permissions: ["read"] },
},
},
});enabled: when false, role/org checks are skippedroles: structured mapping of roles to permissions
Types (from @keyloom/core):
RbacConfig,RbacRolesMapping,RbacRolePermissions
Permission mapping
import { toPermissionMap } from "@keyloom/core/rbac/policy";
const permMap = toPermissionMap(config.rbac); // { 'manage:org': ['admin'], 'read': ['admin','member'] }Core guard (generic)
import { withRole } from "@keyloom/core/rbac/with-role";
const handler = withRole(
async () => {
// protected logic
return "ok";
},
{
requiredRoles: ["admin", "member"],
requiredPermission: "manage:org",
getRole: async () => ({ role: "admin" }),
permMap,
}
);- If
requiredRolesis provided, the current role must be included - If
requiredPermissionis provided, role must be allowed in the map
Next.js helpers
For app router ergonomics use @keyloom/nextjs/rbac:
import {
withRole,
getActiveOrgId,
setActiveOrgCookie,
} from "@keyloom/nextjs/rbac";getActiveOrgId()- reads current org from cookiesetActiveOrgCookie(orgId)- returns a Set-Cookie stringwithRole(action, { getUser, adapter, requiredRoles, requiredPermission, config })- server action wrapper enforcing RBAC
Adapter requirements
When RBAC is enabled, your adapter should implement membership queries:
interface RbacAdapter {
getMembership(
userId: string,
orgId: string
): Promise<{ role: string } | null>;
}Use provided memory adapter implementation as a reference.
Patterns
- Redirect users without an active org to an org selection page
- Use middleware for coarse gating and
withRolefor server-side enforcement - Keep permissions small and composable (e.g.,
read,write,manage:org)
Prerequisites
- Define roles and permissions in
keyloom.config.ts - Ensure your adapter can read memberships:
getMembership(userId, orgId) - Choose an organization selection strategy (cookie, query param, page)
Organization selection workflow (example)
"use server";
import { setActiveOrgCookie } from "@keyloom/nextjs/rbac";
export async function selectOrg(orgId: string) {
// validate orgId belongs to user before setting
return new Response(null, {
status: 204,
headers: { "set-cookie": setActiveOrgCookie(orgId) },
});
}import { getActiveOrgId } from "@keyloom/nextjs/rbac";
import { selectOrg } from "./actions";
export default async function OrgsPage() {
const current = getActiveOrgId();
const orgs = [
{ id: "org_1", name: "Acme" },
{ id: "org_2", name: "Globex" },
];
return (
<form action={(fd) => selectOrg(String(fd.get("orgId")))}>
<select name="orgId" defaultValue={current ?? undefined}>
{orgs.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
<button type="submit">Use organization</button>
</form>
);
}Server action enforcement
import { withRole } from "@keyloom/nextjs/rbac";
import cfg from "@/keyloom.config";
export async function adminOnly(action: () => Promise<Response>) {
return withRole(action, {
requiredRoles: ["owner", "admin"],
getUser: async () => ({ id: "usr_42" }),
adapter: (cfg as any).adapter,
rbacEnabled: cfg.rbac?.enabled !== false,
config: cfg, // derive permission map when provided
});
}API reference
Prop
Type
Types (core): RbacConfig, RbacRolesMapping, Role, Permission, Membership.
Realistic permission mapping
const rbac = {
enabled: true,
roles: {
owner: {
permissions: ["manage:org", "manage:users", "billing", "read", "write"],
},
admin: { permissions: ["manage:users", "read", "write"] },
member: { permissions: ["read"] },
viewer: { permissions: ["read"] },
},
} as const;Business rules examples:
- Only
ownercan access billing and transfer ownership admincan invite/remove members and manage resourcesmembercan read and write project resourcesviewercan only read
Adapter contract
interface RbacAdapter {
getMembership(
userId: string,
orgId: string
): Promise<{ role: string } | null>;
}- Implement
getMembershipefficiently (index on userId+orgId) - Return
nullfor non-members to enforce 403
Troubleshooting
- No active org: set cookie via
setActiveOrgCookieafter user selection - Access denied: role missing required permission; check
toPermissionMapresult - RBAC ignored in middleware: ensure
config.rbac.enabled !== false - Edge checks failing: rely on server
withRolefor final enforcement
Performance and security
- Derive permission map once per request; avoid recomputing repeatedly
- Keep the org cookie HttpOnly and SameSite=Lax
- Do not trust client-provided orgId without server validation
See also
- Next.js middleware: /docs/nextjs/middleware
- Server helpers: /docs/nextjs/overview
- Configuration: /docs/core/config
Next steps
- Add an org selection page and persist selection
- Wrap sensitive server actions with
withRole
How is this guide?