npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sentinelz

v1.1.0

Published

Unified Casbin RBAC/ABAC wrapper with SQL + MongoDB adapters and bundled migrations

Readme

Sentinelz

What is this?

sentinelz wraps Apache Casbin — the open-source library that handles RBAC/ABAC authorization logic ("who can do what") — and removes the usual database setup pain around it.

Normally, using Casbin with a database means picking a specific adapter package (a TypeORM adapter, a Prisma adapter, a Mongoose adapter, ...), installing it separately, wiring it up, and manually creating the table Casbin needs. sentinelz removes that: you pick sql or mongo, give it a connection string, and it:

  1. Creates its own table/collection automatically — no manual CREATE TABLE
  2. Gives you one simple class (Sentinelz) to check permissions and manage roles
  3. Works completely independently of whatever database/ORM your app already uses for its own data — it doesn't integrate with your existing tables, it just owns one small sentinelz_rules table off to the side

Install

npm install sentinelz
# plus the driver for your dialect/database:
npm install knex pg        # sql adapter, Postgres
npm install knex mysql2    # sql adapter, MySQL
npm install knex sqlite3   # sql adapter, SQLite
npm install mongoose       # mongo adapter
npm install redis          # optional: redis cache

Casbin itself ships as part of sentinelz — you never install or import it directly. The adapter/cache packages above are optional dependencies: install only what your chosen adapter/cache needs.

How you'd actually use it

Say you're building an app where users can read/write "articles", and some users are admins. You don't write any Casbin config for this — sentinelz ships a sensible default (plain role-based: subject, object, action + roles) and uses it automatically. Just connect and start calling methods on the instance:

import { SentinelzFactory } from 'sentinelz';

const sentinelz = await SentinelzFactory.create({
  adapter: 'sql',
  adapterConfig: {
    client: 'pg', // or 'mysql2', 'sqlite3'
    connection: process.env.DATABASE_URL,
  },
});
// The sentinelz_rules table is created automatically here — nothing to set up manually.

// Give alice permission to write articles
await sentinelz.addPolicy('alice', 'articles', 'write');

// Make bob an admin, and give admins permission to write articles
await sentinelz.addPolicy('admin', 'articles', 'write');
await sentinelz.addRole('bob', 'admin');

// Now check permissions anywhere in your app:
await sentinelz.enforce('alice', 'articles', 'write'); // true (direct policy)
await sentinelz.enforce('bob', 'articles', 'write'); // true (via admin role)
await sentinelz.enforce('carol', 'articles', 'write'); // false (no policy, no role)

Use it in a route, right before letting the request through:

app.patch('/articles/:id', async (req, res) => {
  const allowed = await sentinelz.enforce(req.user.id, 'articles', 'write');
  if (!allowed) return res.status(403).send('Forbidden');
  // ...update the article
});

If you use NestJS, there's a guard + decorator that does this declaratively — see the NestJS section below.

That's the whole workflow: SentinelzFactory.create() once, add policies/roles as your app needs them (usually from an admin panel or a seed script), and call enforce() wherever you need a yes/no permission check. You interact with one instance and its methods — nothing about Casbin or the underlying database leaks into your code.

Custom permission model (advanced / ABAC)

The default model covers plain RBAC. If you need attribute-based rules or a custom matcher, pass your own Casbin model file — this is the only case where you'd need to touch Casbin's own config format:

const sentinelz = await SentinelzFactory.create({
  modelPath: './rbac_model.conf', // your own model — omit this field to use the bundled default
  adapter: 'sql',
  adapterConfig: { client: 'pg', connection: process.env.DATABASE_URL },
});
# rbac_model.conf — this is exactly what's bundled as the default, shown here as a starting point
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

Adapters

One adapter per storage family — not per ORM:

| Adapter | Backed by | Covers | | ------- | ---------------------------------- | ------------------------------------------------------------------------ | | sql | Knex | PostgreSQL, MySQL, SQLite, MS SQL Server — pick the dialect via client | | mongo | Mongoose | MongoDB |

// Postgres
{ adapter: 'sql', adapterConfig: { client: 'pg', connection: process.env.DATABASE_URL } }

// MySQL
{ adapter: 'sql', adapterConfig: { client: 'mysql2', connection: process.env.DATABASE_URL } }

// SQLite (zero-infra, great for tests)
{ adapter: 'sql', adapterConfig: { client: 'sqlite3', connection: { filename: './dev.sqlite3' } } }

// MongoDB
{ adapter: 'mongo', adapterConfig: { uri: process.env.MONGODB_URI } }

Postgres/MSSQL schema (namespace) support

{
  adapter: 'sql',
  adapterConfig: {
    client: 'pg',
    connection: process.env.DATABASE_URL,
    schema: 'tenant_a',            // isolates this consumer's rows under tenant_a.sentinelz_rules
    createSchemaIfMissing: true,   // opt-in CREATE SCHEMA IF NOT EXISTS during migrate()
  },
}

Ignored (with a warning) for sqlite3/mysql2, which have no schema concept.

Migrations

Both adapters own their schema and run it automatically on SentinelzFactory.create() (migrate: { auto: true } by default). No manual CREATE TABLE, ever. Set migrate: { auto: false } and call sentinelz.migrate() yourself if you'd rather run migrations as an explicit deploy step.

Config sources

// Explicit config (shown above), or:
const sentinelz = await SentinelzFactory.createFromEnv(); // reads SENTINELZ_* env vars
const sentinelz = await SentinelzFactory.createFromFile(
  './sentinelz.config.json',
);

See .env.example for the full SENTINELZ_* environment variable reference.

Optional Redis (or in-memory) caching

