Token导航 LogoToken导航

Agent Skill · 开发

convex-authz(凸授权)

用于辅助安全审计、权限检查、凭据风险、认证流程和常见漏洞排查。它适合让 Agent 梳理敏感配置、检查依赖风险、分析鉴权逻辑或生成安全复核清单。使用时不能把工具输出直接当最终结论,涉及密钥、令牌、用户数据或生产系统时,应先确认最小权限、脱敏方式和操作边界。

convex-authz 是开发类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证信息还不明确,第三方扫描已通过,按安装量在开发类的 21,746 个 Skill 里排第 11,803。

总安装 1,576周安装 67GitHub Stars 8下载 552核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:convex-authz(凸授权)
来源仓库:https://github.com/dbjpanda/convex-tenants
仓库路径:skills/convex-authz
安装命令:
npx skills add https://github.com/dbjpanda/convex-tenants --skill convex-authz
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/dbjpanda/convex-tenants --skill convex-authz

这个 Skill 是做什么的

开发类 Skill 面向写代码、改代码和排查问题,一般会读写仓库文件,部分还会执行构建或测试命令。

提供生产级授权组件,支持 RBAC、ABAC 和 ReBAC 权限模型。具备 O(1) 索引查找能力和完整的审计日志追踪功能。灵感来自 Google Zanzibar 系统,适用于需要细粒度资源控制的场景。使用前应评估权限模型复杂度,避免过度设计导致维护成本上升。

convex-authz 属于开发类 Skill,可作为该场景下的辅助能力补充。

来源仓库内路径:skills/convex-authz

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/dbjpanda/convex-tenants --skill convex-authz;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

convex-authz 怎么安装?

在终端执行 npx skills add https://github.com/dbjpanda/convex-tenants --skill convex-authz。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

convex-authz 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

convex-authz 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

convex-authz 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

convex-authz 用的人多吗?

公开数据是累计安装 1,576 次,近一周安装 67 次,下载 552 次,GitHub 8 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

convex-authz 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理开发类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

开发类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
skill-vetter

技能审查员 · OpenClaw

8,779,018986操作浏览器MIT-0通过
ontology

本体论 · OpenClaw

6,636,960576只读MIT-0通过
entra-app-registration

内部应用程序注册 · Codex、Claude

6,631,872764接触敏感数据未明确通过
entra-app-registration

内部应用程序注册 · Codex、Claude

3,757,680201接触敏感数据未明确通过
nano-banana-pro

纳米香蕉专业版 · OpenClaw

3,526,898364接触敏感数据MIT-0有提醒
convex-authz

凸授权 · 本页

1,5768需要联网未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

@djpanda/convex-authz

A comprehensive, production-ready authorization component for Convex featuring RBAC, ABAC, and ReBAC with O(1) indexed lookups, inspired by Google Zanzibar.

![npm version](https://www.npmjs.com/package/@djpanda/convex-authz) ![License: MIT](https://opensource.org/licenses/MIT) ![Convex Approved](https://www.convex.dev/components/djpanda/convex-tenants)

Features

FeatureDescription
RBACRole-Based Access Control with scoped roles
ABACAttribute-Based Access Control with custom policies
ReBACRelationship-Based Access Control with graph traversal
O(1) LookupsPre-computed permissions for instant checks
Type SafetyFull TypeScript support with type-safe permissions
Audit LoggingTrack all permission changes and checks
Scoped PermissionsResource-level access control
Expiring GrantsTime-limited role assignments and permissions
Convex NativeBuilt specifically for Convex, with real-time updates

Terminology

TermDefinition
RBACRole-Based Access Control - permissions assigned via roles (admin, editor, viewer)
ABACAttribute-Based Access Control - permissions based on user/resource attributes (department=engineering)
ReBACRelationship-Based Access Control - permissions derived from relationships (member of team that owns resource)
ZanzibarGoogle's global authorization system, inspiration for OpenFGA and this component
TupleA relationship triple: (subject, relation, object) e.g., (user:alice, member, team:sales)
ScopeResource-level permission context, e.g., "admin of team:123" vs global "admin"
TraversalFollowing relationship chains to determine inherited access
O(1) LookupConstant-time permission check via pre-computed indexes
Permission OverrideDirect grant/deny that bypasses role-based permissions

Installation

npm install @djpanda/convex-authz

Quick Start

1. Register the Component

// 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

// 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):

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):

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

// 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

// 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:

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

// 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

// Rebuild a single user's effective permissions after upgrading
await authz.recomputeUser(ctx, userId);

Cross-tenant operations

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.
// 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:

// 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:

// 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:

import { AuthzProvider } from "@djpanda/convex-authz/react";
import { api } from "./convex/_generated/api";

<AuthzProvider
  queryRefs={{
    checkPermission: api.app.checkPermission,
    getUserRoles: api.app.getUserRoles,
  }}
  defaultUserId={currentUserId}  // optional; hooks can pass userId in options
>
  <App />
</AuthzProvider>

