Why Bones

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.users gates a route, admin.users.update-role gates a button. Both are features.
  • Feature flags and permissions share a table. Every feature has an enabled switch 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

ApproachTrade-off
Role-name checks in codeSimple 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 servicePowerful, 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