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

@zanzojs/core

v0.2.0

Published

Isomorphic ReBAC Authorization for the Modern Web. Inspired by Google Zanzibar.

Downloads

432

Readme

@zanzojs/core

npm version TypeScript Strict Edge Compatible

The core engine of the ZanzoJS ReBAC ecosystem. 0 dependencies, strictly typed, edge-compatible.

ZanzoJS implements the Google Zanzibar pattern for TypeScript: define your permission model once as a schema, store relationships as tuples in your database, and evaluate permissions at request time with zero network overhead on the frontend.

How it works

Database (tuples)
  → engine.load()          → createZanzoSnapshot()  → Redis (optional cache)
                                                     → Frontend → ZanzoProvider → can() O(1)
  → @zanzojs/drizzle        → SQL filtered queries (for large datasets)

Installation

pnpm add @zanzojs/core

Production Flow (Start Here)

This is the canonical pattern for production use. Read this before anything else.

Step 1: Define your schema (once, at module level)

The schema is immutable. Define it once and reuse it across all requests.

import { ZanzoBuilder, ZanzoEngine } from '@zanzojs/core';

export const schema = new ZanzoBuilder()
  .entity('User', { 
    actions: [] as const, 
    relations: {} 
  })
  .entity('Document', {
    actions: ['read', 'write', 'delete'] as const,
    relations: { 
      owner: 'User', 
      editor: 'User',
      viewer: 'User',
      folder: 'Folder',
    },
    permissions: {
      delete: ['owner'],
      write:  ['owner', 'editor'],
      read:   ['owner', 'editor', 'viewer', 'folder.admin'],
    },
  })
  .entity('Folder', {
    actions: ['read'] as const,
    relations: { admin: 'User' },
    permissions: { read: ['admin'] },
  })
  .build();

Key concept: folder.admin is a nested permission path. It means "the admin of the folder that contains this document". This requires expandTuples() at write time — see @zanzojs/drizzle.

Step 2: Load tuples for the current user only

Never load all tuples for all users. On each request, load only the tuples relevant to the authenticated user.

import { ZanzoEngine, createZanzoSnapshot } from '@zanzojs/core';
import { db, zanzoTuples } from './db';
import { eq } from 'drizzle-orm';

export async function getSnapshot(userId: string) {
  // Load ONLY this user's tuples from the database
  const rows = await db.select()
    .from(zanzoTuples)
    .where(eq(zanzoTuples.subject, `User:${userId}`));

  // Create a fresh engine per request — never reuse across requests
  const engine = new ZanzoEngine(schema);
  engine.load(rows);

  // Compile a flat permission map for the frontend
  return createZanzoSnapshot(engine, `User:${userId}`);
}

The snapshot looks like:

{
  "Document:doc1": ["read", "write", "delete"],
  "Document:doc2": ["read"],
  "Folder:folder1": ["read"]
}

Step 3: Cache the snapshot (recommended)

Recompiling the snapshot on every request is fast, but caching reduces database load. Invalidate the cache when permissions change.

// Recommended pattern — implement in your app, not in ZanzoJS
async function getCachedSnapshot(userId: string) {
  const cached = await redis.get(`snapshot:${userId}`);
  if (cached) return JSON.parse(cached);

  const snapshot = await getSnapshot(userId);
  await redis.set(`snapshot:${userId}`, JSON.stringify(snapshot), 'EX', 3600);
  return snapshot;
}

// Invalidate when permissions change
async function revokeAccess(subject: string, relation: string, object: string) {
  await collapseTuples({ ... });
  await db.delete(zanzoTuples).where(...);
  await redis.del(`snapshot:${subject}`); // invalidate immediately
}

Step 4: Send the snapshot to the frontend

// Next.js API route or Server Component
export async function GET(request: Request) {
  const { userId } = await getSession(request);
  const snapshot = await getCachedSnapshot(userId);
  return Response.json(snapshot);
}

The frontend consumes the snapshot via @zanzojs/react. See that package for details.


Write Operations: expandTuples and collapseTuples

When you grant access via a nested permission path (e.g. folder.admin), you must materialize the derived tuples at write time. This is what makes read-time evaluation fast.

import { expandTuples, collapseTuples } from '@zanzojs/core';

// GRANT — materialize derived tuples when writing to DB
async function grantAccess(subject: string, relation: string, object: string) {
  const baseTuple = { subject, relation, object };
  
  const derived = await expandTuples({
    schema: engine.getSchema(),
    newTuple: baseTuple,
    fetchChildren: async (parentObject, relation) => {
      const rows = await db.select({ object: zanzoTuples.object })
        .from(zanzoTuples)
        .where(and(
          eq(zanzoTuples.subject, parentObject),
          eq(zanzoTuples.relation, relation),
        ));
      return rows.map(r => r.object);
    },
  });

  await db.insert(zanzoTuples).values([baseTuple, ...derived]);
  await redis.del(`snapshot:${subject}`); // invalidate cache
}

// REVOKE — remove derived tuples symmetrically
async function revokeAccess(subject: string, relation: string, object: string) {
  const baseTuple = { subject, relation, object };

  const derived = await collapseTuples({
    schema: engine.getSchema(),
    revokedTuple: baseTuple,
    fetchChildren: async (parentObject, relation) => {
      const rows = await db.select({ object: zanzoTuples.object })
        .from(zanzoTuples)
        .where(and(
          eq(zanzoTuples.subject, parentObject),
          eq(zanzoTuples.relation, relation),
        ));
      return rows.map(r => r.object);
    },
  });

  for (const tuple of [baseTuple, ...derived]) {
    await db.delete(zanzoTuples).where(and(
      eq(zanzoTuples.subject, tuple.subject),
      eq(zanzoTuples.relation, tuple.relation),
      eq(zanzoTuples.object, tuple.object),
    ));
  }
  
  await redis.del(`snapshot:${subject}`); // invalidate cache
}

Engine API Reference

engine.load(tuples)

Hydrates the engine with tuples from the database. Use this in production. Silently skips expired tuples during loading.

const engine = new ZanzoEngine(schema);
engine.load(rowsFromDB);

engine.for(actor).can(action).on(resource)

Evaluates a permission. Returns boolean.

engine.for('User:alice').can('write').on('Document:doc1') // true or false

engine.for(actor).listAccessible(entityType)

Returns all accessible objects of the given type with their allowed actions.

Complexity: O(n) where n is the number of objects of that type in the engine index. Use sparingly for large datasets.

const docs = engine.for('User:alice').listAccessible('Document')
// → [{ object: 'Document:doc1', actions: ['read', 'write'] }]

engine.grant(relation).to(subject).on(object)

Adds a tuple to the engine's in-memory index.

When to use: Unit tests, development seeds, and permission simulation sandboxes only. In production, write permissions directly to your database and use expandTuples(). Mutations via grant() are ephemeral and disappear when the request ends.

// ✅ Good — in tests
engine.grant('owner').to('User:alice').on('Document:doc1')

// ❌ Wrong — in a production API route (mutation is lost after the request)
engine.grant('owner').to('User:alice').on('Document:doc1') // not persisted

With expiration:

engine.grant('viewer')
  .to('User:bob')
  .on('Document:doc1')
  .until(new Date('2026-12-31'))

engine.revoke(relation).from(subject).on(object)

Removes a tuple from the engine's in-memory index. Same constraints as grant().

engine.cleanup()

Removes expired tuples from the index. Returns the count removed.

When to use: Only for long-lived engine instances like background workers or WebSocket servers. In per-request flows, engine.load() already skips expired tuples and cleanup() will always return 0.

Field-level granularity

Permissions can target specific fields within an object using the # separator.

// Grant edit access to a specific field only
engine.grant('editor').to('User:alice').on('Review:cert1#strengths')

// Field permissions are independent — they do NOT inherit from the parent object
engine.for('User:alice').can('edit').on('Review:cert1#strengths') // true
engine.for('User:alice').can('edit').on('Review:cert1') // false (different object)

Migrating from v0.1.x

// v0.1.x — still works but deprecated, will be removed in v1.0.0
engine.addTuple({ subject: 'User:alice', relation: 'owner', object: 'Document:doc1' })
engine.addTuples(rows)
engine.can('User:alice', 'read', 'Document:doc1')

// v0.2.0
engine.grant('owner').to('User:alice').on('Document:doc1') // for tests only
engine.load(rows) // for DB hydration
engine.for('User:alice').can('read').on('Document:doc1')

Documentation

For database adapters and React bindings, see the ZanzoJS Monorepo.