Roles and permissions
Permissions are keyed by feature, not by role name, so admins can create roles at runtime without a code change.
The problem
Most apps start with if (user.role === "admin"). That works until you need a role you didn't hardcode — "Support", "Billing", "Auditor" — and every check in the codebase has to change.
What Bones does
Code checks features, never role names. A feature is a stable key like admin.users.update-role. Roles are rows in a table, and a grid in the admin decides which role has which feature.
- One mechanism for pages and actions.
page.admin.usersgates a route,admin.users.update-rolegates a button. Both are features. - Feature flags and permissions share a table. Every feature has an
enabledswitch that turns it off for everyone, regardless of grants. - Checked on the server.
requirePermission("admin.roles.create")guards the tRPC procedure. The UI hides what you can't use; the server enforces it. - In the session. Every session carries
enabledFeatures, so the client knows what to show without asking again. - Role priority. Roles are ranked. You can only assign roles at or below your own rank, and only edit grants for roles below it. An administrator can't make anyone an owner.
- Guards against lockout. The owner's grants can't be revoked, and the built-in roles can't be deleted.
How it compares
| Approach | Trade-off |
|---|---|
| Role-name checks in code | Simple until the first new role, then a code change every time |
| A policy engine (OPA, Cedar, Casbin) | Expressive, but a new language and often a new service |
| A hosted authorization service | Powerful, but another vendor sitting on every request |
Bones sits in the middle: a feature × role table in Postgres, checked by one middleware, editable from the admin.
Go deeper
- Checking permissions — guarding a procedure and adding a feature key.
- Organizations — the same model, scoped to one organization.