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

v0.1.0

Published

Table and schema utilities for Convex with Zod v4

Downloads

10

Readme

@libar-dev/zod-convex-tables

npm version License: MIT TypeScript

Table definition helpers and schema manipulation utilities for Convex with Zod v4.

Features

  • Table Definition Helpers - zodTable for quick table setup with Convex validators
  • Document Type Schemas - Automatic _id and _creationTime field addition
  • Type-Safe Pick/Omit - Extract schema subsets with full type preservation
  • Zero Type Depth Issues - Avoids Zod's recursive type limitations

Installation

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

Peer Dependencies:

  • convex >= 1.30.0 < 2.0.0
  • convex-helpers >= 0.1.111
  • zod ^4.1.0

Quick Start

Table Definition

import { zodTable } from '@libar-dev/zod-convex-tables';
import { z } from 'zod';

// Create table with raw shape
export const Users = zodTable('users', {
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
});

// Use in schema.ts:
export default defineSchema({
  users: Users.table,
});

// Type-safe document:
type User = z.infer<typeof Users.zDoc>;
// { name: string; email: string; role: 'admin' | 'user' | 'guest'; _id: Id<'users'>; _creationTime: number }

Schema Picking

Extract subsets of schemas with full type inference:

import { safePick, safeOmit } from '@libar-dev/zod-convex-tables';

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  firstName: z.string(),
  lastName: z.string(),
  password: z.string(),
});

// Pick specific fields - type-safe!
const PublicUserSchema = safePick(UserSchema, ['email', 'firstName', 'lastName']);
type PublicUser = z.infer<typeof PublicUserSchema>;
// { email: string; firstName: string; lastName: string }

// Omit sensitive fields - type-safe!
const SafeUserSchema = safeOmit(UserSchema, ['password']);
type SafeUser = z.infer<typeof SafeUserSchema>;
// { id: string; email: string; firstName: string; lastName: string }

API Reference

zodTable(name, shape)

Creates a Convex table definition from a Zod schema shape.

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

export const Posts = zodTable('posts', {
  title: z.string(),
  content: z.string(),
  authorId: zid('users'),
  published: z.boolean(),
});

// Properties available:
Posts.table;     // TableDefinition for schema.ts
Posts.shape;     // Raw Zod shape { title: z.string(), ... }
Posts.zDoc;      // Schema with _id and _creationTime
Posts.name;      // 'posts'

Query Return Types:

export const getPost = query({
  args: { id: v.id('posts') },
  handler: async (ctx, { id }) => ctx.db.get(id),
  returns: Posts.zDoc.nullable(),
});

export const listPosts = query({
  handler: async (ctx) => ctx.db.query('posts').collect(),
  returns: z.array(Posts.zDoc),
});

zodDoc(tableName, schema)

Adds document fields (_id, _creationTime) to any Zod object schema:

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

const UserDocSchema = zodDoc('users', UserSchema);
// Adds _id: zid('users') and _creationTime: z.number()

type UserDoc = z.infer<typeof UserDocSchema>;
// { name: string; email: string; _id: Id<'users'>; _creationTime: number }

zodDocOrNull(tableName, schema)

Creates a nullable document schema (useful for query returns):

const MaybeUserSchema = zodDocOrNull('users', UserSchema);
// Same as zodDoc but with | null

pickShape(schemaOrShape, keys)

Extracts raw shape for function arguments:

import { pickShape } from '@libar-dev/zod-convex-tables';
import { zodToConvexFields } from '@libar-dev/zod-convex-core';

const loginShape = pickShape(UserSchema, ['email', 'password']);

export const login = mutation({
  args: zodToConvexFields(loginShape),
  handler: async (ctx, { email, password }) => {
    // Type-safe: email and password are inferred
  },
});

safePick(schema, keys)

Creates new schema with selected fields (type-safe):

import { safePick } from '@libar-dev/zod-convex-tables';

const UpdateSchema = safePick(UserSchema, ['firstName', 'lastName', 'email']);

type UpdateData = z.infer<typeof UpdateSchema>;
// { firstName: string; lastName: string; email: string }

safeOmit(schema, keys)

Creates schema without specified fields (type-safe):

import { safeOmit } from '@libar-dev/zod-convex-tables';

const PublicUserSchema = safeOmit(UserSchema, ['password', 'internalId']);

type PublicUser = z.infer<typeof PublicUserSchema>;
// All fields except password and internalId

Why Safe Pick/Omit?

Zod's built-in pick() and omit() can trigger TypeScript's type depth limits with complex schemas. Our safe versions avoid this:

// ❌ Can cause: "Type instantiation is excessively deep"
const Picked = ComplexSchema.pick({ field1: true, field2: true });

// ✅ Works with any schema complexity
const Picked = safePick(ComplexSchema, ['field1', 'field2']);

Usage Patterns

Table-Centric Development

// convex/schema/users.ts
export const Users = zodTable('users', {
  email: z.string().email(),
  name: z.string(),
  bio: z.string().optional(),
  verified: z.boolean(),
});

// convex/users.ts
import { Users } from './schema/users';

export const createUser = mutation({
  args: zodToConvexFields(Users.shape),
  handler: async (ctx, userData) => {
    return ctx.db.insert('users', userData);
  },
});

export const getUser = query({
  args: { id: v.id('users') },
  handler: async (ctx, { id }) => ctx.db.get(id),
  returns: Users.zDoc.nullable(),
});

Partial Updates

const Users = zodTable('users', {
  email: z.string().email(),
  name: z.string(),
  bio: z.string().optional(),
});

// Partial update schema
const UpdateUserSchema = safePick(z.object(Users.shape), ['name', 'bio']);

export const updateProfile = mutation({
  args: {
    id: v.id('users'),
    ...zodToConvexFields(UpdateUserSchema.partial().shape),
  },
  handler: async (ctx, { id, ...updates }) => {
    await ctx.db.patch(id, updates);
  },
});

With Authentication Context

const Users = zodTable('users', {
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
  profile: z.object({
    name: z.string(),
    bio: z.string().optional(),
  }),
});

// Public info only
const PublicUserSchema = safePick(z.object(Users.shape), ['profile']);

export const getPublicProfile = query({
  args: { id: v.id('users') },
  handler: async (ctx, { id }) => {
    const user = await ctx.db.get(id);
    return user ? PublicUserSchema.parse(user) : null;
  },
  returns: PublicUserSchema.nullable(),
});

Type Safety

All utilities maintain full TypeScript type inference:

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  password: z.string(),
});

// Full type inference
const PublicSchema = safeOmit(UserSchema, ['password']);

type PublicUser = z.infer<typeof PublicSchema>;
// { id: string; name: string; email: string }
// ^? TypeScript knows exactly which fields are present

// Compile-time error for invalid keys
const Invalid = safePick(UserSchema, ['nonexistent']);
//                                     ^^^^^^^^^^^
// Error: Type '"nonexistent"' is not assignable to type 'keyof ...'

Migration from convex-helpers

If you're migrating from convex-helpers table utilities, here's how to adapt your code.

Table Definitions

convex-helpers approach:

// With convex-helpers
import { zodToConvex } from 'convex-helpers/server/zod';
import { defineSchema, defineTable } from 'convex/server';
import { z } from 'zod/v3';  // Note: Zod v3

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

export default defineSchema({
  users: defineTable(zodToConvex(UserSchema))
    .index('by_email', ['email']),
});

@libar-dev/zod-convex-tables approach:

// With zod-convex-tables
import { zodTable } from '@libar-dev/zod-convex-tables';
import { defineSchema } from 'convex/server';
import { z } from 'zod';  // Zod v4 native

export const Users = zodTable('users', {
  name: z.string(),
  email: z.string().email(),
});

export default defineSchema({
  users: Users.table
    .index('by_email', ['email']),
});

// Bonus: Get document type with _id and _creationTime
type UserDoc = z.infer<typeof Users.zDoc>;

Key Differences

| Feature | convex-helpers | @libar-dev/zod-convex-tables | |---------|---------------|------------------------------| | Zod Version | v3 (via zod/v3) | v4 native | | Table Definition | Manual defineTable() | zodTable() helper | | Document Schema | Manual composition | Automatic zDoc property | | Pick/Omit | Uses Zod's .pick() | Type-safe safePick() / safeOmit() | | Type Depth Issues | Can hit TS2589 | Avoided by design |

Pick/Omit Migration

convex-helpers:

// Can cause TS2589 with complex schemas
const PublicUser = UserSchema.pick({ email: true, name: true });
const SafeUser = UserSchema.omit({ password: true });

zod-convex-tables:

// Type-safe and TS2589-resistant
import { safePick, safeOmit } from '@libar-dev/zod-convex-tables';

const PublicUser = safePick(UserSchema, ['email', 'name']);
const SafeUser = safeOmit(UserSchema, ['password']);

Step-by-Step Migration

  1. Update dependencies:

    npm install @libar-dev/zod-convex-tables zod@^4
    # convex-helpers is still required (peer dependency used by zodTable)
  2. Update imports:

    // Before
    import { zodToConvex } from 'convex-helpers/server/zod';
    import { z } from 'zod/v3';
    
    // After
    import { zodTable } from '@libar-dev/zod-convex-tables';
    import { z } from 'zod';
  3. Replace table definitions:

    // Before
    export default defineSchema({
      users: defineTable(zodToConvex(UserSchema)),
    });
    
    // After
    export const Users = zodTable('users', {
      name: z.string(),
      email: z.string().email(),
    });
    
    export default defineSchema({
      users: Users.table,
    });
  4. Replace pick/omit:

    // Before
    const Partial = Schema.pick({ field1: true });
    
    // After
    import { safePick } from '@libar-dev/zod-convex-tables';
    const Partial = safePick(Schema, ['field1']);

Related Packages

License

MIT License - See LICENSE file for details.

Copyright (c) 2025 Libar AI