3. Use hooks and PermissionGate

  • useCanUser(permission, options?) — Returns {allowed, isLoading, error}. Options: {userId?, scope?}. Uses defaultUserId from the provider when userId is omitted.
  • useUserRoles(options?) — Returns {roles, isLoading, error}. Options: {userId?, scope?}.
  • useRequirePermission(permission, options?) — Throws when the user is not allowed (use an error boundary to show a denied state).
  • PermissionGate — Renders children when allowed, fallback when denied, and loadingFallback (optional) while loading.
import {
  useCanUser,
  useUserRoles,
  useRequirePermission,
  PermissionGate,
} from "@djpanda/convex-authz/react";

function DocumentList() {
  const { allowed, isLoading } = useCanUser("documents:read");

  if (isLoading) return <Spinner />;
  if (!allowed) return <p>You cannot view documents.</p>;
  return <div>{/* list */}</div>;
}

function AdminPanel() {
  useRequirePermission("settings:manage"); // throws if denied; wrap in error boundary
  return <div>Admin content</div>;
}

function EditButton({ docId }: { docId: string }) {
  return (
    <PermissionGate
      permission="documents:update"
      scope={{ type: "document", id: docId }}
      fallback={<span>No access</span>}
      loadingFallback={<span>Checking…</span>}
    >
      <button>Edit</button>
    </PermissionGate>
  );
}

Convex’s reactivity ensures that when permissions or roles change on the backend, the hooks and PermissionGate re-run and the UI updates automatically.


Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                           @djpanda/convex-authz                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────────────┐   │
│  │      RBAC        │  │      ABAC        │  │         ReBAC            │   │
│  │  Role-Based      │  │  Attribute-Based │  │  Relationship-Based      │   │
│  │  Access Control  │  │  Access Control  │  │  Access Control          │   │
│  │                  │  │                  │  │                          │   │
│  │  • Roles         │  │  • User attrs    │  │  • Tuples (S, R, O)      │   │
│  │  • Permissions   │  │  • Policies      │  │  • Graph traversal       │   │
│  │  • Scopes        │  │  • Conditions    │  │  • Inheritance           │   │
│  └──────────────────┘  └──────────────────┘  └──────────────────────────┘   │
│           │                    │                         │                  │
│           ▼                    ▼                         ▼                  │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │                    O(1) Indexed Permission Cache                     │   │
│  │                                                                      │   │
│  │   effectivePermissions  │  effectiveRoles  │  effectiveRelationships │   │
│  │   [user, perm, scope]   │  [user, role]    │  [subject, rel, object] │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

RBAC (Role-Based Access Control)

Assigning Roles

// Global role
await authz.assignRole(ctx, userId, "admin");

// Scoped role (e.g., admin of a specific team)
await authz.assignRole(ctx, userId, "admin", {
  type: "team",
  id: "team_123",
});

// With expiration (24 hours)
await authz.assignRole(ctx, userId, "admin", undefined, Date.now() + 86400000);

Revoking Roles

await authz.revokeRole(ctx, userId, "admin");

// Scoped
await authz.revokeRole(ctx, userId, "admin", { type: "team", id: "team_123" });

Checking Permissions

// Boolean check
const canEdit = await authz.can(ctx, userId, "documents:update");

// Throws if denied
await authz.require(ctx, userId, "documents:update");

// With scope
const canEditTeamDocs = await authz.can(ctx, userId, "documents:update", {
  type: "team",
  id: "team_123",
});

Checking Roles

const isAdmin = await authz.hasRole(ctx, userId, "admin");

// Scoped
const isTeamAdmin = await authz.hasRole(ctx, userId, "admin", {
  type: "team",
  id: "team_123",
});

Wildcard and pattern-matching permissions

Permission checks and overrides support wildcard patterns so you can grant or deny whole families of permissions in one go.

Pattern format: resource:action. Either resource or action (or both) may be *:

PatternMeaningExample matches
*All permissionsdocuments:read, settings:manage
documents:*All actions on documentsdocuments:read, documents:update
*:readRead on any resourcedocuments:read, settings:read
*:*All permissions (same as *)any resource:action

Checking: When you call can(ctx, userId, "documents:read") or require(ctx, userId, "documents:read"), the backend treats any stored role or override that *matches* that permission as granting it. So if the user has a role with documents:* or an override *:read, they are allowed for documents:read.

Allocation: You can pass a pattern into grantPermission and denyPermission:

// Grant all document actions
await authz.grantPermission(ctx, userId, "documents:*", undefined, "Full document access");

// Deny read on any resource
await authz.denyPermission(ctx, userId, "*:read", undefined, "Read access revoked");

Role definitions: When the component evaluates permissions, it matches the requested permission against each role’s permission list using the same pattern rules. So if a role’s permissions include "documents:*" (in the flattened role–permission map), then can(ctx, userId, "documents:read") is allowed. With defineRoles you typically list concrete actions per resource (e.g. documents: ["read", "update"]); to use patterns in roles you would supply a role-permission map that includes pattern strings for that role.

Client-side helper: To test whether a pattern matches a permission without calling the backend, use the exported helper:

