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

v0.1.0

Published

Lightweight ID validation for Convex with Zod v4

Downloads

22

Readme

@libar-dev/zod-convex-ids

npm version License: MIT TypeScript

Type-safe ID validation for Convex tables using Zod v4 with enhanced type tracking.

Why This Package?

Background

This package provides the foundational ID validation system for the @libar-dev/zod-convex ecosystem. While convex-helpers provides basic zid() functionality for Zod v3, this package extends it with enhanced features for Zod v4 compatibility and type-level tracking.

What This Package Provides

Type-Level Table Name Tracking - _tableName property for compile-time validation ✅ Zod v4 Native Support - Uses .brand() for type-safe ID validation ✅ Metadata Registry - WeakMap-based registry for runtime introspection ✅ ID Validator Factory - Generate typed validators from table mappings ✅ Zero Dependencies - Lightweight with only peer dependencies (zod, convex)

Compatibility Note

The zid() API is compatible with convex-helpers while adding Zod v4 support and enhanced type tracking. If you're migrating from convex-helpers, the API remains familiar.

Installation

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

Peer Dependencies:

  • convex >=1.30.0 <2.0.0
  • zod ^4.1.0

Quick Start

Basic ID Validation

import { zid } from '@libar-dev/zod-convex-ids';
import { z } from 'zod';

// Create ID validator for 'users' table
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  organizationId: zid('organizations'), // Type-safe ID reference
});

// TypeScript knows organizationId is Id<'organizations'>
type User = z.infer<typeof UserSchema>;

ID Validator Factory

Create reusable ID validators for your entire schema:

import { createIdValidators } from '@libar-dev/zod-convex-ids';

// Define your table mappings
const ids = createIdValidators({
  user: 'users',
  company: 'companies',
  file: 'files',
  invoice: 'invoices',
} as const);

// Use generated validators
const InvoiceSchema = z.object({
  amount: z.number(),
  userId: ids.userId(),        // Returns Zid<'users'>
  companyId: ids.companyId(),  // Returns Zid<'companies'>
  fileId: ids.fileId(),        // Returns Zid<'files'>
}).strict();

// In mutations with zod-convex-core
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { mutation } from './_generated/server';

export const createInvoice = mutation({
  args: zodToConvex(InvoiceSchema),
  handler: async (ctx, args) => {
    // args.userId is properly typed as Id<'users'>
    const user = await ctx.db.get(args.userId);
    // TypeScript prevents ID type mismatches
    return await ctx.db.insert('invoices', args);
  }
});

Common Chaining Patterns

ID validators support all standard Zod methods:

import { createIdValidators } from '@libar-dev/zod-convex-ids';
import { z } from 'zod';

const ids = createIdValidators({
  user: 'users',
  invoice: 'invoices',
  receipt: 'receipts',
  tag: 'tags',
} as const);

// Optional IDs
const TaskSchema = z.object({
  title: z.string(),
  assigneeId: ids.userId().optional(),      // Id<'users'> | undefined
  approvedBy: ids.userId().nullable(),       // Id<'users'> | null
});

// Arrays of IDs
const ProjectSchema = z.object({
  name: z.string(),
  memberIds: z.array(ids.userId()),          // Id<'users'>[]
  tagIds: z.array(ids.tagId()).optional(),   // Id<'tags'>[] | undefined
});

// Union types for polymorphic references
const PaymentSchema = z.object({
  amount: z.number(),
  documentId: z.union([
    ids.invoiceId(),
    ids.receiptId(),
  ]),  // Id<'invoices'> | Id<'receipts'>
});

API Documentation

Core Functions

zid(tableName)

Creates a type-safe Convex ID validator using Zod v4's .brand() method.

import { zid } from '@libar-dev/zod-convex-ids';

// Create ID validator
const userIdSchema = zid('users');

// TypeScript infers: z.ZodType<Id<'users'>>
type UserId = z.infer<typeof userIdSchema>;

// Runtime validation
const result = userIdSchema.safeParse('user_123abc');
if (result.success) {
  // result.data is Id<'users'>
  const user = await ctx.db.get(result.data);
}

Key Features:

  • Returns z.ZodType<Id<TableName>> with _tableName property
  • Uses Zod v4's native .brand() for type safety
  • Stores metadata in WeakMap registry
  • Provides .describe() annotation: convexId:tableName

createIdValidators(mapping)

Creates a factory object with ID validators for each table.

const ids = createIdValidators({
  user: 'users',
  post: 'posts',
  comment: 'comments',
} as const);

// Usage
const CommentSchema = z.object({
  text: z.string(),
  authorId: ids.userId(),     // Zid<'users'>
  postId: ids.postId(),       // Zid<'posts'>
  parentId: ids.commentId().optional(), // Zid<'comments'> | undefined
});

Type Utilities

import type { Zid, IdValidators } from '@libar-dev/zod-convex-ids';

// Zid<T> represents the return type of zid()
type UserIdValidator = Zid<'users'>;
type PostIdValidator = Zid<'posts'>;

// IdValidators type for factory results
type MyIds = IdValidators<{
  user: 'users';
  post: 'posts';
}>;

Registry Helpers

Access metadata about ID validators:

import { registryHelpers } from '@libar-dev/zod-convex-ids';

const userIdSchema = zid('users');

// Get metadata
const metadata = registryHelpers.getMetadata(userIdSchema);
// { isConvexId: true, tableName: 'users' }

// Check if schema is a Convex ID validator
const isId = registryHelpers.isConvexId(userIdSchema); // true
const isId2 = registryHelpers.isConvexId(z.string()); // false

Type Safety Benefits

The zid() function provides compile-time and runtime safety:

// Compile-time table name tracking
function processUser(userId: Id<'users'>) {
  // TypeScript enforces correct table reference
}

function processPost(postId: Id<'posts'>) {
  // Different ID type - not interchangeable
}

const schema = z.object({
  userId: zid('users'),
  postId: zid('posts'),
});

export const linkUserToPost = mutation({
  args: zodToConvex(schema),
  handler: async (ctx, args) => {
    // TypeScript prevents this mistake:
    // const user = await ctx.db.get(args.postId); // ❌ Type error!

    // Correct usage:
    const user = await ctx.db.get(args.userId);  // ✅
    const post = await ctx.db.get(args.postId);  // ✅
  }
});

Integration Patterns

With Table Definitions

import { zid } from '@libar-dev/zod-convex-ids';
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { defineTable } from 'convex/server';

const PostSchema = z.object({
  title: z.string(),
  content: z.string(),
  authorId: zid('users'),
  categoryId: zid('categories').optional(),
});

export const posts = defineTable(zodToConvex(PostSchema))
  .index('by_author', ['authorId'])
  .index('by_category', ['categoryId']);

With Complex Schemas

// Nested ID references
const CommentSchema = z.object({
  text: z.string(),
  authorId: zid('users'),
  postId: zid('posts'),
  parentId: zid('comments').nullable(), // Self-reference
  mentionedUserIds: z.array(zid('users')), // Array of IDs
});

// Union types with IDs
const NotificationSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('comment'),
    commentId: zid('comments'),
    userId: zid('users'),
  }),
  z.object({
    type: z.literal('like'),
    postId: zid('posts'),
    userId: zid('users'),
  }),
]);

Integration with Generated Validators

When using @libar-dev/zod-convex-gen for build-time validator generation, zid() types are preserved and converted to v.id() validators automatically.

How It Works

The generator introspects Zod schemas and converts zid() calls to their Convex equivalents:

// Source Zod Schema (src/table-schemas/posts.ts)
import { z } from 'zod';
import { zid } from '@libar-dev/zod-convex-ids';

export const PostsTableSchema = z.object({
  authorId: zid('users'),      // Type-safe ID validation
  categoryId: zid('categories'),
  title: z.string(),
  content: z.string(),
}).strict();
// Generated Convex Validator (convex/generatedValidators/posts.ts)
import { v } from 'convex/values';

export const postsFields = {
  authorId: v.id('users'),      // Converted to Convex ID validator
  categoryId: v.id('categories'),
  title: v.string(),
  content: v.string(),
} as const;

Type Preservation

The table name from zid('tableName') is preserved during generation:

  • Compile-time: TypeScript tracks table names via Zid<'tableName'> branded type
  • Generation-time: Generator extracts table name and creates v.id('tableName')
  • Runtime: Convex validates IDs against the specified table

Usage Example

// 1. Define schema with zid() (outside Convex bundle)
// src/table-schemas/comments.ts
export const CommentsTableSchema = z.object({
  postId: zid('posts'),
  authorId: zid('users'),
  text: z.string(),
}).strict();

// 2. Generate validators (build-time)
// npx zod-convex-gen

// 3. Use generated validators (zero Zod in bundle)
// convex/schema.ts
import { commentsFields } from './generatedValidators/comments';

export default defineSchema({
  comments: defineTable(commentsFields)
    .index('by_post', ['postId'])
    .index('by_author', ['authorId'])
});

Memory Impact

Using zid() with generated validators provides the best of both worlds:

  • Type safety from Zid<'tableName'> branded types
  • Reduced memory overhead - Zod not included in Convex bundle
  • Runtime validation - Convex validates table names at runtime
  • Significant memory reduction compared to runtime Zod (75-83% decrease in tested applications)

Related Package

See @libar-dev/zod-convex-gen for complete build-time generation documentation and workflow examples.

Build-Time Schema Pattern (Local Factory)

When creating build-time schemas in src/validation-schemas/ or src/table-schemas/ that will be processed by @libar-dev/zod-convex-gen, you cannot import from the main application's convex/ directory. Instead, create a local ID validator factory within the schema file:

Pattern:

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

// Create local factory for build-time generation
const ids = createIdValidators({
  company: 'companies',
  file: 'files',
  userMessage: 'aiChatMessages',
  businessAccount: 'businessAccounts',
} as const);

export const Camt053ArgsSchema = z.object({
  companyId: ids.companyId(),
  fileId: ids.fileId(),
  userMessageId: ids.userMessageId().optional(),
}).strict();

Why Local Factory:

  • Build-time schemas execute outside the main app context
  • Generator cannot resolve imports from convex/ directory
  • Local factory provides same type safety as application factory
  • Generated validators are identical (v.id('aiChatMessages'))

When to Use:

  • src/validation-schemas/ - Build-time function argument schemas
  • src/table-schemas/ - Build-time table schemas (if using generator)

When NOT to Use:

  • convex/ directory code - Use application factory (import { ids } from '../toolkit/validation/idValidators')
  • Runtime validation - Application factory is always available

Generated Output:

The generator extracts table names from both patterns identically:

// Generated: convex/generatedValidators/camt053Processor.ts
export const camt053ArgsFields = {
  userMessageId: v.optional(v.id('aiChatMessages')) // Identical to application factory
} as const;

Pattern Comparison:

| Location | Pattern | Import From | |----------|---------|-------------| | convex/ directory | Application Factory | import { ids } from '../toolkit/validation/idValidators' | | src/validation-schemas/ | Local Factory | import { createIdValidators } from '@libar-dev/zod-convex-ids' | | src/table-schemas/ | Local Factory | import { createIdValidators } from '@libar-dev/zod-convex-ids' |

Known Limitations

Test Environment Behavior

The zid() validator performs minimal validation in test environments:

// In tests (outside Convex runtime)
const schema = z.object({ userId: zid('users') });
const result = schema.safeParse({ userId: 'any-string' });
// result.success = true (accepts ANY string in tests)

// In Convex runtime
// Full validation is performed (table name and ID format)

Why This Design Decision?

This behavior is intentional to simplify testing:

  • ✅ No need to generate valid Convex IDs in test data
  • ✅ Tests focus on business logic, not ID format
  • ✅ Faster test execution (no ID validation overhead)
  • ❌ Runtime validation still enforced in production

When This Matters:

During testing, you can use any string format for IDs:

// All these work in tests:
schema.safeParse({ userId: 'test-user' });       // ✅ Passes
schema.safeParse({ userId: '12345' });           // ✅ Passes
schema.safeParse({ userId: 'abc-def-ghi' });     // ✅ Passes

// In Convex runtime, only valid IDs work:
// { userId: 'k17abc123def456' } // ✅ Valid Convex ID format
// { userId: 'test-user' }       // ❌ Rejected by Convex

Best Practices for Test Data:

While any string works, use realistic ID formats for clarity:

// ✅ RECOMMENDED: Numeric format (matches common patterns)
const testUserId = '12345;users' as Id<'users'>;
const testPostId = '67890;posts' as Id<'posts'>;

// ✅ ALTERNATIVE: Descriptive names (easier to debug)
const testUserId = 'test-user-123' as Id<'users'>;

// ⚠️ AVOID: Confusing formats
const testUserId = 'x' as Id<'users'>; // Too short, unclear in logs

Key Insight: The test behavior only affects Zod's .safeParse() validation. Convex's runtime ID validation is always enforced when data enters the database.

Workflow Arguments

Convex workflows require v.id() validators, not Zod schemas:

// ❌ Workflows don't support Zod directly
export const myWorkflow = workflow({
  args: { userId: zid('users') }, // Won't work
});

// ✅ Use v.id() for workflows
export const myWorkflow = workflow({
  args: { userId: v.id('users') },
});

// ✅ Use Zod for regular mutations/queries
export const myMutation = mutation({
  args: zodToConvex(z.object({ userId: zid('users') })),
});

Migration from convex-helpers

The API is compatible with convex-helpers while adding Zod v4 support and enhanced features.

Step 1: Update Dependencies

{
  "dependencies": {
+   "@libar-dev/zod-convex-ids": "^0.1.0",
+   "zod": "^4.1.0"
  },
  "devDependencies": {
-   "convex-helpers": "^0.1.x"  // Can keep for other features
  }
}

Step 2: Update Imports

Before (convex-helpers):

import { zid } from 'convex-helpers/server/zod';
import { z } from 'zod/v3';  // Zod v3

After (@libar-dev/zod-convex-ids):

import { zid } from '@libar-dev/zod-convex-ids';
import { z } from 'zod';  // Zod v4 (no /v3 suffix)

Step 3: Leverage Enhanced Features (Optional)

While the basic API is the same, you can now use additional features:

// Basic usage (same as convex-helpers)
const userId = zid('users');

// NEW: ID validator factory for consistency
import { createIdValidators } from '@libar-dev/zod-convex-ids';

const ids = createIdValidators({
  user: 'users',
  post: 'posts',
  comment: 'comments',
} as const);

// Use factory-generated validators
const CommentSchema = z.object({
  text: z.string(),
  authorId: ids.userId(),     // Type-safe, consistent naming
  postId: ids.postId(),
});

What's Enhanced

Compared to convex-helpers, this package adds:

  • Zod v4 support - Uses native .brand() for type safety
  • Type-level tracking - _tableName property for compile-time validation
  • Metadata registry - Runtime introspection via registryHelpers
  • ID validator factories - Generate validators from table mappings
  • Build-time generation - Works with @libar-dev/zod-convex-gen for memory optimization

Related Packages

License

MIT License - See LICENSE file for details.

Copyright (c) 2025 Libar AI