tsbouncerpreview

TypeScript-native authorization · Apache-2.0 · ESM only

Stop guessing who can see what.

tsbouncer is an authorization graph that runs inside your app — one model, one set of tuples, over storage you already own. Every decision returns not just allow or deny, but the reason why.

npx tsx src/verify.ts — the Hono guide's assertion script, abridged
$ npm i @tsbouncer/tsbouncer
ok   a known user reads what they own                     200
ok   a role grants access to every document it is attached to 200
ok   a viewer may read but not write                      403
ok   an editor may not change who has access              403
ok   a grant whose row was hard-deleted is a 404          404
ok   why returns the decision tree, not a guess           200

  19 of 19 scenarios passed

Declare it. Ask it. Get a reason.

The whole library is three calls. The model is code, the grants are data, and the answer cites its sources.

src/model.ts

defineType('document', {
  relations: {
    owner: relation('user'),
    editor: relation('user')
      .or(relation('role', { through: 'holder' })),
  },
  permissions: {
    read: permission.or('owner', 'editor'),
    write: permission.or('owner', 'editor'),
    manage: permission.or('owner'),
  },
})

src/verify.ts

// Bob holds the editor role. Carol only views.
await authz.can('user:bob', 'document.read', 'document:1')
// → true — editor, through role:acme:editor#holder

await authz.can('user:carol', 'document.write', 'document:1')
// → false — a viewer may read but not write

await authz.explain({ subject: 'user:carol',
  permission: 'document.write', resource: 'document:1' })
// → the tree: which edges held, which missed,
//    and how many store reads it took

Why teams pick it up

Each of these is a checked guarantee, not a slogan — the contract report runs against a real store on every build.

In-process, not over the wire

The evaluator runs inside your request handler against storage you already own. No sidecar, no extra hop, no new failure domain.

Every decision explains itself

explain() returns the tree that produced the answer — which edges held, which missed, how many reads it took. Support stops guessing.

Fail closed, everywhere

Unknown condition, missing key, thrown predicate, exhausted budget: all deny. There is no permissive fallback anywhere.

Budgets, not hope

Depth, node, and deadline ceilings per request. Exhaustion denies and reports truncated, so a partial answer never looks complete.

Lists that refuse to invent

A wildcard grant reports a symbolic set, not a fabricated roster. listResources always says whether its answer is whole.

Conditions as code

Predicates are TypeScript functions beside your domain logic. Tuple bindings win; the request only fills the gaps.

Coming from Google Zanzibar or OpenFGA? Readthe comparison catalog — including the rows where this library is the worse answer.