import { matchesPermissionPattern } from "@djpanda/convex-authz";

matchesPermissionPattern("documents:read", "documents:*"); // true
matchesPermissionPattern("documents:read", "*:read");      // true
matchesPermissionPattern("settings:read", "documents:*"); // false

The same wildcard behavior applies to all permission checks.

Getting User Roles

const roles = await authz.getUserRoles(ctx, userId);
// Returns: [{ role: "admin", scopeKey: "global" }, { role: "editor", scopeKey: "team:123", scope: { type: "team", id: "123" } }]

Bulk operations and offboarding

For large-scale or enterprise workflows, the API supports bulk permission checks (up to 100 permissions) and role updates (up to 20 roles) in a single call.

Bulk permission check (canAny)

Check whether the user has any of the given permissions in one round-trip:

const allowed = await authz.canAny(ctx, userId, [
  "documents:read",
  "documents:update",
  "documents:delete",
], scope);
// true if the user has at least one of these permissions

Bulk role assign and revoke

Assign or revoke multiple roles for one user in a single transaction:

// Assign multiple roles at once (max 20 per call)
const { assigned, assignmentIds } = await authz.assignRoles(ctx, userId, [
  { role: "admin" },
  { role: "editor", scope: { type: "team", id: "team_1" } },
  { role: "viewer", scope: { type: "org", id: "org_1" }, expiresAt: Date.now() + 86400000 },
], actorId);

// Revoke multiple roles at once (max 20 per call)
const { revoked } = await authz.revokeRoles(ctx, userId, [
  { role: "editor", scope: { type: "team", id: "team_1" } },
  { role: "viewer" },
], actorId);

Revoke all roles

Revoke every role for a user (optionally only in a given scope):

const count = await authz.revokeAllRoles(ctx, userId);
const countScoped = await authz.revokeAllRoles(ctx, userId, { type: "team", id: "team_1" }, actorId);

Full user offboarding

Remove all roles, permission overrides, attributes, and optionally ReBAC relationships for a user in one call (optionally scoped). Also clears indexed effectiveRoles, effectivePermissions, and effectiveRelationships when present:

const result = await authz.offboardUser(ctx, userId, {
  scope: { type: "org", id: "org_1" },  // optional: only remove data in this scope
  actorId: "system",
  removeAttributes: true,   // default true
  removeOverrides: true,    // default true
  removeRelationships: true, // default true when no scope (full offboard)
});
// result: { rolesRevoked, overridesRemoved, attributesRemoved, relationshipsRemoved, effectiveRolesRemoved, effectivePermissionsRemoved, effectiveRelationshipsRemoved }

When scope is omitted, the call performs a full deprovision: all roles, overrides, attributes, and all ReBAC relationships where the user is the subject are removed. When scope is provided, only data in that scope is removed and relationships are left unchanged.

User deprovisioning (full wipe)

For security incident response, enterprise offboarding, or single-button deactivation, use deprovisionUser to atomically wipe all roles, attributes, relationships, and permission overrides for a user (no scope, no options):

const result = await authz.deprovisionUser(ctx, userId, {
  actorId: "security-team",
  enableAudit: true,
});
// result: { rolesRevoked, overridesRemoved, attributesRemoved, relationshipsRemoved, effectiveRolesRemoved, effectivePermissionsRemoved, effectiveRelationshipsRemoved }

Bulk arrays are limited per call: permissions in canAny up to 100 items, roles in assignRoles / revokeRoles up to 20 items. The client and component validate and throw a clear error if exceeded.


ABAC (Attribute-Based Access Control)

Setting User Attributes

await authz.setAttribute(ctx, userId, "department", "engineering");
await authz.setAttribute(ctx, userId, "clearanceLevel", 5);
await authz.setAttribute(ctx, userId, "location", { country: "US", state: "CA" });

Getting Attributes

const attributes = await authz.getUserAttributes(ctx, userId);
// Returns: [{ key: "department", value: "engineering" }, { key: "clearanceLevel", value: 5 }]

Defining Policies

The condition function may return either a boolean or a Promise<boolean>, so you can use async logic (e.g. querying the database or calling external APIs).

import { definePolicies, evaluatePolicyCondition } from "@djpanda/convex-authz";

const policies = definePolicies({
  "documents:update": {
    // User can update if they own the document (sync)
    condition: (ctx) => ctx.resource?.ownerId === ctx.subject.userId,
    message: "Only document owners can update",
  },
  "reports:view": {
    // Only engineering department with clearance >= 3 (sync)
    condition: (ctx) =>
      ctx.subject.attributes.department === "engineering" &&
      (ctx.subject.attributes.clearanceLevel as number) >= 3,
    message: "Requires engineering department with clearance level 3+",
  },
  "documents:delete": {
    // Async: e.g. check external service or fetch extra data
    condition: async (ctx) => {
      const doc = await getDocument(ctx.resource?.id);
      return doc != null && doc.ownerId === ctx.subject.userId;
    },
    message: "Only document owners can delete",
  },
});

