Skip to content
tsbouncerpreview
guide

Getting started

Install it, declare a model, and answer one question about one document.

Install

Shell
npm i @tsbouncer/tsbouncer @tsbouncer/in-memory   # kernel plus the process-local backend

tsbouncer is the kernel and two ports — nothing else. Backends are separate plugin packages: @tsbouncer/in-memory and @tsbouncer/json-file need nothing external, @tsbouncer/redis needs a server, @tsbouncer/kysely, @tsbouncer/drizzle, and @tsbouncer/prisma wrap the client you already have, and @tsbouncer/defaults picks one for you. Install exactly the backends you use; importing the root never loads one you did not ask for.

ESM only. There is no CommonJS build, and require() will not work. Node 20.11 or newer.

Declare a model

The model is code, validated at runtime when defineModel is called. A relation that names a type which does not exist throws immediately, not on the first request that happens to use it.

TypeScript
import { defineModel, defineType, permission, relation } from '@tsbouncer/tsbouncer';

const model = defineModel({
  types: {
    user: defineType({}),
    team: defineType({ relations: { member: relation(['user']) } }),
    document: defineType({
      relations: {
        owner: relation(['user']),
        editor: relation('user').or(relation('team', { through: 'member' })),
      },
      permissions: {
        read: permission.or('owner', 'editor'),
      },
    }),
  },
});

Connect a store

TypeScript
import { createAuthz } from '@tsbouncer/tsbouncer';
import { memoryStore } from '@tsbouncer/in-memory';

const authz = createAuthz({ model, store: memoryStore() });

memoryStore is process-local and is the right choice for tests. For anything else, see Stores.

Write and ask

TypeScript
await authz.write([
  { subject: 'user:alice', relation: 'owner', resource: 'document:1' },
  { subject: 'team:eng#member', relation: 'editor', resource: 'document:2' },
  { subject: 'user:alice', relation: 'member', resource: 'team:eng' },
]);

await authz.can('user:alice', 'document.read', 'document:1'); // true  — owner
await authz.can('user:alice', 'document.read', 'document:2'); // true  — team member
await authz.can('user:bob', 'document.read', 'document:2'); // false — not a member

Three calls, and the third one is the interesting one: alice’s access to document:2 comes from a membership tuple and a userset edge, not from a tuple that names her on the document.

Use it from a route

The library has no opinion about your framework, so this is the whole integration:

TypeScript
import { isAuthorizationError } from '@tsbouncer/tsbouncer';

app.get('/api/documents/:id', async (req, res) => {
  const subject = `user:${req.header('x-user-id')}`;

  try {
    await authz.assert({
      subject,
      permission: 'document.read',
      resource: `document:${req.params.id}`,
    });
  } catch (error) {
    if (isAuthorizationError(error)) {
      res.status(403).json({ error: 'forbidden' });
      return;
    }
    throw error; // a store or model failure is NOT a denial
  }

  res.json(await loadDocument(req.params.id));
});

Next

  • The model — every shape the evaluator understands.
  • Data — the reference format, and what write-time validation catches.
  • HTTP integration — Express, Hono, Fastify, and where to put the check.