Skip to content

Resources and inheritance

Every grant and every check in perms.io names a resource URI. A resource URI is a /-separated path that you choose to identify something in your application. Your URIs don't need to match your app's real URLs; they only need to be consistent.

/documents
/documents/42
/documents/42/comments/7
/tenants/acme/projects/web-app

Rules for URIs

A resource URI:

  • starts with /;
  • has no empty segments, so no // and no trailing /;
  • contains no whitespace or control characters;
  • is at most 2048 bytes long;
  • contains none of these characters in any segment: ( ) [ ] { } * + ? | ^ $ \.

A request that breaks these rules is rejected with InvalidArgument. If your IDs can contain reserved characters, percent-encode each segment. The TypeScript SDK's resource() helper does this for you.

Grants apply to everything beneath them

A grant on a URI applies to that URI and every URI below it. Matching works segment by segment, so a grant on /documents covers /documents/42 but not /documents-archive.

/documents                  ← grant document.read to alice here
├── /documents/42           ✔ alice can read
│   └── /documents/42/comments/7   ✔ alice can read
└── /documents/43           ✔ alice can read
/invoices/1                 ✘ not covered
Grant on Check on Result
/documents /documents allowed
/documents /documents/42 allowed
/documents /documents/42/comments/7 allowed
/documents/42 /documents denied (grants don't flow upwards)
/documents /invoices/1 denied

Inheritance also covers resources that didn't exist when the grant was made. Grant on a tenant or a folder once, and everything you later create beneath it is covered.

The _ wildcard segment

A segment that is exactly _ in a stored grant matches exactly one segment, whatever its value:

Grant on Check on Result
/documents/_/comments /documents/42/comments allowed
/documents/_/comments /documents/42/comments/7 allowed (beneath a match)
/documents/_/comments /documents/42 denied (the grant is deeper)
/documents/_/comments /documents/42/attachments denied

In a URI being checked, _ is literal. It is not a wildcard, and it only matches grants made with _ in that position. You can't widen a check by sending _.

How a check is decided

A check asks: does principal P hold every permission in a list on every resource in a list, within one project?

  1. A permission counts if the principal holds it directly, or through a role, in a grant on the resource or on any ancestor of it (including _ matches).
  2. If every permission is held on every resource, the check passes (passed: true).
  3. Otherwise it fails (passed: false) and missing_permissions lists, per resource, the permissions that weren't found.

A failed check is a normal response, not an error. Checks fail closed: a URI with no applicable grant is denied.

Grants belong to a project. A check in one project never sees grants made in another.

Designing your URIs

  • Put the thing you grant on at the top. If you usually grant per tenant, start URIs with the tenant: /tenants/{tenantId}/....
  • Use stable IDs, not names that can change.
  • Build URIs the same way everywhere. A grant on /Documents/42 doesn't cover /documents/42.
  • Check the most specific URI for the object being accessed. Inheritance takes care of broader grants.
/tenants/acme                         ← tenant admins
/tenants/acme/projects/web-app        ← project members
/tenants/acme/projects/web-app/documents/42   ← what your code checks