const authz = new Authz(components.authz, { permissions, roles, policies, tenantId: "my-app" });

When you evaluate a policy (e.g. after RBAC allows), always await the condition so both sync and async policies work. Use evaluatePolicyCondition to normalize to a Promise:

const policy = policies["documents:update"];
if (policy) {
  const allowed = await evaluatePolicyCondition(policy.condition, policyCtx);
  if (!allowed) throw new Error(policy.message ?? "Permission denied");
}

Policy Context

Policies receive a context object with:

interface PolicyContext {
  subject: {
    userId: string;
    roles: string[];
    attributes: Record<string, unknown>;
  };
  resource?: {
    type: string;
    id: string;
    [key: string]: unknown; // Resource data
  };
  action: string; // The permission being checked
  environment?: {
    timestamp: number;
    ip?: string;
  };
  hasRole: (role: string) => boolean;
  hasAttribute: (key: string) => boolean;
  getAttribute: <T = unknown>(key: string, defaultValue?: T) => T | undefined;
}

API note: Existing sync conditions remain valid. There is no breaking change; only the return type is widened to allow Promise<boolean> for async policies.


ReBAC (Relationship-Based Access Control)

ReBAC enables access control based on relationships between entities, perfect for hierarchical systems like CRMs, document sharing, and organizational structures.

Relationship Model

Relationships are stored as tuples: (subject, relation, object)

user:alice  ──member──►  team:sales
team:sales  ──owner──►   account:acme
account:acme ──parent──► deal:big_deal

Adding Relationships

// Use the Authz client — NOT direct component calls
// User is member of team
await authz.addRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });

// Team owns account
await authz.addRelation(ctx, { type: "team", id: "sales" }, "owner", { type: "account", id: "acme" });

Checking Direct Relationships

// Use the Authz client — NOT direct component calls
const isMember = await authz.hasRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });
// Returns: true

// To remove a relationship
await authz.removeRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });

Relationship Traversal (Inherited Access)

The real power of ReBAC is checking access through relationship chains:

// Define how permissions flow through relationships
const traversalRules = {
  // A deal viewer is anyone who can view the parent account
  "deal:viewer": [
    { through: "account", via: "parent", inherit: "viewer" }
  ],
  // An account viewer is any member of the owning team
  "account:viewer": [
    { through: "team", via: "owner", inherit: "member" }
  ],
};

// Graph traversal is available via the component query directly
// Check: Can alice view big_deal?
const result = await ctx.runQuery(components.authz.rebac.checkRelationWithTraversal, {
  subjectType: "user",
  subjectId: "alice",
  relation: "viewer",
  objectType: "deal",
  objectId: "big_deal",
  traversalRules,
  maxDepth: 5,
});

// Returns:
// {
//   allowed: true,
//   path: [
//     "account:acme -[parent]-> deal:big_deal",
//     "team:sales -[owner]-> account:acme",
//     "user:alice -[member]-> team:sales"
//   ],
//   reason: "Access via relationship chain"
// }

Traversal uses a maxDepth limit (default 5) and tracks visited (objectType, objectId, relation) nodes so that circular relationships do not cause infinite loops.

CRM Example

// Setup CRM hierarchy using the Authz client
const setupCRM = async (ctx) => {
  // Sales rep Alice is on sales team
  await authz.addRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });

  // Sales team owns Acme Corp account
  await authz.addRelation(ctx, { type: "team", id: "sales" }, "owner", { type: "account", id: "acme_corp" });

  // Acme Corp has a big deal
  await authz.addRelation(ctx, { type: "account", id: "acme_corp" }, "parent", { type: "deal", id: "big_deal" });

  // Now Alice can access the deal through the relationship chain!
};

O(1) Indexed Lookups

For high-performance production use, the indexed system pre-computes permissions for instant lookups.

Using the Indexed API

import { Authz } from "@djpanda/convex-authz";
import { components } from "./_generated/api";

const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });

// O(1) permission check - single index lookup
const canEdit = await authz.can(ctx, userId, "documents:update");

// O(1) role check
const isAdmin = await authz.hasRole(ctx, userId, "admin");

// O(1) relationship check
const isMember = await authz.hasRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: "sales" });

How It Works

Traditional (O(n)):                    Indexed (O(1)):
┌──────┐                               ┌──────┐
│ User │                               │ User │
└──┬───┘                               └──┬───┘
   │                                      │
   ▼                                      ▼
┌──────────┐                           ┌─────────────────────────────┐
│ Get Roles│ ◄── Query                 │ Index Lookup:               │
└──┬───────┘                           │ effectivePermissions        │
   │                                   │ [userId, permission, scope] │
   ▼                                   └─────────────────────────────┘
┌───────────────┐                                  │
│ Expand Perms  │ ◄── Loop                         ▼
└──┬────────────┘                              true/false
   │
   ▼
┌──────────────┐
│ Check Each   │ ◄── Loop
│ Permission   │
└──┬───────────┘
   │
   ▼
true/false

Trade-offs