Caches enforce() results; a decorator over the core enforcer, not baked into it — fully disabled by default, and disabling it changes nothing about correctness.

const sentinelz = await SentinelzFactory.create({
  adapter: 'sql',
  adapterConfig: { client: 'pg', connection: process.env.DATABASE_URL },
  cache: {
    enabled: true,
    type: 'redis', // or 'memory' for an in-process fallback
    ttl: 300,
    redisUrl: process.env.REDIS_URL,
  },
});

Any policy or role mutation (addPolicy, removePolicy, addRole, ...) invalidates the whole cache — correctness over partial-invalidation cleverness.

NestJS

import { Module } from '@nestjs/common';
import { SentinelzModule } from 'sentinelz/nestjs';

@Module({
  imports: [
    SentinelzModule.register({
      adapter: 'sql',
      adapterConfig: { client: 'pg', connection: process.env.DATABASE_URL },
    }),
  ],
})
export class AppModule {}
import { Controller, Patch, Param, UseGuards } from '@nestjs/common';
import { SentinelzGuard, CheckAbility } from 'sentinelz/nestjs';

@Controller('articles')
@UseGuards(SentinelzGuard)
export class ArticlesController {
  @Patch(':id')
  @CheckAbility({ action: 'write', resource: 'articles' })
  async updateArticle(@Param('id') id: string) {
    // Only reached if req.user.id has write permission on articles
    return { success: true, id };
  }
}

sentinelz/nestjs is a separate export path — a non-Nest consumer never pulls in @nestjs/common.

API

What ...args actually means

A handful of methods below take ...args: string[] instead of named parameters. That's not vagueness — it's because the shape depends on which Casbin model you're using (see Custom permission model), so the method can't hardcode parameter names that would only be right for one model.

With the bundled default model (which is what you're using unless you passed a custom modelPath), every one of these is always a (subject, object, action) triple:

await sentinelz.enforce('alice', 'articles', 'write');
//                        ^subject  ^object    ^action
// "Is alice allowed to write articles?"

await sentinelz.addPolicy('alice', 'articles', 'write');
// "Grant alice permission to write articles" — same 3 positions, different verb

If you bring your own ABAC model, the number and meaning of the arguments follows whatever you defined in that model's [request_definition]/[policy_definition] instead — sentinelz just passes them through to Casbin.

Sentinelz (the instance you get back from SentinelzFactory.create())

| Method | What it does | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | enforce(...args) → boolean | The core check: is this request allowed? | | addPolicy(...args) → boolean | Grants a permission directly to a subject (not via a role) | | removePolicy(...args) → boolean | Revokes a permission added with addPolicy | | updatePolicy(oldRule, newRule) → boolean | Swaps one policy rule for another in one call | | getPolicy() → string[][] | Every policy rule currently loaded | | getPoliciesForUser(user) → string[][] | Rules that apply directly to one subject | | addRole(user, role) → boolean | Assigns a role to a subject — grant the role itself permissions via addPolicy(role, obj, act), and every subject with that role inherits them | | removeRole(user, role) → boolean | Removes a role assignment (the role's own policies are untouched) | | getRolesForUser(user) → string[] | Every role assigned to a subject | | getUsersForRole(role) → string[] | Every subject assigned to a role | | getAllRoles() → string[] | Every role name that appears anywhere in the current policies | | loadPolicy() → void | Reloads policies from storage into memory | | loadFilteredPolicy(filter) → void | Loads only the rules matching filter instead of the whole table (see Filtered policy loading) | | isFiltered() → boolean | True while a filtered subset is loaded | | savePolicy() → void | Persists the current in-memory policies to storage | | clearPolicy() → void | Empties the in-memory policy set (storage untouched until you savePolicy()) | | migrate() → void | Runs the adapter's schema setup explicitly (see Migrations) | | close() → void | Closes the underlying database connection |

Full type signatures (with per-method doc comments — these show up on hover in your editor too) live in src/core/enforcer.interface.ts.

Filtered policy loading

By default every rule is loaded into memory. For large rule tables, load just what you need:

// Rules for alice, plus the role (`g`) rows and role permissions she inherits
await sentinelz.loadFilteredPolicy({
  ptype: ['p', 'g'],
  v0: ['alice', 'admin'],
});

A PolicyFilter has ptype ('p' rules vs 'g' role assignments) and v0..v5 (the rule's positional values — with the default model, v0 is the subject, v1 the object, v2 the action). Each field is a list of accepted values; fields are ANDed; omitted fields don't constrain.

  • Include the g rows and role subjects. If you filter only p rows for alice, her role assignments aren't loaded and inherited permissions silently disappear.
  • savePolicy() throws while filtered (saving would overwrite storage with the loaded subset). Call loadPolicy() to return to the full set.
  • addPolicy/removePolicy/addRole/… still write through to storage as usual.
  • This is runtime-only: there is no startup/config option, so the instance always boots with the full policy set.

SentinelzFactory

class SentinelzFactory {
  static create(config: ISentinelzConfig): Promise<ISentinelz>;
  static createFromEnv(modelPath?: string): Promise<ISentinelz>;
  static createFromFile(configPath: string): Promise<ISentinelz>;
}

Errors thrown are typed: AdapterNotFoundError, AdapterInitializationError, MigrationError, EnforcementError, PolicyManagementError — all extend SentinelzError.

Examples

Runnable examples live under examples/: sql-sqlite-example, sql-postgres-example, mongo-example, nestjs-example.

Development

yarn install
yarn build
yarn test              # unit tests (mocked/SQLite, no external services)
docker compose up -d   # postgres, mysql, mongo, redis
yarn test:integration

Releases are fully automated — see docs/RELEASING.md.

License

MIT