@zanzojs/react
v0.2.0
Published
React bindings for Zanzo ReBAC. O(1) permission checks via ZanzoProvider and useZanzo hook.
Maintainers
Readme
@zanzojs/react
React bindings for ZanzoJS. O(1) permission checks on the frontend with zero network requests after hydration.
How it works
The server compiles a flat permission map (snapshot) once per user. The frontend receives it, hydrates it into a ZanzoProvider, and evaluates every permission check as a simple Map lookup — no graphs, no network, no re-renders.
Server: engine.load() → createZanzoSnapshot() → JSON response
Client: ZanzoProvider → useZanzo().can() → O(1) booleanInstallation
pnpm add @zanzojs/core @zanzojs/reactStep-by-Step Guide
1. Generate the snapshot on the server
On login or on each page load, compile the snapshot for the authenticated user. Always create a fresh engine per request.
import { ZanzoEngine, createZanzoSnapshot } from '@zanzojs/core';
import { schema } from './zanzo.config';
import { db, zanzoTuples } from './db';
import { eq } from 'drizzle-orm';
export async function getUserSnapshot(userId: string) {
const actor = `User:${userId}`;
// 1. Load this user's direct assignment tuples
const userTuples = await db.select()
.from(zanzoTuples)
.where(eq(zanzoTuples.subject, actor));
// 2. Load structural tuples (the skeleton of your graph)
// This allows the engine to walk paths like "Document -> folder -> Folder"
const structuralTuples = await db.select()
.from(zanzoTuples)
.where(inArray(zanzoTuples.relation, ['folder', 'workspace', 'parent']));
// Fresh engine per request — never reuse a shared instance across requests
const requestEngine = new ZanzoEngine(schema);
requestEngine.load([...userTuples, ...structuralTuples]);
return createZanzoSnapshot(requestEngine, actor);
}[!IMPORTANT] Why structuralTuples? If your schema uses nested paths (e.g.
folder.admin), the engine needs the relationship between aDocumentand itsFolderto evaluate the path. If you only loadUser:alice -> editor -> Folder:1, the engine won't know which documents belong to that folder unless you also load theDocument:A -> folder -> Folder:1tuples.
Never reuse the engine across requests. A shared engine would accumulate tuples from multiple users. Always instantiate a new
ZanzoEngineper request.
2. Wrap your app with ZanzoProvider
'use client';
import { ZanzoProvider } from '@zanzojs/react';
interface AppLayoutProps {
children: React.ReactNode;
snapshot: Record<string, string[]>;
}
export default function AppLayout({ children, snapshot }: AppLayoutProps) {
return (
<ZanzoProvider snapshot={snapshot}>
{children}
</ZanzoProvider>
);
}3. Check permissions in any client component
'use client';
import { useZanzo } from '@zanzojs/react';
export function DocumentActions({ documentId }: { documentId: string }) {
const { can } = useZanzo();
return (
<div>
{can('read', `Document:${documentId}`) && <ReadButton />}
{can('write', `Document:${documentId}`) && <EditButton />}
{can('delete', `Document:${documentId}`) && <DeleteButton />}
</div>
);
}4. List accessible resources (O(n))
'use client';
import { useZanzo } from '@zanzojs/react';
export function DocumentList() {
const { listAccessible } = useZanzo();
// O(n) — iterates the snapshot. Use for rendering lists, not in tight loops.
const docs = listAccessible('Document');
return (
<ul>
{docs.map(({ object, actions }) => (
<li key={object}>
{object} — {actions.join(', ')}
</li>
))}
</ul>
);
}
can()is O(1).listAccessible()is O(n). Usecan()for individual checks inside render loops. UselistAccessible()to build lists of accessible resources.
Keeping the snapshot fresh
The snapshot reflects permissions at the time it was compiled. If permissions change after compilation, the client snapshot becomes stale.
Recommended strategies:
Re-fetch on critical routes — Force a fresh snapshot on sensitive pages:
// In a Next.js Server Component
const snapshot = await getUserSnapshot(userId); // always freshInvalidate on permission change — When granting or revoking access, invalidate the cached snapshot immediately:
await redis.del(`snapshot:${userId}`);TTL-based revalidation — Cache the snapshot with a short TTL (e.g. 5 minutes) and revalidate in the background.
Documentation
For backend setup and database adapters, see the ZanzoJS Monorepo.