OperationTraditionalIndexed
Permission CheckO(roles × perms)O(1)
Role AssignmentO(1)O(permissions)
Permission GrantO(1)O(1)
Memory UsageLowerHigher (denormalized)

Use Indexed for production workloads with many permission checks.


Audit Logging

All authorization changes are logged for compliance and debugging.

Automatic Logging

The following actions are automatically logged:

  • role_assigned - When a role is assigned
  • role_revoked - When a role is revoked
  • permission_granted - When a direct permission is granted
  • permission_denied - When a permission is explicitly denied
  • attribute_set - When a user attribute is set
  • attribute_removed - When a user attribute is removed
  • permission_check - (Optional) When permissions are checked

Querying the Audit Log

Without pagination options, getAuditLog returns a simple array (optional limit, default 100):

// Get all logs for a user
const logs = await authz.getAuditLog(ctx, {
  userId: "user_123",
  limit: 50,
});

// Get logs by action type
const roleChanges = await authz.getAuditLog(ctx, {
  action: "role_assigned",
  limit: 100,
});

For scalable browsing, use cursor-based pagination by passing numItems (and optionally cursor for the next page). The return value is then {page, isDone, continueCursor}:

// First page
const result = await authz.getAuditLog(ctx, { numItems: 50 });
if (!Array.isArray(result)) {
  console.log(result.page);
  if (!result.isDone) {
    // Next page
    const next = await authz.getAuditLog(ctx, {
      numItems: 50,
      cursor: result.continueCursor,
    });
  }
}

Log Entry Structure

{
  _id: "...",
  timestamp: 1704672000000,
  actorId: "admin_user",  // Who made the change
  action: "role_assigned",
  userId: "target_user",  // Who was affected
  details: {
    role: "editor",
    scope: { type: "team", id: "team_123" },
  },
}

Permission Overrides

Grant or deny specific permissions that override role-based assignments.

Granting Permissions

// Grant a permission directly (bypasses role checks)
await authz.grantPermission(ctx, userId, "documents:delete", undefined, "Temporary access for migration");

// With scope
await authz.grantPermission(ctx, userId, "documents:delete", { type: "team", id: "team_123" });

// With expiration
await authz.grantPermission(ctx, userId, "documents:delete", undefined, "Temporary", Date.now() + 3600000);

Denying Permissions

// Deny a permission (even if user has it via role)
await authz.denyPermission(ctx, userId, "documents:delete", undefined, "Access restricted");

Schema Reference

Tables

TablePurpose
roleAssignmentsUser role assignments
userAttributesUser attributes for ABAC
permissionOverridesDirect permission grants/denials
relationshipsReBAC relationship tuples
effectivePermissionsPre-computed permissions (O(1))
effectiveRolesPre-computed roles (O(1))
effectiveRelationshipsPre-computed relationships (O(1))
auditLogAuthorization audit trail

Indexes

All tables have optimized indexes for common query patterns:

// roleAssignments
.index("by_user", ["userId"])
.index("by_role", ["role"])
.index("by_user_and_role", ["userId", "role"])

// effectivePermissions (O(1) lookup)
.index("by_user_permission_scope", ["userId", "permission", "scopeKey"])

// relationships
.index("by_subject_relation_object", ["subjectType", "subjectId", "relation", "objectType", "objectId"])

API Reference

Authz Client

class Authz<P, R, Policy> {
  // Permission checks
  can(ctx, userId, permission, scope?): Promise<boolean>
  canAny(ctx, userId, permissions: string[], scope?): Promise<boolean>   // bulk: any of N permissions (max 100)
  require(ctx, userId, permission, scope?): Promise<void>

  // Role management
  hasRole(ctx, userId, role, scope?): Promise<boolean>
  assignRole(ctx, userId, role, scope?, expiresAt?, actorId?): Promise<string>
  assignRoles(ctx, userId, roles: RoleAssignItem[], actorId?): Promise<{ assigned: number; assignmentIds: string[] }>  // bulk, max 20
  revokeRole(ctx, userId, role, scope?, actorId?): Promise<boolean>
  revokeRoles(ctx, userId, roles: RoleScopeItem[], actorId?): Promise<{ revoked: number }>  // bulk, max 20
  revokeAllRoles(ctx, userId, scope?, actorId?): Promise<number>
  getUserRoles(ctx, userId, scope?): Promise<Role[]>
  getUserPermissions(ctx, userId, scope?): Promise<PermissionResult>

  // Offboarding
  offboardUser(ctx, userId, options?: { scope?, actorId?, removeAttributes?, removeOverrides?, removeRelationships? }): Promise<OffboardResult>
  deprovisionUser(ctx, userId, options?: { actorId?, enableAudit? }): Promise<OffboardResult>  // full wipe: roles, overrides, attributes, relationships

  // Attribute management
  setAttribute(ctx, userId, key, value, actorId?): Promise<string>
  removeAttribute(ctx, userId, key, actorId?): Promise<boolean>
  getUserAttributes(ctx, userId): Promise<Attribute[]>

  // Permission overrides
  grantPermission(ctx, userId, permission, scope?, reason?, expiresAt?, actorId?): Promise<string>
  denyPermission(ctx, userId, permission, scope?, reason?, expiresAt?, actorId?): Promise<string>

  // Audit
  getAuditLog(ctx, options?): Promise<AuditEntry[] | { page: AuditEntry[]; isDone: boolean; continueCursor: string }>
}

Argument validation

All public methods on Authz validate their arguments before calling the component. Invalid inputs throw an Error with a clear message so you can fail fast and fix call sites.

ArgumentRuleExample error
userIdNon-empty string, max 512 characters"userId must be a non-empty string"
permissionMust be resource:action (e.g. documents:read)"Invalid permission format: \"read\". Expected \"resource:action\""
scopeWhen provided, type and id must be non-empty strings"scope must have non-empty type when provided"
roleNon-empty string; must be one of the roles passed at construction"Unknown role: \"superadmin\""
expiresAtWhen provided, must be a finite number (timestamp)"expiresAt must be a finite number"
Attribute keyNon-empty string"Attribute key must be a non-empty string"
getAuditLog limitWhen provided, positive integer 1–1000"limit must be a positive integer when provided"
getAuditLog numItemsWhen provided (pagination), positive integer 1–1000same as limit
Relation argssubjectType, subjectId, relation, objectType, objectId must be non-empty strings"subjectType must be a non-empty string"
canAny permissionsNon-empty array, each element valid resource:action, length ≤ 100"permissions must not exceed 100 items"
assignRoles / revokeRoles rolesNon-empty array, each role valid, length ≤ 20"roles must not exceed 20 items"

Optional parameters are only validated when present (e.g. omitting scope is valid; passing scope: {type: "", id: "x"} throws).


Inspired by Google Zanzibar

This component implements concepts from Google Zanzibar, Google's global authorization system that powers Google Drive, YouTube, Cloud, and more.

Zanzibar Concepts Implemented

Zanzibar ConceptOur ImplementationDescription
Relation Tuplesrelationships table(user:alice, member, team:sales)
UsersetsTraversal rulesGroups defined by relationships
Check APIcheckPermissionFastO(1) "can user X do Y on Z?"
Expand APIcheckRelationWithTraversalFind all paths granting access
Read APIgetSubjectRelationsList all relationships
Watch APIConvex reactivityReal-time permission updates
Computed RelationseffectivePermissionsPre-computed for O(1) lookup

How Zanzibar Works

┌─────────────────────────────────────────────────────────────────────────────┐
│                        Google Zanzibar Model                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Relation Tuples (stored):                                                  │
│  ┌────────────────────────────────────────────────────────────────────┐     │
│  │  (user:alice, member, team:sales)                                  │     │
│  │  (team:sales, owner, account:acme)                                 │     │
│  │  (account:acme, parent, deal:big_deal)                             │     │
│  └────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│  Authorization Model (defines inheritance):                                  │
│  ┌────────────────────────────────────────────────────────────────────┐     │
│  │  type deal                                                         │     │
│  │    relations                                                       │     │
│  │      define parent: [account]                                      │     │
│  │      define viewer: viewer from parent  ← Computed relation        │     │
│  └────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│  Check: "Can alice view deal:big_deal?"                                     │
│  ┌────────────────────────────────────────────────────────────────────┐     │
│  │  1. deal:big_deal.viewer = viewer from parent                      │     │
│  │  2. parent = account:acme                                          │     │
│  │  3. account:acme.viewer = member from owner                        │     │
│  │  4. owner = team:sales                                             │     │
│  │  5. team:sales.member includes user:alice ✓                        │     │
│  │  → ALLOWED                                                         │     │
│  └────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Key Zanzibar Benefits We Provide

  1. Consistency at Scale
  • Pre-computed permissions ensure fast, consistent checks
  • No permission drift between reads
  1. Flexible Permission Model
  • Combine RBAC, ABAC, and ReBAC as needed
  • Support complex hierarchies (org → team → project → resource)
  1. Auditability
  • Full audit log of all permission changes
  • Path tracing shows WHY access was granted
  1. Real-time Updates
  • Convex reactivity means UI updates instantly when permissions change
  • No polling required (better than Zanzibar!)

Comparison with Other Solutions

Feature@djpanda/convex-authzOpenFGAOsoCerbos
RBAC✅✅✅✅
ABAC✅⚠️ Limited✅✅
ReBAC✅✅ Native✅⚠️
O(1) Lookups✅✅✅✅
Convex Native✅❌❌❌
Type Safety✅ TypeScriptDSLPolarYAML
Real-time✅ Convex queriesPollingPollingPolling
Self-hosted✅✅✅✅

Testing

Running Package Tests

cd packages/authz
npm test

Using with convex-test

import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "./component/schema.js";
import { api } from "./component/_generated/api.js";

describe("authorization", () => {
  it("should assign and check roles", async () => {
    const t = convexTest(schema, modules);

    await t.mutation(api.mutations.assignRole, {
      userId: "user_123",
      role: "admin",
    });

    const hasRole = await t.query(api.queries.hasRole, {
      userId: "user_123",
      role: "admin",
    });

    expect(hasRole).toBe(true);
  });
});

