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

@libar-dev/zod-convex-builders

v0.1.0

Published

Authentication-aware function builders for Convex with Zod v4

Readme

@libar-dev/zod-convex-builders

npm version License: MIT TypeScript

Authentication-aware function builders for Convex with zero-runtime validation using generated PropertyValidators.

Why This Package?

Zero-Cast Integration

  • HTTP actions: Function pattern eliminates FunctionReference casts
  • Log payloads: Fixed Record<string, unknown> (no generic needed)
  • 3 generic parameters: Avoids generic explosion (DataModel, TUserId, TMembership)
  • 50-70% code reduction: Similar to convex-workflows migration

Core Features

  • 90% Bundle Reduction - Zero runtime Zod (60-65MB → 10-15MB) via generated PropertyValidators
  • Full Type Safety - Type-only Zod imports with complete type inference
  • Authentication Builders - Create auth-aware queries, mutations, actions, AND HTTP endpoints with branded types
  • HTTP Authentication - Centralized CORS + auth for HTTP actions (zero manual infrastructure, zero casts)
  • Return Validation - Optional runtime validation for function boundaries
  • Test Utilities - Helpers for testing functions with VObject return types

Installation

npm install @libar-dev/zod-convex-builders zod@^4.1.0

Peer Dependencies:

  • convex >=1.30.0 <2.0.0
  • zod ^4.1.0 (build-time only, zero runtime cost)

Dependencies:

  • @libar-dev/zod-convex-core - Core conversion engine
  • @libar-dev/zod-convex-codecs - Bidirectional codecs

Build-Time Tools:

  • @libar-dev/zod-convex-gen - Validator generation CLI (REQUIRED for 90% reduction)

Quick Start

1. Define Schema (Build-Time Only)

// src/validation-schemas/getUser.ts
import { z } from 'zod';
import { createIdValidators } from '@libar-dev/zod-convex-ids';

const ids = createIdValidators({ user: 'users' } as const);

export const GetUserArgsSchema = z.object({
  userId: ids.userId(),
}).strict();

export type GetUserArgs = z.infer<typeof GetUserArgsSchema>; // REQUIRED

2. Generate Validators

npm run generate:validators
# Creates: convex/generatedValidators/getUser.ts

3. Use in Convex Functions

// convex/users.ts
import type { GetUserArgs } from '../src/validation-schemas/getUser';
import { getUserArgsFields } from './generatedValidators/getUser';
import { zQuery } from '@libar-dev/zod-convex-builders';
import { query } from './_generated/server';

export const getUser = zQuery(
  query,
  getUserArgsFields,  // PropertyValidators (NO runtime Zod)
  async (ctx, args: GetUserArgs) => {
    return await ctx.db.get(args.userId);
  }
);

👉 For complete end-to-end guide, see examples/withReturns.ts

Core Features

Function Wrappers

| Wrapper | Visibility | Use Case | |---------|-----------|----------| | zQuery, zMutation, zAction | Public | Client-callable functions | | zInternalQuery, zInternalMutation, zInternalAction | Internal | Server-only functions | | zCustomQuery, zCustomMutation, zCustomAction | Configurable | Custom context extension |

Authentication Builders

Create auth-aware functions with zero-cast integration:

import { createAuthWrappers } from '@libar-dev/zod-convex-builders';

const authWrappers = createAuthWrappers<DataModel, UserId, Membership>(
  {
    getAuthenticatedUserId: async (ctx) => asUserId(identity.subject),
    getUserMemberships: async (ctx, userId) => getMemberships(ctx, userId),
    workflowUserMarker: asWorkflowUserId('WORKFLOW_USER'),
    isAdminRole: (membership) => membership.role === 'admin',
    // ... more config
  },
  { query, mutation, action }
);

export const zAuthenticatedQuery = authWrappers.zAuthenticatedQuery;
export const zAuthenticatedMutation = authWrappers.zAuthenticatedMutation;
// ... more exports

Available Builders:

  • zAuthenticatedQuery/Mutation/Action - Requires authentication
  • zOptionalAuthQuery/Mutation - Optional authentication
  • zAdminQuery/Mutation - Admin-only access

👉 For complete authentication setup, see examples/README.md

HTTP Authentication Wrappers

Create auth-aware HTTP actions with centralized CORS and zero infrastructure code:

import { createHttpAuthWrappers } from '@libar-dev/zod-convex-builders';
import { httpAction } from './_generated/server';
import { internal } from './_generated/api';

