# Authorization
Application authorization
- Package: @bijectionhq/authz
- Category: Authentication
- Status: Reference
- License: Apache-2.0
- Bijection source (repository access required): https://github.com/bijectionhq/authz/tree/3bb0fa19b552139b9d82afc4a2e329b62787f076
- Upstream: https://github.com/dbjpanda/convex-authz/tree/0717b7fbc0c0a6760261f454ba8000124a670019
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/authz
## Get the source
Bijection repository access is required. Request access at https://bijection.com/contact.
```sh
git clone https://github.com/bijectionhq/authz.git
cd authz
git checkout 3bb0fa19b552139b9d82afc4a2e329b62787f076
```
## Before using this component
Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.
## Upstream package documentation
Recorded 2026-10-23. This describes @djpanda/convex-authz; check the Bijection adaptation separately.
A comprehensive authorization component that implements RBAC, ABAC, and ReBAC (relationship-based access control) with O(1) indexed permission lookups. Inspired by Google Zanzibar, it provides role assignments, attribute-based policies, and relationship graph traversal for complex permission scenarios. Pre-computes permissions into indexed tables for instant checks while supporting scoped permissions, expiring grants, and real-time updates through Convex reactivity.
### Use cases
- Building multi-tenant SaaS applications where users need different roles (admin, editor, viewer) across different organizations or teams
- Implementing document or project-based permissions where access depends on relationships like team membership or resource ownership
- Creating complex authorization rules that combine user attributes, resource properties, and organizational hierarchies
- Supporting temporary access grants with automatic expiration for contractors or time-limited collaborations
- Scaling permission checks in high-traffic applications where authorization latency must remain constant regardless of system size
### How it works
You define permissions and roles in a configuration file using `definePermissions()` and `defineRoles()`, then create an `Authz` client that connects to the component. The component supports role inheritance and composition through `inherits` and `includes` properties.
The system pre-computes all effective permissions into indexed tables (`effectivePermissions`, `effectiveRoles`, `effectiveRelationships`) enabling O(1) lookups via `authz.can()` and `authz.require()`. You assign roles with `assignRole()` and create relationships with `createTuple()` for ReBAC scenarios.
React integration works through an `AuthzProvider` that wraps your Convex queries, providing hooks like `useCanUser()` and components like `PermissionGate`. Permission checks support wildcard patterns (`documents:*`, `*:read`) and scoped contexts where roles apply to specific resources rather than globally.
The component maintains audit logs and supports expiring grants by accepting expiration timestamps. All changes trigger Convex's reactive updates, automatically refreshing UI components when permissions change.
### Upstream README
# @djpanda/convex-authz
A comprehensive, production-ready authorization component for [Convex](https://convex.dev) featuring **RBAC**, **ABAC**, and **ReBAC** with **O(1) indexed lookups**, inspired by [Google Zanzibar](https://research.google/pubs/pub48190/).
## Features
| Feature | Description |
| ---------------------- | ------------------------------------------------------ |
| **RBAC** | Role-Based Access Control with scoped roles |
| **ABAC** | Attribute-Based Access Control with custom policies |
| **ReBAC** | Relationship-Based Access Control with graph traversal |
| **O(1) Lookups** | Pre-computed permissions for instant checks |
| **Type Safety** | Full TypeScript support with type-safe permissions |
| **Audit Logging** | Track all permission changes and checks |
| **Scoped Permissions** | Resource-level access control |
| **Expiring Grants** | Time-limited role assignments and permissions |
| **Convex Native** | Built specifically for Convex, with real-time updates |
## Terminology
| Term | Definition |
| ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| **RBAC** | Role-Based Access Control - permissions assigned via roles (admin, editor, viewer) |
| **ABAC** | Attribute-Based Access Control - permissions based on user/resource attributes (department=engineering) |
| **ReBAC** | Relationship-Based Access Control - permissions derived from relationships (member of team that owns resource) |
| **Zanzibar** | Google's global authorization system, inspiration for OpenFGA and this component |
| **Tuple** | A relationship triple: `(subject, relation, object)` e.g., `(user:alice, member, team:sales)` |
| **Scope** | Resource-level permission context, e.g., "admin of team:123" vs global "admin" |
| **Traversal** | Following relationship chains to determine inherited access |
| **O(1) Lookup** | Constant-time permission check via pre-computed indexes |
| **Permission Override** | Direct grant/deny that bypasses role-based permissions |
---
## Installation
```bash
npm install @djpanda/convex-authz
```
---
## Quick Start
### 1. Register the Component
```typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import authz from "@djpanda/convex-authz/convex.config";
const app = defineApp();
app.use(authz);
export default app;
```
### 2. Define Your Permissions and Roles
```typescript
// convex/authz.ts
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { components } from "./_generated/api";
// Step 1: Define permissions
const permissions = definePermissions({
documents: {
create: true,
read: true,
update: true,
delete: true,
},
settings: {
view: true,
manage: true,
},
});
// Step 2: Define roles
const roles = defineRoles(permissions, {
admin: {
documents: ["create", "read", "update", "delete"],
settings: ["view", "manage"],
},
editor: {
documents: ["create", "read", "update"],
settings: ["view"],
},
viewer: {
documents: ["read"],
},
});
// Step 3: Create the authz client
export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
```
#### Role inheritance and composition
Roles can be defined in terms of other roles to avoid repeating permission lists:
- `**inherits**` – one parent role; effective permissions = parent’s permissions ∪ this role’s direct permissions.
- `**includes**` – multiple roles; effective permissions = union of all included roles’ permissions ∪ this role’s direct permissions.
Example with inheritance (admin > editor > viewer):
```typescript
const roles = defineRoles(permissions, {
viewer: { documents: ["read"] },
editor: { inherits: "viewer", documents: ["create", "update"] },
admin: { inherits: "editor", documents: ["delete"], settings: ["manage"] },
});
```
Example with composition (combine roles):
```typescript
const roles = defineRoles(permissions, {
editor: { documents: ["create", "read", "update"] },
billing_admin: { billing: ["view", "manage"] },
billing_manager: { includes: ["editor", "billing_admin"], settings: ["view"] },
});
```
**Note:** `inherits` and `includes` are reserved keys in role definitions; do not use them as permission resource names.
### 3. Use in Your Functions
```typescript
// convex/documents.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { authz } from "./authz";
import { getAuthUserId } from "@convex-dev/auth/server";
export const updateDocument = mutation({
args: { docId: v.id("documents"), content: v.string() },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
// Check permission (throws if denied)
await authz.require(ctx, userId, "documents:update");
// Or with scope
await authz.require(ctx, userId, "documents:update", {
type: "document",
id: args.docId,
});
// Proceed with update...
},
});
```
---
## Unified Authz v2
v2 consolidates everything into a single `Authz` class. If you previously used `IndexedAuthz`, just rename it — the constructor signature is identical.
### What changed
- **One class**: `Authz` replaces both the original `Authz` and `IndexedAuthz`. O(1) reads via pre-computed effective tables are now the default.
- **ReBAC on `Authz`**: `hasRelation`, `addRelation`, and `removeRelation` are available directly on every `Authz` instance.
- **ABAC policy types**: Policies accept a `type` field (`"static"` or `"deferred"`). In the current implementation, both types are evaluated at read-time when `can()` is called — Convex mutations cannot call queries, so write-time evaluation is not possible. The `type` field is reserved for future optimization but currently has no behavioral difference.
- `**canWithContext()`**: Check deferred ABAC policies that need runtime context (e.g. IP address, time of day).
- `**recomputeUser()**`: Rebuild a user's effective-permissions table on demand — useful after a schema change or post-deploy migration.
- `**withTenant()**`: Get a scoped copy of the client bound to a different tenant for cross-tenant admin operations.
### ReBAC example
```typescript
// Add a relationship
await authz.addRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: teamId });
// Check a relationship
const isMember = await authz.hasRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: teamId });
// Remove a relationship
await authz.removeRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: teamId });
```
### ReBAC → Permission Bridge
Use `defineRelationPermissions` to automatically grant permissions when relationships are created:
```typescript
import { defineRelationPermissions } from "@djpanda/convex-authz";
const authz = new Authz(components.authz, {
permissions, roles, tenantId: "my-app",
relationPermissions: defineRelationPermissions({
"document:viewer": ["documents:read"],
"document:editor": ["documents:read", "documents:update"],
"document:owner": ["documents:read", "documents:update", "documents:delete"],
}),
});
// Adding a relation automatically grants scoped permissions
await authz.addRelation(ctx, { type: "user", id: userId }, "editor", { type: "document", id: docId });
// can() now checks relation-derived permissions — no separate hasRelation() needed
const canUpdate = await authz.can(ctx, userId, "documents:update", { type: "document", id: docId }); // true
// Removing the relation revokes the permissions
await authz.removeRelation(ctx, { type: "user", id: userId }, "editor", { type: "document", id: docId });
```
### ABAC example
```typescript
// Policies are always evaluated at read-time when can() is called.
// The optional type field ("static" | "deferred") is reserved for future use
// and currently has no behavioral effect.
const policies = definePolicies({
"documents:read": {
condition: (ctx) => ctx.getAttribute("verified") === true,
message: "Only verified users can read documents",
},
"billing:export": {
condition: (ctx) => {
const hour = new Date().getUTCHours();
return hour >= 9 && hour <= 17;
},
message: "Billing exports only during business hours",
},
});
// Use canWithContext() when request context is available
const allowed = await authz.canWithContext(ctx, userId, "documents:read", undefined, {
ipAllowlisted: true,
});
```
### Post-deploy rebuild
```typescript
// Rebuild a single user's effective permissions after upgrading
await authz.recomputeUser(ctx, userId);
```
### Cross-tenant operations
```typescript
const otherTenantAuthz = authz.withTenant("other-tenant-id");
const allowed = await otherTenantAuthz.can(ctx, userId, "documents:read");
```
### Migration guide: `IndexedAuthz` → `Authz`
> **Note:** `IndexedAuthz` is no longer exported in v2. The import below will fail — just replace it with `Authz`.
```typescript
// Before (v1) — this import no longer works in v2
// import { IndexedAuthz } from "@djpanda/convex-authz";
// const authz = new IndexedAuthz(components.authz, { permissions, roles, tenantId: "my-app" });
// After (v2) — same constructor, just rename the class
import { Authz } from "@djpanda/convex-authz";
const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
```
After upgrading, run `recomputeUser()` for each existing user to backfill the effective-permissions table:
```typescript
// one-time migration mutation
export const backfillEffectivePermissions = mutation({
args: {},
handler: async (ctx) => {
const users = await ctx.db.query("users").collect();
for (const user of users) {
await authz.recomputeUser(ctx, String(user._id));
}
},
});
```
---
## React integration
The package provides React hooks and a `PermissionGate` component so your UI can check permissions and roles reactively. Your app must expose Convex queries that wrap the Authz component (e.g. `checkPermission`, `getUserRoles`). The hooks call those queries via Convex’s `useQuery`, so permission and role changes stay up to date without polling.
### 1. Expose Convex queries
Define queries that delegate to your authz client, for example:
```typescript
// convex/app.ts (or similar)
import { query } from "./_generated/server";
import { v } from "convex/values";
import { authz } from "./authz";
export const checkPermission = query({
args: {
userId: v.string(),
permission: v.string(),
scope: v.optional(v.object({ type: v.string(), id: v.string() })),
},
handler: async (ctx, args) => {
return authz.can(ctx, args.userId, args.permission, args.scope);
},
});
export const getUserRoles = query({
args: {
userId: v.string(),
scope: v.optional(v.object({ type: v.string(), id: v.string() })),
},
handler: async (ctx, args) => {
return authz.getUserRoles(ctx, args.userId, args.scope);
},
});
```
### 2. Wrap your app with AuthzProvider
Pass your Convex query refs (and optionally a default user id) to the provider:
```tsx
import { AuthzProvider } from "@djpanda/convex-authz/react";
import { api } from "./convex/_generated/api";
You cannot view documents.
; return {
// Permission checks
can(ctx, userId, permission, scope?): Promise