Multi-Tenant Data Isolation

Every table in the authz component includes a required tenantId field as the leading column in every database index. This provides database-level data isolation between tenants — queries for one tenant can never return data from another.

tenantId vs scope

tenantIdscope
PurposeData isolation boundaryResource-level grouping
EnforcementDatabase-level (index prefix)Application-level (query filter)
RequiredAlwaysOptional
Example"acme-corp"{type: "project", id: "proj-123"}
  • tenantId answers: "whose data is this?" — the organization/customer boundary
  • scope answers: "within this tenant, what resource does this apply to?" — e.g. admin of a specific project

Configuration

// Single-tenant apps — pass any constant string
const authz = new Authz(components.authz, {
  permissions, roles,
  tenantId: "my-app",
});

// Multi-tenant apps — pass the current organization/tenant ID
const authz = new Authz(components.authz, {
  permissions, roles,
  tenantId: currentOrgId,
});

Cross-tenant operations

For rare admin operations that need to access a different tenant's data, use the withTenant() method:

// Returns a new Authz instance scoped to a different tenant
const otherTenant = authz.withTenant("other-org-id");
await otherTenant.getUserRoles(ctx, userId);

Compliance

The tenantId-first index design satisfies SOC2 and HIPAA data isolation requirements:

  • All queries are partitioned by tenant at the database index level
  • Cross-tenant data access is structurally impossible through the standard API
  • tenantId is required in the constructor — it cannot be accidentally omitted

Best Practices

1. Use Scoped Roles for Multi-tenancy

// Don't: Global admin
await authz.assignRole(ctx, userId, "admin");

// Do: Scoped admin
await authz.assignRole(ctx, userId, "admin", { type: "org", id: orgId });

2. Use the Authz Client for Production

// Authz uses O(1) indexed lookups by default — no separate class needed
const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });

3. Use ReBAC for Complex Hierarchies

// CRM, document sharing, org charts → ReBAC
// Simple role assignments → RBAC

4. Set Expiration for Temporary Access

await authz.assignRole(ctx, userId, "contractor", undefined,
  Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days
);

5. Always Use Audit Logging

The audit log is invaluable for:

  • Compliance (SOC2, GDPR)
  • Debugging access issues
  • Security incident investigation

6. Cleanup of Expired Data (Scheduled via Component)

Expired role assignments and permission overrides (and their indexed rows) are purged by a scheduled cleanup job that you enable once—no need to add convex/crons.ts yourself. The component embeds @convex-dev/crons; run this once after installing the component to register the daily job:

npx convex run authz/cronSetup:ensureCleanupCronRegistered

Or from an init script that runs on deploy (e.g. convex/init.ts invoked via convex dev --run init):

await ctx.runMutation(components.authz.cronSetup.ensureCleanupCronRegistered, {});

The job runs every 24 hours and cleans roleAssignments, permissionOverrides, effectiveRoles, and effectivePermissions. Optional: you can instead define the cleanup in your app's convex/crons.ts or run components.authz.mutations.runScheduledCleanup manually.

6.1. Audit log retention

To avoid unbounded growth of the audit log (compliance and cost), the same cron registration also schedules a daily audit retention job. Configure it with Convex environment variables (Dashboard or CLI):

VariableDescription
AUDIT_RETENTION_DAYSDelete entries older than this many days (e.g. 90). Omit or 0 = do not prune by age.
AUDIT_RETENTION_MAX_ENTRIESCap total entries by deleting oldest until count ≤ this value (e.g. 100000). Omit or 0 = do not prune by count.

Set at least one to enable retention. The job runs every 24 hours (same ensureCleanupCronRegistered flow). You can also run components.authz.mutations.runAuditRetentionCleanup manually with optional args {maxAgeDays?, maxEntries?} to override env for that run.

7. Use Authz as a Global Singleton

Authz is a global component — install it once and share a single client instance across your entire app. Do not create multiple Authz instances per app.

convex/
  convex.config.ts   ← app.use(authz) — registered once
  authz.ts           ← definePermissions, defineRoles, export authz client
  documents.ts       ← import { authz } from "./authz"
  billing.ts         ← import { authz } from "./authz"
  settings.ts        ← import { authz } from "./authz"
// convex/authz.ts — single source of truth
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { components } from "./_generated/api";

const permissions = definePermissions({
  documents: { create: true, read: true, update: true, delete: true },
  billing: { view: true, manage: true },
  settings: { view: true, manage: true },
});

const roles = defineRoles(permissions, {
  admin: {
    documents: ["create", "read", "update", "delete"],
    billing: ["view", "manage"],
    settings: ["view", "manage"],
  },
  viewer: {
    documents: ["read"],
    settings: ["view"],
  },
});

// Export the single authz client — import this everywhere
export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
// convex/documents.ts — uses the shared client
import { mutation } from "./_generated/server";
import { authz } from "./authz";

export const deleteDocument = mutation({
  args: { docId: v.id("documents") },
  handler: async (ctx, args) => {
    await authz.require(ctx, userId, "documents:delete");
    // ...
  },
});

7. Organize Permissions by Domain

In larger apps, split permission and role definitions by domain and merge them into a single authz client using definePermissions and defineRoles:

// convex/permissions/documents.ts
export const documentPermissions = {
  documents: { create: true, read: true, update: true, delete: true },
};
export const documentRoles = {
  editor: { documents: ["create", "read", "update"] as const },
  viewer: { documents: ["read"] as const },
};

// convex/permissions/billing.ts
export const billingPermissions = {
  billing: { view: true, manage: true },
};
export const billingRoles = {
  billing_admin: { billing: ["view", "manage"] as const },
};
// convex/authz.ts — merge all domains
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { components } from "./_generated/api";
import { documentPermissions, documentRoles } from "./permissions/documents";
import { billingPermissions, billingRoles } from "./permissions/billing";

const permissions = definePermissions(documentPermissions, billingPermissions);
const roles = defineRoles(permissions, documentRoles, billingRoles);

export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });

This keeps each domain self-contained while producing a single, type-safe authz client.

8. Integrating with Other Convex Components

When other Convex components (e.g., @djpanda/convex-tenants) need authorization, they share the same global authz instance. The pattern:

  1. Register both components independently in convex.config.ts
  2. The other component exports its required permissions and roles
  3. Merge them with your app's own definitions
  4. Pass the authz client to the other component's API factory
graph LR
  subgraph app ["Your App (convex.config.ts)"]
    AuthzComp["authz component"]
    TenantsComp["tenants component"]
  end

  subgraph authzSetup ["convex/authz.ts"]
    AppPerms["App permissions"]
    TenantPerms["Tenant permissions"]
    Merge["definePermissions + defineRoles"]
    Client["authz client (singleton)"]
    AppPerms --> Merge
    TenantPerms --> Merge
    Merge --> Client
  end

  Client -->|"import { authz }"| Docs["convex/documents.ts"]
  Client -->|"import { authz }"| Billing["convex/billing.ts"]
  Client -->|"passed to makeTenantsAPI"| TenantsAPI["convex/tenants.ts"]
// convex/convex.config.ts — register both components
import { defineApp } from "convex/server";
import authz from "@djpanda/convex-authz/convex.config";
import tenants from "@djpanda/convex-tenants/convex.config";

const app = defineApp();
app.use(authz);
app.use(tenants);
export default app;
// convex/authz.ts — merge app + component permissions
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { TENANTS_PERMISSIONS, TENANTS_ROLES } from "@djpanda/convex-tenants";
import { components } from "./_generated/api";

// Your app's own permissions
const appPermissions = {
  documents: { create: true, read: true, update: true, delete: true },
};
const appRoles = {
  editor: { documents: ["create", "read", "update"] as const },
};

// Merge with tenant component's permissions
const permissions = definePermissions(appPermissions, TENANTS_PERMISSIONS);
const roles = defineRoles(permissions, appRoles, TENANTS_ROLES);

export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
// convex/tenants.ts — pass the shared authz client
import { makeTenantsAPI } from "@djpanda/convex-tenants";
import { components } from "./_generated/api";
import { authz } from "./authz";

export const {
  createOrg,
  inviteMember,
  removeMember,
  // ...
} = makeTenantsAPI(components.tenants, {
  authz,
  creatorRole: "owner",
  auth: async (ctx) => {
    // return the current user ID
  },
});

This way every part of your app — your own functions and third-party components — shares a single, consistent authorization layer.



Development

# Install dependencies
npm install

# Run development mode
npm run dev

# Run tests
npm test

# Build for production
npm run build

# Type check
npm run typecheck

File Structure

packages/authz/
├── package.json          # Package configuration
├── README.md             # This documentation
├── src/
│   ├── client/
│   │   ├── index.ts      # Main exports (Authz, helpers)
│   │   └── index.test.ts # Client tests
│   ├── component/
│   │   ├── convex.config.ts  # Component registration
│   │   ├── schema.ts         # Database tables and indexes
│   │   ├── helpers.ts        # Shared utilities
│   │   ├── queries.ts        # Query functions
│   │   ├── mutations.ts      # Mutation functions
│   │   ├── rebac.ts          # ReBAC relationship functions
│   │   ├── indexed.ts        # O(1) indexed functions
│   │   ├── authz.test.ts     # RBAC/ABAC tests
│   │   ├── rebac.test.ts     # ReBAC tests
│   │   ├── indexed.test.ts   # O(1) indexed tests
│   │   └── _generated/       # Auto-generated types
│   └── test.ts           # Test helpers
└── example/              # Example app

License

MIT


Contributing

Contributions are welcome! Please read our CONTRIBUTING.md before submitting a PR.

平台分布

Codex

36.16%
按下载量换算200

Claude

29.74%
按下载量换算164

Cursor

18.98%
按下载量换算105

Gemini CLI

10.15%
按下载量换算56

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills