RBAC Design
A practical recipe for designing role-based access control in Meridian-backed applications. This guide walks through modeling roles, scoping permissions, and enforcing policy at the API edge without leaking authorization logic into your UI layer.
1. Model roles as capabilities, not titles
A common mistake is encoding job titles (admin, editor, viewer) directly as roles. Titles drift; capabilities are stable. Define a flat set of capability strings such as billing:read, billing:write, and users:invite, then compose roles from those.
This lets you split or merge titles later without touching policy code, and it keeps audit logs meaningful when permissions evolve.
2. Scope every permission to a resource
Global flags like is_admin are a trap. Tie each grant to a resource id: an organization, a workspace, a project. A user should be able to be admin of one workspace and read-only on another without forking your auth model.
type Grant = {
subject: string; // user id
capability: string; // "billing:read"
resource: string; // "workspace:42"
expires_at?: string; // ISO date
};
function can(
grants: Grant[],
capability: string,
resource: string,
): boolean {
return grants.some(
(g) =>
g.capability === capability &&
g.resource === resource,
);
}3. Enforce policy at the edge, hint at the UI
The server is the only honest source of truth. Every mutating endpoint must re-check the caller's grants against the target resource. The UI should use the same grant list to hide actions the user cannot perform, but treat hiding as a UX hint, never as a security boundary.
When you ship a new capability, add it to the seed grants for existing roles in the same migration. Skipping that step is the single most common source of mysterious 403s in production.