Subjects and roles
A subject is the entity requesting access — almost always an authenticated user, but the type does not require a human. A service account or API key wrapper can be a subject as long as it has an id and roles.
Subject shape
import type { Subject } from "@siremzam/sentinel";
const user: Subject<AppSchema> = {
id: "user-42",
roles: [
{ role: "admin", tenantId: "acme" },
{ role: "viewer", tenantId: "globex" },
],
attributes: {
department: "finance",
},
};| Field | Purpose |
|---|---|
id | Stable identifier — used in conditions and audit entries |
roles | List of role assignments (see below) |
attributes | Optional bag of extra data for ABAC conditions |
Role assignments
Each assignment links a role from your schema to an optional tenant:
{ role: "admin", tenantId: "acme" }
{ role: "member" } // global — applies in every tenant contextGlobal roles
A role without tenantId is global. When evaluating with any tenantId, global roles are included in the resolved role set.
Use global roles sparingly — they are powerful and easy to misuse. Platform super-admin is a typical case.
Tenant-scoped roles
When you pass tenantId to evaluate(), only assignments matching that tenant plus global roles are considered.
Why: A user who is admin at Company A must not receive admin powers when the request context is Company B.
Role resolution and hierarchy
Before rules are matched, the engine:
- Filters assignments by
tenantId(if provided) - Collects role names
- Expands via role hierarchy if configured (
admin→ alsomanager,member, …)
import { RoleHierarchy } from "@siremzam/sentinel";
const hierarchy = new RoleHierarchy<AppSchema>()
.define("admin", ["manager"])
.define("manager", ["member"]);
const engine = new AccessEngine<AppSchema>({
schema: {} as AppSchema,
roleHierarchy: hierarchy,
});Mapping from your auth layer
Sentinel does not load users from a database. Your authentication middleware or session loader should produce a Subject:
function toSubject(dbUser: DbUser): Subject<AppSchema> {
return {
id: dbUser.id,
roles: dbUser.memberships.map(m => ({
role: m.role as AppSchema["roles"],
tenantId: m.orgId,
})),
};
}Keep this mapping in one module — every route and job should use the same shape.