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

@amadeni/convex-lib

v0.1.8

Published

Convex auth primitives and type validation toolbox

Readme

@amadeni/convex-lib

Typed auth, admin, capability, and authorization primitives for Convex apps.

The library is designed for real Convex projects using:

  • generated convex/_generated/* builders and context types
  • convex-helpers
  • Convex Auth
  • strict TypeScript
  • capability-based access control
  • action/query/mutation wrappers

Install

pnpm add @amadeni/convex-lib convex convex-helpers zod

Recommended Setup

For most apps, keep one central convex/lib.ts setup file and export all wrappers from a single composer:

import {
  createActionResolvers,
  createCapabilityChecker,
  createConvexLib,
  createError,
  createPermissionCheckerFromCapabilities,
  typedRef,
} from '@amadeni/convex-lib';
import { action, mutation, query } from './_generated/server';

// Keep generated function refs outside `convex/` to avoid API cycles.
import {
  getCapabilityOverrideRef,
  getPermissionEntryRef,
  getUserBySubjectRef,
} from '../lib/convex-refs';

const capabilityRegistry = {
  'posts.manage': {
    label: 'Manage posts',
    category: 'content',
    defaultRoles: ['admin', 'editor'] as const,
    grants: [{ resource: 'posts', actions: ['read', 'update'] as const }],
  },
  'posts.delete': {
    label: 'Delete posts',
    category: 'content',
    defaultRoles: ['admin'] as const,
    grants: {
      posts: {
        delete: true as const,
      },
    },
  },
};

const resolveUser = async ctx => {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw createError.unauthenticated();
  }

  const user = await ctx.db
    .query('users')
    .withIndex('by_subject', q => q.eq('subject', identity.subject))
    .unique();

  if (!user) {
    throw createError.notFound('users', identity.subject);
  }

  return user;
};

const capabilityChecker = createCapabilityChecker({
  registry: capabilityRegistry,
  getOverride: async (ctx, key) => {
    return await ctx.db
      .query('capabilityOverrides')
      .withIndex('by_key', q => q.eq('key', key))
      .first();
  },
});

const permissionChecker = createPermissionCheckerFromCapabilities({
  registry: capabilityRegistry,
  getOverride: async (ctx, key) => {
    return await ctx.db
      .query('capabilityOverrides')
      .withIndex('by_key', q => q.eq('key', key))
      .first();
  },
  getDocument: async (ctx, _table, id) => await ctx.db.get(id),
  defaultAllow: false,
});

const actionRuntime = createActionResolvers({
  registry: capabilityRegistry,
  resolveUser: async ctx => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw createError.unauthenticated();
    }

    const user = await ctx.runQuery(typedRef(getUserBySubjectRef), {
      subject: identity.subject,
    });
    if (!user) {
      throw createError.notFound('users', identity.subject);
    }

    return user;
  },
  getCapabilityOverrideRef,
  getCapabilityOverrideArgs: (_ctx, key) => ({ key }),
  getPermissionRef: getPermissionEntryRef,
  getPermissionArgs: (_ctx, role, resource) => ({ role, resource }),
});

export const {
  authQuery,
  authMutation,
  authAction,
  adminQuery,
  adminMutation,
  adminAction,
  capabilityQuery,
  capabilityMutation,
  capabilityAction,
  authorizedQuery,
  authorizedMutation,
  authorizedAction,
} = createConvexLib({
  query,
  mutation,
  action,
  isAdmin: user => user.role === 'admin',
  runtime: {
    query: {
      resolveUser,
      capabilityChecker,
      permissionChecker,
    },
    mutation: {
      resolveUser,
      capabilityChecker,
      permissionChecker,
    },
    action: actionRuntime,
  },
});

This keeps the app-specific Convex types from _generated/server intact, so wrapped handlers still behave like your real QueryCtx, MutationCtx, and ActionCtx. Direct destructuring from createConvexLib(...) is supported in strict TypeScript projects, so you should not need an intermediate typed constant just to export the primitives.

Composer

createConvexLib(...) is the recommended top-level API for apps that want one central setup and one flat export surface.

It returns:

  • authQuery, authMutation, authAction
  • adminQuery, adminMutation, adminAction
  • capabilityQuery, capabilityMutation, capabilityAction
  • authorizedQuery, authorizedMutation, authorizedAction

If you prefer lower-level composition, createPrimitives(...) and createAuthorized(...) are still exported separately.

Runtime Config

The preferred config shape is runtime-aware:

runtime: {
  query: { resolveUser, capabilityChecker, permissionChecker },
  mutation: { resolveUser, capabilityChecker, permissionChecker },
  action: { resolveUser, capabilityChecker, permissionChecker },
}

This is clearer than parallel flat options and maps directly to how Convex runtimes differ in practice.

Flat legacy options like resolveUserAction, capabilityCheckerAction, and permissionCheckerAction are still supported as compatibility aliases.

Action Bridging

Actions often cannot use ctx.db directly the same way as queries and mutations. Use createActionResolvers(...) to build a runtime.action entry from runQuery(...) refs:

const actionRuntime = createActionResolvers({
  registry: capabilityRegistry,
  resolveUser: async ctx => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw createError.unauthenticated();
    }

    return await ctx.runQuery(typedRef(getUserBySubjectRef), {
      subject: identity.subject,
    });
  },
  getCapabilityOverrideRef,
  getCapabilityOverrideArgs: (_ctx, key) => ({ key }),
  getPermissionRef,
  getPermissionArgs: (_ctx, role, resource) => ({ role, resource }),
});

It returns:

  • resolveUser
  • capabilityChecker when registry and getCapabilityOverrideRef are provided
  • permissionChecker when getPermissionRef is provided

Use direct resolveUser when your action user lookup depends on async auth state. Use getUserRef plus getUserArgs when your app already has a reusable internal query for that lookup. Both paths can be passed directly to runtime.action.

Capabilities To CRUD

If your app derives CRUD authorization from capability grants plus capability overrides, createPermissionCheckerFromCapabilities(...) is the preferred path. Use createPermissionChecker(...) only when your app already stores explicit CRUD permission entries and does not derive them from capabilities.

Add grants to capability definitions:

const capabilityRegistry = {
  'posts.manage': {
    label: 'Manage posts',
    category: 'content',
    defaultRoles: ['editor'] as const,
    grants: {
      posts: {
        read: true as const,
        update: true as const,
      },
    },
  },
};

Array-style grants are supported too:

const capabilityRegistry = {
  'posts.manage': {
    label: 'Manage posts',
    category: 'content',
    defaultRoles: ['editor'] as const,
    grants: [{ resource: 'posts', actions: ['read', 'update'] as const }],
  },
};

Then create the permission checker:

const permissionChecker = createPermissionCheckerFromCapabilities({
  registry: capabilityRegistry,
  getOverride: async (ctx, key) => {
    return await ctx.db
      .query('capabilityOverrides')
      .withIndex('by_key', q => q.eq('key', key))
      .first();
  },
  getDocument: async (ctx, _table, id) => await ctx.db.get(id),
  defaultAllow: false,
});

This removes the common local adapter that translates capability grants into CRUD permissions.

Generated API Cycle Guidance

Avoid importing convex/_generated/api inside convex/lib.ts when that file is itself part of your Convex API surface. That can create circular type dependencies during code generation.

Recommended pattern:

  1. Keep generated builders like query, mutation, and action inside convex/lib.ts.
  2. Keep function refs used by action bridges in a file outside convex/, for example src/lib/convex-refs.ts.
  3. Wrap exported refs with typedRef(...) in that external file when you want an explicit type barrier without writing verbose FunctionReference<...> annotations.
  4. Import those refs into convex/lib.ts.

Example:

import { typedRef } from '@amadeni/convex-lib';
import { internal } from '../convex/_generated/api';

export const getUserBySubjectRef = typedRef(
  internal.users.internal.getBySubject,
);
export const getCapabilityOverrideRef = typedRef(
  internal.capabilities.internal.getOverride,
);

That keeps the setup typed without feeding convex/_generated/api back into the same module graph being generated.

Handler Context

Wrapped handlers preserve the app’s real Convex context and add auth fields on top:

  • ctx.user
  • ctx.userId
  • ctx.role

The original Convex surface remains available and typed:

  • ctx.db.query('table')
  • ctx.runQuery(...)
  • ctx.runMutation(...)
  • ctx.storage
  • ctx.scheduler

That means helper functions expecting real QueryCtx, MutationCtx, or ActionCtx still accept the wrapped ctx.

Authorized Helpers

authorizedQuery(...) adds:

  • ctx.ownedQuery(tableName)
  • ctx.ownedDoc(tableName, documentId)

authorizedMutation(...) adds:

  • ctx.ownedDoc(tableName, documentId)
  • ctx.ownedMutation.patch(tableName, documentId, patch)
  • ctx.ownedMutation.delete(tableName, documentId)

These remain typed against your app’s data model when you pass generated builders from _generated/server.

Error Helpers

createError includes:

  • unauthenticated()
  • unauthorized(message?)
  • forbidden(message?)
  • inactiveUser(message?)
  • notFound(entity, id?)
  • conflict(message?)
  • badRequest(message?)
  • internal(message?)

Zod Helpers

import { addSystemFields, zid } from '@amadeni/convex-lib';
import { z } from 'zod';

const userSchema = z.object(
  addSystemFields('users', {
    email: z.string().email(),
    role: z.string().optional(),
  }),
);

const userId = zid('users');

addSystemFields(...) returns a Zod object shape, so wrap it with z.object(...).