Designing a Permission Schema: Resources, Actions, Wildcards, and Deny-Wins
Your permission naming convention is an API you will live with for years. A practical guide to resource:action naming, choosing granularity, using wildcards safely, and applying deny-wins semantics — with a reference schema you can copy.
Your permission naming convention is an API. Once services and customers depend on invoice:read, renaming it is a breaking change. It is worth getting right early. This is a practical guide to designing a permission schema that stays legible as your product grows.
The resource:action Convention
Adopt a consistent, two-part naming scheme: a resource (the noun) and an action (the verb). Keep resources singular and actions in a small, controlled vocabulary.
invoice:read invoice:create invoice:update invoice:delete
report:export user:invite billing:manage apikey:rotateChoosing Granularity
Too coarse (billing:manage covers everything) and you cannot express least privilege. Too fine (invoice:update-due-date) and you drown in permissions. A good rule: model a distinct action when a real role would plausibly need one without the other. Start coarser; split when a use case demands it.
Wildcards
Wildcards collapse families of permissions. Support them on both halves of the pair, and reserve the full wildcard for genuine superadmins:
{ resource: 'invoice:*', action: 'read' } // read any invoice sub-resource
{ resource: 'report', action: '*' } // any action on reports
{ resource: '*', action: '*' } // superadmin — grant sparinglySee wildcard permissions for matching semantics.
Deny-Wins Semantics
Decide your conflict-resolution rule up front. Deny-wins — an explicit deny overrides any allow — is the safest default and enables broad grants with precise carve-outs:
// Grant broad read, but hard-block a sensitive resource:
{ resource: '*', action: 'read', effect: 'allow' }
{ resource: 'salary-record', action: 'read', effect: 'deny' } // winsResource Hierarchies
Encode hierarchy in the resource segment so wildcards can express containment: project:acme:invoice lets project:acme:* grant everything under one project without touching another. Keep the separator consistent across your whole schema.
A Reference Schema
| Resource | Actions | Notes |
|---|---|---|
| invoice | read, create, update, delete | Core CRUD; delete often deny-carved for non-admins |
| report | read, export | Export is higher-privilege than read |
| user | read, invite, remove | Invite/remove are admin-tier |
| billing | read, manage | Coarse by design; split if needed |
| apikey | read, create, rotate, revoke | Credential lifecycle actions |
How WardenAuth Implements This
WardenAuth uses resource + action permissions with wildcard matching and deny-wins resolution natively — the exact primitives above. Try it free and model your schema in the dashboard.