const httpAuthWrappers = createHttpAuthWrappers<DataModel, UserId, Membership>(
  {
    getUserIdFromIdentity: (subject) => asUserId(subject),

    // Function pattern (NOT FunctionReference) - consumer writes wrapper
    getUserMemberships: async (ctx, userId) => {
      return await ctx.runQuery(internal.auth.getMemberships, { userId });
    },

    verifyEntityAccess: async (ctx, userId, entityId) => {
      return await ctx.runQuery(internal.auth.checkEntityAccess, { userId, entityId });
    },

    cors: {
      clientOrigin: process.env.CLIENT_ORIGIN,
      fallbackOrigin: 'http://localhost:3000',
    },
    log: {
      info: (_ctx, data) => console.log(data),
      error: (_ctx, data) => console.error(data),
    },
  },
  httpAction
);

export const { httpAuthenticatedAction } = httpAuthWrappers;

// Usage - ZERO authentication/CORS code in handler!
export const streamAiMessage = httpAuthenticatedAction({
  handler: async (ctx, request) => {
    // ctx.userId is UserId (typed, authenticated)
    // ctx.memberships is Membership[] (typed)
    // CORS headers added automatically
    // ZERO casts!

    const body = await request.json();
    // ... business logic only
    return new Response(JSON.stringify({ success: true }));
  }
});

Available Builders:

  • httpAuthenticatedAction - Requires authentication, returns 401 if not authenticated
  • httpOptionalAuthAction - Optional authentication (userId/memberships null if not authenticated)
  • httpAdminAction - Admin-only access, returns 403 if not admin

Key Benefits:

  • Zero manual authentication - Wrapper handles identity verification
  • Automatic CORS headers - Centralized configuration
  • Typed contexts - Branded UserId and Membership types flow automatically
  • Optional authorization - ctx.verifyEntityAccess(entityId) helper
  • Response-based errors - 401/403 responses, not exceptions
  • Zero casts - Function pattern eliminates all type casts

Why Function Pattern Instead of FunctionReference?

Design Decision: HTTP wrappers use function pattern (not FunctionReference) for zero-cast integration.

Reason: HTTP actions cannot access ctx.db directly. Auth wrappers have a unique requirement that workflows don't: wrappers must call these functions internally with generic-typed arguments and preserve perfect type flow.

Pattern Comparison

| Aspect | FunctionReference Pattern | Function Pattern (Used Here) | |--------|--------------------------|------------------------------| | Wrapper Casts | 3 casts to any required | 0 casts ✅ | | Consumer Code | Simple (direct reference) | One-time wrapper (~3 lines) | | Type Safety | ⚠️ Casts break type safety | ✅ Full inference chain | | Pattern Alignment | ✅ Framework-native type | ✅ Zero-cast goal (broader principle) | | Use Case | Pass reference to engine | Wrapper calls function directly |

Key Insight: Context Matters

The Convex-Native pattern recommends FunctionReference as a concrete framework type. However, auth wrappers differ from workflows:

Workflows (where FunctionReference works):

  • Pass FunctionReference to engine (engine handles calls)
  • No type preservation needed
  • FunctionReference pattern is perfect

Auth Wrappers (why function pattern is needed):

  • Wrapper calls function directly with generic-typed TUserId
  • Must preserve TUserIdTMembership[] type flow
  • FunctionReference requires casts that defeat zero-cast goal

Consumer writes one-time wrapper:

getUserMemberships: async (ctx, userId) => {
  return await ctx.runQuery(internal.auth.getMemberships, { userId });
}
// ✅ Zero casts! Types flow automatically: TUserId → TMembership[]

Trade-off Analysis:

  • FunctionReference: Simple config (direct reference) but 3 casts in wrapper internals
  • Function Pattern: One-time wrapper (~3 lines per function) but zero casts everywhere

Decision: Function pattern prioritizes the broader Convex-Native goal (zero-cast integration, perfect type flow) over strict framework type usage. This is a pragmatic deviation that serves the pattern's core principle.

Benefits:

  • Zero casts (vs 3 casts with FunctionReference pattern)
  • Consistent with query/mutation wrappers pattern (same function approach)
  • Type-safe at all boundaries - complete inference chain
  • Consumer controls integration layer - explicit wrapper code

👉 For HTTP action patterns, see examples/README.md

Return Type Validation

Add runtime validation to function boundaries:

export const getUser = zQuery(
  query,
  getUserArgsFields,
  async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
  {
    returns: v.union(v.object(userDocFields), v.null()),  // Runtime validation
  }
);

👉 For return validators guide, see examples/withReturns.ts

Test Utilities

Handle VObject types in tests:

import { unwrapForTest, isNonNull, extractPage } from '@libar-dev/zod-convex-builders/testUtils';

// Option 1: Type annotation (recommended)
const result = (await mutation(...)) as MessageCreationResult;

// Option 2: Unwrap helper
const result = unwrapForTest<MessageCreationResult>(await mutation(...));

// Nullable results
if (isNonNull(result)) {
  expect(result.name).toBe('John');
}

// Pagination
const items = extractPage(paginatedResult);

Available Utilities:

  • unwrapForTest<T>() - Unwrap VObject types
  • unwrapAsyncForTest<T>() - Async unwrap
  • isNonNull<T>() - Type guard for nullable
  • extractPage<T>() - Extract pagination array
  • isDiscriminatedAs() - Type guard for unions

👉 For complete testing patterns, see examples/testPatterns.ts

Branded Types

Type-safe branded types for domain modeling:

import {
  type UserId,
  type WorkflowUserId,
  asUserId,
  asWorkflowUserId,
  isWorkflowUserId,
} from '@libar-dev/zod-convex-builders';

const userId = asUserId('user_123');
const workflowId = asWorkflowUserId('WORKFLOW_USER');

if (isWorkflowUserId(ctx.userId)) {
  // Handle workflow context
}

Examples & Resources

Complete Guides

| File | Description | Lines | |------|-------------|-------| | examples/withReturns.ts | Complete end-to-end flow from schema → validator → function → test | 397 | | examples/testPatterns.ts | 7 comprehensive testing patterns for all return types | 519 | | examples/commonTypes.ts | Reusable schema library (pagination, results, CRUD, etc.) | 438 | | examples/README.md | Navigation guide and troubleshooting | 323 |

Quick Navigation

New to the package?

  1. Read examples/withReturns.ts for complete understanding
  2. Reference examples/README.md for common scenarios

Writing tests? → See examples/testPatterns.ts for all 7 testing patterns

Building schemas? → See examples/commonTypes.ts for reusable patterns

Migrating from v0.0.x? → See Migration from Runtime Zod below + examples/README.md

Troubleshooting? → See examples/README.md#troubleshooting

API Reference

Function Wrappers

import {
  zQuery,
  zInternalQuery,
  zMutation,
  zInternalMutation,
  zAction,
  zInternalAction,
} from '@libar-dev/zod-convex-builders';

// Usage: zQuery(builder, argsFields, handler, options?)
export const getUser = zQuery(query, getUserArgsFields, async (ctx, args) => {
  return await ctx.db.get(args.userId);
});

Authentication Wrappers Factory

import { createAuthWrappers } from '@libar-dev/zod-convex-builders';

const wrappers = createAuthWrappers<DataModel, UserId, Membership>(
  config,
  { query, mutation, action }
);

// Returns:
// - zAuthenticatedQuery, zAuthenticatedMutation, zAuthenticatedAction
// - zOptionalAuthQuery, zOptionalAuthMutation
// - zAdminQuery, zAdminMutation (if isAdminRole provided)

HTTP Authentication Wrappers Factory

import { createHttpAuthWrappers } from '@libar-dev/zod-convex-builders';

const httpWrappers = createHttpAuthWrappers<DataModel, UserId, Membership>(
  {
    getUserIdFromIdentity: (subject) => asUserId(subject),

    // Function pattern for zero-cast integration
    getUserMemberships: async (ctx, userId) => {
      return await ctx.runQuery(internal.auth.getMemberships, { userId });
    },

    verifyEntityAccess: async (ctx, userId, entityId) => {
      return await ctx.runQuery(internal.auth.checkEntityAccess, { userId, entityId });
    },

    cors: {
      clientOrigin: process.env.CLIENT_ORIGIN,
      fallbackOrigin: 'http://localhost:3000',
    },
    log: {
      info: (_ctx, data) => console.log(data),
      error: (_ctx, data) => console.error(data),
    },
  },
  httpAction
);

// Returns:
// - httpAuthenticatedAction - Requires authentication
// - httpOptionalAuthAction - Optional authentication
// - httpAdminAction - Admin-only (if isAdminRole provided)

Custom Function Integration

import { zCustomQuery, zCustomMutation, zCustomAction, NoOp } from '@libar-dev/zod-convex-builders';

export const zq = zCustomQuery(query, NoOp);
export const zm = zCustomMutation(mutation, NoOp);

Test Utilities (TEST CODE ONLY)

import {
  unwrapForTest,
  unwrapAsyncForTest,
  isNonNull,
  extractPage,
  isDiscriminatedAs,
} from '@libar-dev/zod-convex-builders/testUtils';

Branded Types

import {
  type UserId,
  type WorkflowUserId,
  type MembershipId,
  type AnyUserId,
  asUserId,
  asWorkflowUserId,
  asMembershipId,
  isWorkflowUserId,
} from '@libar-dev/zod-convex-builders';

Memory Optimization

PropertyValidators-Only Pattern achieves massive bundle reduction:

  • Before: ~60-65MB bundle (runtime Zod validation)
  • After: ~10-15MB bundle (generated PropertyValidators)
  • Reduction: 75-83% decrease in memory usage

How It Works

  1. Define Zod schemas in src/validation-schemas/ (outside Convex bundle)
  2. Run npm run generate:validators to create PropertyValidators
  3. Import type-only Zod (import type { z }) in Convex functions
  4. Use generated validators (getUserArgsFields) at runtime

Result: Zero runtime Zod, full type safety maintained.

Verify Bundle Reduction

# Build and check bundle size
npx convex dev --once --debug-bundle-path
ls -lh .convex/bundle/  # Expected: ~10-15MB

# Compare with runtime Zod (before migration)
du -sh .convex/bundle/  # Should show 75-83% reduction

Tested environment: 56+ table schemas, 100+ functions, production Convex app

Migration from Runtime Zod

If upgrading from v0.0.x (runtime Zod pattern):

Before (v0.0.x - DEPRECATED)

import { z } from 'zod';  // Runtime import ❌
import { zQuery } from '@libar-dev/zod-convex-builders';

export const getUser = zQuery(
  query,
  z.object({ userId: z.string() }),  // Runtime Zod ❌
  async (ctx, args) => { /* ... */ }
);

After (v0.1.0+ - REQUIRED)

// 1. Move schema to src/validation-schemas/getUser.ts
export const GetUserArgsSchema = z.object({ userId: ids.userId() }).strict();
export type GetUserArgs = z.infer<typeof GetUserArgsSchema>; // ✅ REQUIRED

// 2. Generate: npm run generate:validators

// 3. Use PropertyValidators
import type { GetUserArgs } from '../src/validation-schemas/getUser';  // ✅ Type-only
import { getUserArgsFields } from './generatedValidators/getUser';  // ✅ Generated

export const getUser = zQuery(query, getUserArgsFields, async (ctx, args: GetUserArgs) => {
  return await ctx.db.get(args.userId);
});

Migration Checklist

  • [ ] Move Zod schemas from convex/ to src/validation-schemas/
  • [ ] Add type alias exports to all schemas (export type)
  • [ ] Run npm run generate:validators
  • [ ] Update functions to use generated validators
  • [ ] Change import { z }import type { z } in Convex files
  • [ ] Verify bundle: Should be ~10-15MB (down from 60-65MB)

👉 For complete migration guide, see examples/README.md

Convex-Native Pattern

This package uses optional generic parameters with defaults for zero-cast integration:

export interface AuthConfig<
  DataModel extends GenericDataModel,
  TUserId extends string = string,      // ✅ Default: backward compatible
  TMembership = unknown                   // ✅ Flexible default
>

Convex-Native Pattern: Limited to 3 type parameters maximum:

  • DataModel (Convex GenericDataModel)
  • TUserId (User ID type, default: string)
  • TMembership (Membership type, default: unknown)

Note: Previous versions had 4 parameters (including TLogPayload), but this was removed to avoid generic explosion. Log payloads now use fixed Record<string, unknown> type.

Benefits:

  • Zero casts - Types flow from factory config through all consumers
  • Centralized config - Configure once, use everywhere
  • Backward compatible - Defaults work for existing code
  • Type safety - Full branded type support with IntelliSense
  • 50-70% code reduction - Function pattern eliminates wrapper overhead

Development

Building from Source

# Monorepo development
npm run build:packages  # Builds all packages

# Standalone development
npm run build  # Uses committed validators

Modifying Schemas

# After editing src/schemas/
npm run generate:validators
git add src/generatedValidators/
npm run build

Published Package: Ships with pre-built dist/ - no build step required.

Performance

Validation Overhead

  • Validation happens once at function boundary
  • Return validation is optional - use when needed
  • Codecs are cached - no repeated parsing
  • Type inference is compile-time - zero runtime overhead

PropertyValidators Performance

PropertyValidators Path (RECOMMENDED):

  • Zero Zod parsing - validators passed directly to Convex runtime
  • Single validation - Convex validates once at function entry
  • No codec conversion - args passed through without transformation
  • Result: 75-83% memory reduction (60-65MB → 10-15MB)

Validation Strength

PropertyValidators provide complete validation guarantees:

  • Structural validation (types, IDs, nested objects)
  • Business rules (email format, string length, refinements)

Key Insight: PropertyValidators use Convex's optimized validator system, generated from Zod schemas. Business rules preserved during generation.

Visibility Preservation

The builders correctly preserve Convex's visibility type system:

// Public - appears in api.queries
export const myPublicQuery = zAuthenticatedQuery({ ... });

// Internal - appears in api.internal.queries only
export const myInternalQuery = zInternalAuthenticatedQuery({ ... });

Runtime properties:

myPublicQuery.isPublic === true;
myInternalQuery.isInternal === true;

Related Packages

License

MIT License - See LICENSE file for details.

Copyright (c) 2025 Libar AI