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

convex-verify

v1.0.5

Published

Type-safe verification and validation for Convex database operations

Readme

convex-verify

Type-safe verification and validation for Convex database operations.

Features

  • Type-safe insert/patch - Full TypeScript inference for your schema
  • Default values - Make fields optional in insert() with automatic defaults
  • Protected columns - Prevent accidental updates to critical fields in patch()
  • Validation plugins - Unique row/column enforcement, custom validators
  • Extensible - Create your own validation plugins

Installation

pnpm install convex-verify

Peer Dependencies:

  • convex >= 1.31.3

Quick Start

import {
	defaultValuesConfig,
	protectedColumnsConfig,
	uniqueColumnConfig,
	uniqueRowConfig,
	verifyConfig,
} from "convex-verify";

import schema from "./schema";

export const { insert, patch, dangerouslyPatch } = verifyConfig(schema, {
	// Make fields optional with defaults
	defaultValues: defaultValuesConfig(schema, () => ({
		posts: { status: "draft", views: 0 },
	})),

	// Prevent patching critical fields
	protectedColumns: protectedColumnsConfig(schema, {
		posts: ["authorId"],
	}),

	// Enforce unique row combinations
	uniqueRow: uniqueRowConfig(schema, {
		posts: ["by_author_slug"],
	}),

	// Enforce unique column values
	uniqueColumn: uniqueColumnConfig(schema, {
		users: ["by_email", "by_username"],
	}),

	// Custom/third-party plugins (optional)
	plugins: [],
});

Then use in your mutations:

import { insert, patch } from "./verify";

export const createPost = mutation({
	args: { title: v.string(), content: v.string() },
	handler: async (ctx, args) => {
		// status and views are optional - defaults are applied
		return await insert(ctx, "posts", {
			title: args.title,
			content: args.content,
			authorId: ctx.auth.userId,
		});
	},
});

export const updatePost = mutation({
	args: { id: v.id("posts"), title: v.string() },
	handler: async (ctx, args) => {
		// authorId is protected - TypeScript won't allow it here
		await patch(ctx, "posts", args.id, {
			title: args.title,
		});
	},
});

API Reference

verifyConfig(schema, config)

Main configuration function that returns typed insert, patch, and dangerouslyPatch functions.

const { insert, patch, dangerouslyPatch, configs } = verifyConfig(schema, {
  // Type-affecting configs
  defaultValues?: DefaultValuesConfig,
  protectedColumns?: ProtectedColumnsConfig,

  // Built-in validation configs
  uniqueRow?: UniqueRowConfig,
  uniqueColumn?: UniqueColumnConfig,

  // Custom/third-party plugins
  plugins?: ValidatePlugin[],
});

Returns

| Function | Description | | ------------------ | ----------------------------------------------------------------------------------- | | insert | Insert with default values applied and validation plugins run | | patch | Patch with protected columns removed from type and validation plugins run | | dangerouslyPatch | Patch with full access to all columns (bypasses protected columns type restriction) | | configs | The original config object (for debugging) |


Transforms

Transforms modify the input type of insert().

defaultValuesConfig(schema, config)

Makes specified fields optional in insert() by providing default values.

import { defaultValuesConfig } from "convex-verify";
// or
import { defaultValuesConfig } from "convex-verify/transforms";

Static Config

const defaults = defaultValuesConfig(schema, {
	posts: { status: "draft", views: 0 },
	comments: { likes: 0 },
});

Dynamic Config (Fresh Values)

Use a function for values that should be generated fresh on each insert:

const defaults = defaultValuesConfig(schema, () => ({
	posts: {
		status: "draft",
		slug: generateRandomSlug(),
		createdAt: Date.now(),
	},
}));

Async Config

const defaults = defaultValuesConfig(schema, async () => ({
	posts: {
		category: await fetchDefaultCategory(),
	},
}));

Configs

Configs modify the input type of patch().

protectedColumnsConfig(schema, config)

Removes specified columns from the patch() input type, preventing accidental updates.

import { protectedColumnsConfig } from "convex-verify";
// or
import { protectedColumnsConfig } from "convex-verify/configs";

Example

const protected = protectedColumnsConfig(schema, {
	posts: ["authorId", "createdAt"],
	comments: ["postId", "authorId"],
});

Bypassing Protection

Use dangerouslyPatch() when you need to update protected columns:

// Regular patch - authorId not allowed
await patch(ctx, "posts", id, {
	authorId: newAuthorId, // ❌ TypeScript error
	title: "New Title", // ✅ OK
});

// Dangerous patch - full access
await dangerouslyPatch(ctx, "posts", id, {
	authorId: newAuthorId, // ✅ OK (bypasses protection)
	title: "New Title",
});

Note: dangerouslyPatch() still runs validation plugins - only the type restriction is bypassed.


Validation

Validation configs check data during insert() and patch() operations. They run after transforms and can throw errors to prevent the operation.

uniqueRowConfig(schema, config)

