Skip to main content

Authorization

There are two independent mechanisms, and you will usually use both.

1. Role-based (in-memory)

Roles live on the identity itself, in one of two places:

  1. metadataPublic.roles: string[] — admin-set, trusted, preferred.
  2. traits.roles: string[] — self-serve-settable, only use for low-risk roles.

metadataPublic.roles wins when both are present.

@Get('/admin/users')
@Tenant('default')
@RequireRole('admin', 'support') // admin OR support passes
listUsers() { /* … */ }

No network call, no Keto dependency — role checks are a pure function of the identity already on the request.

2. Relationship-based (Keto)

For data-scoped checks (can user X edit listing Y?), use @RequirePermission:

@Put('/listings/:id')
@RequirePermission({
namespace: 'listings',
relation: 'edit',
object: (req) => `listings:${req.params.id}`, // pure function, no I/O
})
updateListing(@Param('id') id: string) { /* … */ }

Under the hood the guard calls Keto's checkPermission with subject = 'user:' + user.id. Keto answers with a CheckPermissionResult wrapped in an Axios response (response.data.allowed); false → 403. The library unwraps that for you — your handler never sees the Axios envelope.

When to use which

DecisionUse rolesUse Keto permissions
Check is global to the service
Check depends on a specific object id
Role set is small and stable
Relationships are dynamic (ownership, sharing)
You need an audit trail of grants/revokes