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

convex-roles

v0.1.0

Published

Convex Component for role-based access control, permissions, and authorization

Downloads

84

Readme

convex-roles

A Convex Component for role-based access control (RBAC), permissions, and authorization with full type safety.

Features

  • Role hierarchy with inheritance
  • Permission checking with wildcard support and autocomplete
  • Ownership validation for resources
  • Temporal permissions (trials, invitations)
  • Audit logging
  • React hooks for frontend authorization
  • Test utilities for unit testing
  • Full type safety with autocomplete for ctx.db, permissions, and table names

Installation

npm install convex-roles

Quick Start

1. Add the component to your Convex app

// convex/convex.config.ts
import { defineApp } from "convex/server";
import roles from "convex-roles/convex.config";

const app = defineApp();
app.use(roles);

export default app;

2. Configure roles with full type safety

// convex/auth.ts
import { Roles } from "convex-roles";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";

// Define permissions with `as const` for type-safe autocomplete
const permissionsConfig = {
  admin: ["*"],                                    // Wildcard: all permissions
  moderator: ["users:ban", "content:moderate"],
  user: ["content:create", "content:read"],
} as const;

// Create roles with DataModel and Permissions generics
export const roles = new Roles<DataModel, typeof permissionsConfig>(
  components.roles,
  {
    // Define roles with inheritance
    roles: {
      admin: { inherits: ["moderator"] },
      moderator: { inherits: ["user"] },
      user: {},
    },

    // Assign permissions to roles
    permissions: permissionsConfig,

    // Resolve user from your database
    resolver: {
      getUser: async (ctx, identity) => {
        const user = await ctx.db
          .query("users")
          .withIndex("by_auth", (q) => q.eq("authId", identity.subject))
          .first();
        return user ? { id: user._id, roles: user.roles ?? [] } : null;
      },
    },

    // Optional features
    features: {
      auditLog: true,
    },
  }
);

export type Permissions = typeof permissionsConfig;

3. Use in your functions

// convex/posts.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { roles } from "./auth";

// Require authentication
export const listMine = query({
  args: {},
  handler: roles.withAuth(async (ctx) => {
    return ctx.db
      .query("posts")
      .withIndex("by_user", (q) => q.eq("userId", ctx.user.id))
      .collect();
  }),
});

// Require a specific role
export const moderate = mutation({
  args: { postId: v.id("posts") },
  handler: roles.withRole("moderator", async (ctx, { postId }) => {
    return ctx.db.patch(postId, { moderated: true });
  }),
});

// Require a specific permission (with autocomplete!)
export const create = mutation({
  args: { title: v.string() },
  handler: roles.withPermission("content:create", async (ctx, { title }) => {
    return ctx.db.insert("posts", { title, userId: ctx.user.id });
  }),
});

// Require ownership OR permission
export const update = mutation({
  args: { id: v.id("posts"), title: v.string() },
  handler: roles.withOwnershipOrPermission({
    table: "posts",
    ownerField: "userId",
    permission: "content:moderate",
  }, async (ctx, { id, title }) => {
    return ctx.db.patch(id, { title });
  }),
});

API Reference

Wrapper Methods

| Method | Description | |--------|-------------| | withAuth(handler) | Requires authentication | | withRole(role, handler) | Requires a specific role (with inheritance) | | withPermission(permission, handler) | Requires a specific permission | | withOwnership(config, handler) | Requires ownership of a resource | | withOwnershipOrPermission(config, handler) | Requires ownership OR a permission |

Compose API

For complex authorization logic:

import type { MutationCtx } from "./_generated/server";
import type { Id } from "./_generated/dataModel";

export const deletePost = mutation({
  args: { id: v.id("posts") },
  handler: roles
    .compose<MutationCtx, { id: Id<"posts"> }>()
    .withAuth()
    .anyOf([
      roles.isRole("admin"),
      roles.isOwner("posts", "userId"),
    ])
    .handle(async (ctx, { id }) => {
      return ctx.db.delete(id);
    }),
});

Guard Helpers

| Helper | Description | |--------|-------------| | isRole(role) | Guard that checks for a role | | requiresPermission(permission) | Guard that checks for a permission | | isOwner(table, field) | Guard that checks resource ownership |

Manual Checks

// Get authenticated user
const result = await roles.getUser(ctx);
if (result) {
  const { user, identity } = result;
}

// Check permission manually
const canEdit = await roles.hasPermission(ctx, user, "content:edit");

// Check role manually
const isAdmin = roles.hasRole(user, "admin");

// Log to audit
await roles.audit(ctx, {
  action: "custom_action",
  userId: user.id,
  result: canEdit ? "allowed" : "denied",
});

Temporal Permissions

Grant time-limited permissions for trials, invitations, etc:

// Grant a 7-day trial
await roles.grantTemporalPermission(ctx, {
  userId: "user123",
  permission: "premium:access",
  validUntil: Date.now() + 7 * 24 * 60 * 60 * 1000,
  reason: "Free trial",
});

// Revoke temporal permission
await roles.revokeTemporalPermission(ctx, {
  userId: "user123",
  permission: "premium:access",
});

// Get active temporal permissions
const perms = await roles.getTemporalPermissions(ctx, "user123");

React Hooks

Shared Configuration (Recommended)

To avoid duplicating permissions between backend and frontend, create a shared configuration file:

your-project/
├── shared/
│   └── permissions.ts    # Shared configuration
├── convex/
│   └── auth.ts           # Backend - imports from shared/
└── src/
    └── App.tsx           # Frontend - imports from shared/
// shared/permissions.ts
export const permissionsConfig = {
  admin: ["*"],
  moderator: ["users:ban", "content:moderate"],
  user: ["content:create", "content:read"],
} as const;

export const rolesConfig = {
  admin: { inherits: ["moderator"] },
  moderator: { inherits: ["user"] },
  user: {},
} as const;

export type Permissions = typeof permissionsConfig;
// convex/auth.ts
import { Roles } from "convex-roles";
import { permissionsConfig, rolesConfig } from "../shared/permissions";

export const roles = new Roles<DataModel, typeof permissionsConfig>(
  components.roles,
  {
    roles: rolesConfig,
    permissions: permissionsConfig,
    // ... resolver config
  }
);
// src/lib/roles.ts - Create pre-configured hooks once
import { createRolesConfig } from "convex-roles/react";
import { api } from "../convex/_generated/api";
import { permissionsConfig, rolesConfig } from "../../shared/permissions";

export const {
  useRoles,
  usePermissions,
  RequireRole,
  RequirePermission,
} = createRolesConfig({
  getUserQuery: api.users.getCurrent,
  roles: rolesConfig,
  permissions: permissionsConfig,
});
// src/App.tsx - Use without passing options everywhere
import { useRoles, RequirePermission } from "./lib/roles";

function MyComponent() {
  // No options needed - they're pre-configured!
  const { user, hasRole, hasPermission, isLoading } = useRoles();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {hasRole("admin") && <AdminPanel />}
      {hasPermission("content:create") && <CreateButton />}

      {/* Components also work without options */}
      <RequirePermission permission="content:moderate">
        <ModeratorPanel />
      </RequirePermission>
    </div>
  );
}

If you prefer not to use the factory pattern, you can pass options to each hook/component:

import { useRoles, RequirePermission } from "convex-roles/react";

const rolesOptions = {
  getUserQuery: api.users.getCurrent,
  roles: rolesConfig,
  permissions: permissionsConfig,
};

function MyComponent() {
  const { hasPermission } = useRoles(rolesOptions);

  return (
    <RequirePermission permission="content:create" options={rolesOptions}>
      <CreateButton />
    </RequirePermission>
  );
}

Note: The shared configuration is safe to expose to the frontend. It only defines the structure of your authorization system (which roles/permissions exist), not the actual user data. The real security checks happen on the backend.

Test Utilities

The package includes test utilities for unit testing:

import {
  createMockUser,
  createMockTemporalPermission,
  createMockAuditEntry,
  createMockRolesConfig,
  hasRoleWithInheritance,
  hasPermissionHelper,
} from "convex-roles/test";

// Create mock data
const user = createMockUser({ roles: ["admin"] });
const config = createMockRolesConfig({
  roles: { admin: { inherits: ["user"] }, user: {} },
  permissions: { admin: ["*"], user: ["read"] },
});

// Test role inheritance
hasRoleWithInheritance(user, "admin", config.roles);  // true
hasRoleWithInheritance(user, "user", config.roles);   // true (inherited)

// Test permission checking
hasPermissionHelper(user, "write", config);  // true (wildcard)
hasPermissionHelper(user, "read", config);   // true (inherited)

// Create mock temporal permission
const perm = createMockTemporalPermission({
  userId: user.id,
  permission: "premium:access",
  validUntil: Date.now() + 7 * 24 * 60 * 60 * 1000,
});

// Create mock audit entry
const entry = createMockAuditEntry({
  action: "permission_check",
  userId: user.id,
  result: "allowed",
});

Permission Patterns

Wildcards

const permissions = {
  admin: ["*"],              // All permissions
  moderator: ["content:*"],  // All content permissions
} as const;

Resource Actions

const permissions = {
  user: [
    "posts:create",
    "posts:read",
    "posts:update:own",
    "comments:create",
  ],
} as const;

Role Inheritance

Roles can inherit from other roles:

roles: {
  admin: { inherits: ["moderator"] },      // admin has moderator + user permissions
  moderator: { inherits: ["user"] },        // moderator has user permissions
  user: {},                                  // base role
}

Configuration

Full Options

new Roles<DataModel, typeof permissions>(component, {
  roles: {
    roleName: {
      inherits?: string[];      // Roles to inherit from
      description?: string;     // Optional description
    }
  },

  permissions: {
    roleName: string[];         // List of permissions (use `as const`)
  },

  resolver: {
    getUser: (ctx, identity) => Promise<{ id: string; roles: string[] } | null>;
  },

  features?: {
    auditLog?: boolean;         // Enable audit logging (default: false)
  },

  errors?: {
    unauthorized?: (reason?: string) => Error;  // Custom auth error
    forbidden?: (reason?: string) => Error;     // Custom forbidden error
    notFound?: (resource?: string) => Error;    // Custom not found error
  },
});

Custom Errors

You can customize the errors thrown by authorization checks:

export const roles = new Roles<DataModel, typeof permissionsConfig>(
  components.roles,
  {
    // ... other config
    errors: {
      unauthorized: (reason) => new ConvexError({
        code: "UNAUTHORIZED",
        message: reason ?? "Authentication required",
      }),
      forbidden: (reason) => new ConvexError({
        code: "FORBIDDEN",
        message: reason ?? "Access denied",
      }),
      notFound: (resource) => new ConvexError({
        code: "NOT_FOUND",
        message: `${resource ?? "Resource"} not found`,
      }),
    },
  }
);

Component Schema

The component uses two tables:

  • temporalPermissions: Time-limited permissions
  • auditLog: Permission check history

Package Exports

| Export | Description | |--------|-------------| | convex-roles | Main client API (Roles class) | | convex-roles/react | React hooks and components | | convex-roles/convex.config | Component configuration | | convex-roles/test | Test utilities |

License

MIT