Enforces uniqueness across multiple columns using composite indexes.

import { uniqueRowConfig } from "convex-verify";
// or
import { uniqueRowConfig } from "convex-verify/plugins";

Usage

// As a named config key (recommended)
verifyConfig(schema, {
	uniqueRow: uniqueRowConfig(schema, {
		posts: ["by_author_slug"],
	}),
});

// Or in the plugins array
verifyConfig(schema, {
	plugins: [
		uniqueRowConfig(schema, {
			posts: ["by_author_slug"],
		}),
	],
});

Shorthand (Index Names)

const uniqueRows = uniqueRowConfig(schema, {
	posts: ["by_author_slug"], // Unique author + slug combo
	projects: ["by_org_slug"], // Unique org + slug combo
});

With Options

const uniqueRows = uniqueRowConfig(schema, {
	posts: [
		{
			index: "by_author_slug",
			identifiers: ["_id", "authorId"], // Fields to check for "same document"
		},
	],
});

uniqueColumnConfig(schema, config)

Enforces uniqueness on single columns using indexes.

import { uniqueColumnConfig } from "convex-verify";
// or
import { uniqueColumnConfig } from "convex-verify/plugins";

Usage

// As a named config key (recommended)
verifyConfig(schema, {
	uniqueColumn: uniqueColumnConfig(schema, {
		users: ["by_email", "by_username"],
	}),
});

// Or in the plugins array
verifyConfig(schema, {
	plugins: [
		uniqueColumnConfig(schema, {
			users: ["by_email", "by_username"],
		}),
	],
});

The column name is derived from the index name by removing by_ prefix:

  • by_username → checks username column
  • by_email → checks email column

Shorthand (Index Names)

const uniqueColumns = uniqueColumnConfig(schema, {
	users: ["by_username", "by_email"],
	organizations: ["by_slug"],
});

With Options

const uniqueColumns = uniqueColumnConfig(schema, {
	users: [
		"by_username", // shorthand
		{ index: "by_email", identifiers: ["_id", "clerkId"] }, // with options
	],
});

Custom Plugins

The plugins array accepts custom validation plugins for extensibility.

createValidatePlugin(name, config, handlers)

Create custom validation plugins.

import { createValidatePlugin } from "convex-verify";
// or
import { createValidatePlugin } from "convex-verify/core";

Example: Required Fields Plugin

const requiredFields = createValidatePlugin(
	"requiredFields",
	{ fields: ["title", "content"] },
	{
		insert: (context, data) => {
			for (const field of context.config.fields) {
				if (!data[field]) {
					throw new ConvexError({
						message: `Missing required field: ${field}`,
					});
				}
			}
			return data;
		},
	},
);

Example: Async Validation

const checkOwnership = createValidatePlugin(
	"checkOwnership",
	{},
	{
		patch: async (context, data) => {
			const existing = await context.ctx.db.get(context.patchId);
			const user = await getCurrentUser(context.ctx);

			if (existing?.authorId !== user._id) {
				throw new ConvexError({
					message: "Not authorized to edit this document",
				});
			}
			return data;
		},
	},
);

Plugin Context

Plugins receive a ValidateContext object:

type ValidateContext = {
	ctx: GenericMutationCtx; // Convex mutation context
	tableName: string; // Table being operated on
	operation: "insert" | "patch";
	patchId?: GenericId; // Document ID (patch only)
	onFail?: OnFailCallback; // Callback for failure details
	schema?: SchemaDefinition; // Schema reference
};

Subpath Imports

For smaller bundle sizes, you can import from specific subpaths:

// Import everything from root
import { uniqueRowConfig, verifyConfig } from "convex-verify";
import { protectedColumnsConfig } from "convex-verify/configs";
// Or import from specific subpaths
import { createValidatePlugin, verifyConfig } from "convex-verify/core";
import { uniqueColumnConfig, uniqueRowConfig } from "convex-verify/plugins";
import { defaultValuesConfig } from "convex-verify/transforms";
import { getTableIndexes } from "convex-verify/utils";

Error Handling

onFail Callback

All operations accept an optional onFail callback for handling validation failures:

await insert(ctx, "posts", data, {
	onFail: (args) => {
		if (args.uniqueRow) {
			console.log("Duplicate row:", args.uniqueRow.existingData);
		}
		if (args.uniqueColumn) {
			console.log("Duplicate column:", args.uniqueColumn.conflictingColumn);
		}
	},
});

Error Types

Validation plugins throw ConvexError with specific codes:

  • UNIQUE_ROW_VERIFICATION_ERROR - Duplicate row detected
  • UNIQUE_COLUMN_VERIFICATION_ERROR - Duplicate column value detected

TypeScript

This library is written in TypeScript and provides full type inference:

  • insert() types reflect optional fields from defaultValues
  • patch() types exclude protected columns
  • Plugin configs are type-checked against your schema
  • Index names are validated against your schema's indexes

License

MIT