Skip to content

Instantly share code, notes, and snippets.

@jacobsamo
Created December 24, 2025 08:57
Show Gist options
  • Select an option

  • Save jacobsamo/f1370c4d302fe9bdf78d803158f8fcdd to your computer and use it in GitHub Desktop.

Select an option

Save jacobsamo/f1370c4d302fe9bdf78d803158f8fcdd to your computer and use it in GitHub Desktop.
I created this neat little helper function for defining tables for convex using zod, it utilises the convex-helpers (https://github.com/get-convex/convex-helpers) package for transforming zod into convex schemas, this apporach reduces code duplicated as well as reducing the change of missing a type or field in queries or mutations.
// convex/helpers.ts
import { NoOp } from "convex-helpers/server/customFunctions";
import {
zCustomAction,
zCustomMutation,
zCustomQuery,
} from "convex-helpers/server/zod4";
import { components } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import {
type MutationCtx,
type QueryCtx,
action,
internalMutation,
mutation,
query,
} from "./_generated/server";
async function getUser(ctx: MutationCtx | QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
console.log("IDENTITY", identity);
if (!identity) return null;
const user = await ctx.db
.query("users")
.withIndex("by_id", (q) =>
q.eq("_id", identity.subject as any)
)
.unique();
if (!user) return null;
return user;
}
export const authedMutation = zCustomMutation(mutation, {
args: {},
input: async (ctx, args) => {
const user = await getUser(ctx);
console.log("USER", user);
if (!user) throw new Error("Unauthorized");
return {
ctx: {
...ctx,
user,
},
args,
};
},
});
export const authedQuery = zCustomQuery(query, {
args: {},
input: async (ctx, args) => {
const user = await getUser(ctx);
console.log("USER", user);
if (!user) throw new Error("Unauthorized");
return { ctx: { ...ctx, user }, args };
},
});
export const zodQuery = zCustomQuery(query, NoOp);
export const zodMutation = zCustomMutation(mutation, NoOp);
export const zodInternalMutation = zCustomMutation(internalMutation, NoOp);
export const zodAction = zCustomAction(action, NoOp);
import { zid, zodToConvex } from "convex-helpers/server/zod4";
import { defineTable } from "convex/server";
import * as z from "zod";
/**
* Defines a Convex table schema with automatic _id and _creationTime fields using convex-helpers.
*
* @example
* ```typescript
* // Define a table schema
* const userTable = defineTable("users", {
* clerkUserId: z.string(),
* name: z.string(),
* email: z.string().email(),
* role: z.enum(["admin", "user"])
* });
*
* // Use as a normal Zod schema
* export const userSchema = userTable.schema;
* type User = z.infer<typeof userTable.schema>;
*
* // Get insert schema (optional _id/_creationTime that can't be overridden)
* const insertUserSchema = userTable.insertSchema();
* type InsertUser = z.infer<typeof insertUserSchema>;
*
* // Get update schema (all fields partial, no _id/_creationTime)
* const updateUserSchema = userTable.updateSchema();
* type UpdateUser = z.infer<typeof updateUserSchema>;
*
*
* // Use in Convex schema.ts
* import { defineSchema } from "convex/server";
*
* export default defineSchema({
* users: userTable.table()
* .index("by_email", ["email"])
* });
*
* // Or export just the schema for mutations/queries
* export const { schema: userSchema, insert: userInsert, update: userUpdate } = userTable;
* ```
*
* @param {string} tableName - The name of the Convex table
* @param {{ [key: string]: z.ZodType }} schema - Zod object shape defining the table fields (without _id and _creationTime)
* @returns Object with schema, insert(), update(), table() methods and tableName
*/
export const zodTable = <
Table extends string,
T extends { [key: string]: z.ZodType },
>(
tableName: Table,
schema: T,
) => {
// add _id, _creationTime, and isArchived (for soft deletes)
const fullSchema = z.object({
...schema,
_id: zid(tableName),
_creationTime: z.number(),
});
const insertSchema = fullSchema.partial({
_id: true,
_creationTime: true,
});
const updateSchema = fullSchema
.omit({ _id: true, _creationTime: true })
.partial();
return {
tableName,
/**
* The complete Zod schema including _id and _creationTime.
* Use this for type inference and validation of full table rows.
*
* @example
* type User = z.infer<typeof userTable.schema>;
*/
schema: fullSchema,
/**
* Returns an insert schema where _id and _creationTime are optional.
* These fields cannot be overridden - Convex will generate them automatically.
*
* @example
* ```typescript
*
* // In a mutation
* export const createUser = mutation({
* args: userTable.insertSchema,
* handler: async (ctx, args) => {
* await ctx.db.insert("users", args);
* }
* });
* ```
*/
insertSchema,
/**
* Returns an update schema where all fields are partial and _id/_creationTime are omitted.
* Use this for patch operations where you only want to update specific fields.
*
* @example
* ```typescript
*
* // In a mutation
* export const updateUser = mutation({
* args: {
* userId: zid("users"),
* updates: userTable.updateSchema,
* },
* handler: async (ctx, args) => {
* await ctx.db.patch(args.userId, args.updates);
* }
* });
* ```
*/
updateSchema,
/**
* Converts the Zod schema to a Convex Table
* This uses the zodToConvex helper from convex-helpers. and the defineTable from "convex/server" to return a table
*
* @example
* ```typescript
* import { defineSchema } from "convex/server";
*
* export default defineSchema({
* users: userTable.table()
* .index("by_email", ["email"])
* });
* ```
*/
table: () => {
return defineTable(zodToConvex(fullSchema));
},
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment