All your permission logic in one place. Type-safe. Multi-tenant. Explainable.
Stable 1.0 — semver guarantees apply from this release. See API stability policy.
Most auth libraries give you create, read, update, delete and call it a day. Your app has invoice:approve, users are admin in one tenant and viewer in another, and when access breaks nobody can tell you why without grepping the codebase.
Sentinel replaces scattered role checks with a single policy engine — domain actions instead of CRUD, tenant-scoped roles by default, and every decision tells you exactly which rule matched and why.
Zero dependencies. ~1,800 lines. 1:1 test-to-code ratio.
Documentation: vegtelenseg.github.io/sentinel · edit on GitHub
For AI agents: AGENTS.md — compact reference (read this instead of the full docs tree). Index: llms.txt · llms.txt on docs site
| Start here | |
|---|---|
| New to Sentinel | What is Sentinel? |
| Five-minute setup | Quickstart |
| How decisions are made | How evaluation works |
| Try in the browser | Interactive playground |
npm install @siremzam/sentinelWithout Sentinel — scattered, fragile, no tenant awareness:
app.post("/invoices/:id/approve", async (req, res) => {
if (
user.role === "admin" ||
(user.role === "manager" && invoice.ownerId === user.id)
) {
// which tenant? who knows
}
});With Sentinel — centralized, type-safe, explainable:
import { AccessEngine, createPolicyFactory } from "@siremzam/sentinel";
import type { SchemaDefinition, Subject } from "@siremzam/sentinel";
interface AppSchema extends SchemaDefinition {
roles: "admin" | "member";
resources: "invoice";
actions: "invoice:approve" | "invoice:read";
}
const { allow } = createPolicyFactory<AppSchema>();
const engine = new AccessEngine<AppSchema>({ schema: {} as AppSchema });
engine.addRule(
allow().roles("admin").actions("invoice:approve").on("invoice").build(),
);
const user: Subject<AppSchema> = {
id: "u1",
roles: [{ role: "admin", tenantId: "acme" }],
};
engine.evaluate(user, "invoice:approve", "invoice", {}, "acme"); // allowedProtect a route:
import { guard } from "@siremzam/sentinel/middleware/express";
app.post(
"/invoices/:id/approve",
guard(engine, "invoice:approve", "invoice", {
getSubject: (req) => req.user,
getTenantId: (req) => req.headers["x-tenant-id"],
}),
handler,
);→ Continue in the Quickstart for schema design, ABAC conditions, multitenancy, and explain().
- Type-safe schema — typos in actions fail at compile time, not in production
- Domain actions —
invoice:approve, not generic CRUD - Built-in multitenancy — per-tenant roles; optional
strictTenancy explain()— per-rule traces when debugging access- Audit hooks —
onDecision+toAuditEntry()for structured logs - Zero dependencies — small surface, easy to review
→ Why Sentinel? · Feature comparison
| Example | Description |
|---|---|
| standalone | Engine only — evaluate, permit, explain |
| express-multi-tenant | HTTP API with tenant header |
Deny by default. Fail closed on condition errors. Frozen rules. See Security model and SECURITY.md.