🎯

convex-schema-validators

🎯Skill

from fluid-tools/claude-skills

VibeIndex|
What it does

Validates and generates TypeScript-safe Convex database schemas with robust type checking and validator patterns.

convex-schema-validators

Installation

Install skill:
npx skills add https://github.com/fluid-tools/claude-skills --skill convex-schema-validators
14
Last UpdatedJan 15, 2026

Skill Details

SKILL.md

"Guide for Convex schema design, validators, and TypeScript types. Use when defining database schemas, creating validators for function arguments/returns, working with document types, or ensuring type safety. Activates for schema.ts creation, validator usage, Id/Doc type handling, or TypeScript integration tasks."

Overview

# Convex Schema & Validators Guide

Overview

Convex uses a schema-first approach with built-in validators for type safety. This skill covers schema design, validator patterns, TypeScript type integration, and best practices for type-safe Convex development.

TypeScript: NEVER Use `any` Type

CRITICAL RULE: This codebase has @typescript-eslint/no-explicit-any enabled. Using any will cause build failures.

❌ WRONG:

```typescript

const data: any = await ctx.db.get(id);

function process(items: any[]) { ... }

```

βœ… CORRECT:

```typescript

const data: Doc<"users"> | null = await ctx.db.get(id);

function process(items: Doc<"items">[]) { ... }

```

When to Use This Skill

Use this skill when:

  • Creating or modifying convex/schema.ts
  • Defining validators for function arguments and returns
  • Working with document IDs and types
  • Setting up indexes for efficient queries
  • Handling optional fields and unions
  • Integrating Convex types with TypeScript

Schema Definition

Basic Schema Structure

```typescript

// convex/schema.ts

import { defineSchema, defineTable } from "convex/server";

import { v } from "convex/values";

export default defineSchema({

users: defineTable({

name: v.string(),

email: v.string(),

avatarUrl: v.optional(v.string()),

role: v.union(v.literal("admin"), v.literal("user")),

createdAt: v.number(),

})

.index("by_email", ["email"])

.index("by_role", ["role"]),

messages: defineTable({

authorId: v.id("users"),

channelId: v.id("channels"),

content: v.string(),

isDeleted: v.boolean(),

})

.index("by_channel", ["channelId"])

.index("by_author", ["authorId"])

.index("by_channel_author", ["channelId", "authorId"]),

channels: defineTable({

name: v.string(),

members: v.array(v.id("users")),

isPrivate: v.boolean(),

}),

});

```

Table Definition Patterns

```typescript

// defineTable takes a validator object

defineTable({

field1: v.string(),

field2: v.number(),

});

// Chain indexes after defineTable

defineTable({

userId: v.id("users"),

status: v.string(),

})

.index("by_user", ["userId"])

.index("by_status", ["status"])

.index("by_user_status", ["userId", "status"]);

// Search indexes for full-text search

defineTable({

title: v.string(),

body: v.string(),

}).searchIndex("search_body", {

searchField: "body",

filterFields: ["title"],

});

```

Validator Reference

Primitive Validators

```typescript

import { v } from "convex/values";

v.string(); // string

v.number(); // number (float64)

v.boolean(); // boolean

v.null(); // null literal

v.int64(); // 64-bit integer (NOT v.bigint() - deprecated!)

v.bytes(); // ArrayBuffer

```

Complex Validators

```typescript

// Document IDs

v.id("tableName"); // Id<"tableName">

// Arrays

v.array(v.string()); // string[]

v.array(v.id("users")); // Id<"users">[]

v.array(v.object({ x: v.number() })); // { x: number }[]

// Objects

v.object({

name: v.string(),

age: v.number(),

email: v.optional(v.string()),

});

// Records (string keys, typed values)

v.record(v.string(), v.number()); // Record

v.record(v.id("users"), v.string()); // Record, string>

// Unions (OR types)

v.union(v.string(), v.null()); // string | null

v.union(v.literal("a"), v.literal("b")); // "a" | "b"

// Optionals (field may be missing)

v.optional(v.string()); // string | undefined

// Literals (exact values)

v.literal("active"); // "active" literal type

v.literal(42); // 42 literal type

v.literal(true); // true literal type

// Any (escape hatch - avoid if possible)

v.any(); // any (use sparingly!)

```

Common Validator Patterns

```typescript

// Nullable field (can be null)

status: v.union(v.string(), v.null());

// Optional field (may not exist)

nickname: v.optional(v.string());

// Optional AND nullable

deletedAt: v.optional(v.union(v.number(), v.null()));

// Enum-like unions

role: v.union(v.literal("admin"), v.literal("moderator"), v.literal("user"));

// Nested objects

settings: v.object({

theme: v.union(v.literal("light"), v.literal("dark")),

notifications: v.object({

email: v.boolean(),

push: v.boolean(),

}),

});

// Array of objects

members: v.array(

v.object({

userId: v.id("users"),

role: v.string(),

joinedAt: v.number(),

})

);

```

Function Validators

CRITICAL: Every Function MUST Have `returns` Validator

```typescript

// ❌ WRONG: Missing returns

export const foo = mutation({

args: {},

handler: async (ctx) => {

// implicitly returns undefined

},

});

// βœ… CORRECT: Explicit v.null()

export const foo = mutation({

args: {},

returns: v.null(),

handler: async (ctx) => {

return null;

},

});

```

Query with Validators

```typescript

import { query } from "./_generated/server";

import { v } from "convex/values";

export const getUser = query({

args: {

userId: v.id("users"),

},

returns: v.union(

v.object({

_id: v.id("users"),

_creationTime: v.number(),

name: v.string(),

email: v.string(),

role: v.string(),

}),

v.null()

),

handler: async (ctx, args) => {

return await ctx.db.get(args.userId);

},

});

```

Mutation with Validators

```typescript

import { mutation } from "./_generated/server";

import { v } from "convex/values";

export const createUser = mutation({

args: {

name: v.string(),

email: v.string(),

role: v.optional(v.union(v.literal("admin"), v.literal("user"))),

},

returns: v.id("users"),

handler: async (ctx, args) => {

return await ctx.db.insert("users", {

name: args.name,

email: args.email,

role: args.role ?? "user",

createdAt: Date.now(),

});

},

});

```

Action with Validators

```typescript

import { action } from "./_generated/server";

import { v } from "convex/values";

export const processImage = action({

args: {

imageUrl: v.string(),

options: v.object({

width: v.number(),

height: v.number(),

format: v.union(v.literal("png"), v.literal("jpeg")),

}),

},

returns: v.object({

processedUrl: v.string(),

size: v.number(),

}),

handler: async (ctx, args) => {

// Process image...

return {

processedUrl: "https://...",

size: 1024,

};

},

});

```

TypeScript Types

Importing Types

```typescript

import { Doc, Id } from "./_generated/dataModel";

// Document type for a table

type User = Doc<"users">;

// {

// _id: Id<"users">;

// _creationTime: number;

// name: string;

// email: string;

// ...

// }

// ID type for a table

type UserId = Id<"users">;

```

Using Types in Code

```typescript

import { Doc, Id } from "./_generated/dataModel";

// Function parameter types

async function getUserName(

ctx: QueryCtx,

userId: Id<"users">

): Promise {

const user = await ctx.db.get(userId);

return user?.name ?? null;

}

// Variable types

const users: Doc<"users">[] = await ctx.db.query("users").collect();

// Record with Id keys

const userMap: Record, string> = {};

for (const user of users) {

userMap[user._id] = user.name;

}

```

Context Types

```typescript

import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";

// Query context - read-only

async function readUser(ctx: QueryCtx, id: Id<"users">) {

return await ctx.db.get(id);

}

// Mutation context - read and write

async function createUser(ctx: MutationCtx, name: string) {

return await ctx.db.insert("users", { name, createdAt: Date.now() });

}

// Action context - no db, uses runQuery/runMutation

async function processUser(ctx: ActionCtx, id: Id<"users">) {

const user = await ctx.runQuery(internal.users.getById, { id });

// ...

}

```

Index Design

Index Naming Convention

Include all fields in the index name: by_field1_and_field2_and_field3

```typescript

// Schema

export default defineSchema({

messages: defineTable({

channelId: v.id("channels"),

authorId: v.id("users"),

content: v.string(),

isDeleted: v.boolean(),

})

// βœ… This single index serves THREE query patterns:

// 1. All messages in channel: .eq("channelId", id)

// 2. Messages by author in channel: .eq("channelId", id).eq("authorId", id)

// 3. Non-deleted messages by author: .eq("channelId", id).eq("authorId", id).eq("isDeleted", false)

.index("by_channel_author_deleted", ["channelId", "authorId", "isDeleted"]),

});

// ❌ REDUNDANT: Don't create by_channel if you have by_channel_author_deleted

// The compound index can serve channel-only queries by partial prefix match

```

Index Usage

```typescript

// Using indexes in queries

const messages = await ctx.db

.query("messages")

.withIndex("by_channel_author_deleted", (q) =>

q.eq("channelId", channelId).eq("authorId", authorId).eq("isDeleted", false)

)

.collect();

// Partial prefix match (uses first field only)

const allChannelMessages = await ctx.db

.query("messages")

.withIndex("by_channel_author_deleted", (q) => q.eq("channelId", channelId))

.collect();

```

Validator Extraction from Schema

Reusing Schema Validators

```typescript

// convex/schema.ts

import { defineSchema, defineTable } from "convex/server";

import { v } from "convex/values";

// Define shared validators

export const userValidator = v.object({

name: v.string(),

email: v.string(),

role: v.union(v.literal("admin"), v.literal("user")),

});

export default defineSchema({

users: defineTable(userValidator),

});

```

```typescript

// convex/users.ts

import { mutation, query } from "./_generated/server";

import { v } from "convex/values";

import schema from "./schema";

// Extract validator from schema and extend with system fields

const userDoc = schema.tables.users.validator.extend({

_id: v.id("users"),

_creationTime: v.number(),

});

export const getUser = query({

args: { userId: v.id("users") },

returns: v.union(userDoc, v.null()),

handler: async (ctx, args) => {

return await ctx.db.get(args.userId);

},

});

```

Common Patterns

Pattern 1: Status Enum

```typescript

// Schema

const statusValidator = v.union(

v.literal("pending"),

v.literal("processing"),

v.literal("completed"),

v.literal("failed")

);

export default defineSchema({

jobs: defineTable({

status: statusValidator,

data: v.string(),

}).index("by_status", ["status"]),

});

// Usage in functions

export const getJobsByStatus = query({

args: {

status: v.union(

v.literal("pending"),

v.literal("processing"),

v.literal("completed"),

v.literal("failed")

),

},

returns: v.array(

v.object({

_id: v.id("jobs"),

_creationTime: v.number(),

status: v.string(),

data: v.string(),

})

),

handler: async (ctx, args) => {

return await ctx.db

.query("jobs")

.withIndex("by_status", (q) => q.eq("status", args.status))

.collect();

},

});

```

Pattern 2: Polymorphic Documents

```typescript

// Schema with discriminated union pattern

export default defineSchema({

notifications: defineTable({

userId: v.id("users"),

type: v.union(

v.literal("message"),

v.literal("mention"),

v.literal("system")

),

// Common fields

read: v.boolean(),

createdAt: v.number(),

// Type-specific data stored as object

data: v.union(

v.object({ type: v.literal("message"), messageId: v.id("messages") }),

v.object({

type: v.literal("mention"),

messageId: v.id("messages"),

mentionedBy: v.id("users"),

}),

v.object({

type: v.literal("system"),

title: v.string(),

body: v.string(),

})

),

}).index("by_user", ["userId"]),

});

```

Pattern 3: Timestamps Pattern

```typescript

// Helper for timestamp fields

const timestampsValidator = {

createdAt: v.number(),

updatedAt: v.number(),

};

export default defineSchema({

posts: defineTable({

title: v.string(),

body: v.string(),

authorId: v.id("users"),

...timestampsValidator,

}),

});

```

Pattern 4: Soft Deletes

```typescript

export default defineSchema({

items: defineTable({

content: v.string(),

deletedAt: v.optional(v.number()),

}).index("by_active", ["deletedAt"]),

});

// Query active items only

const activeItems = await ctx.db

.query("items")

.withIndex("by_active", (q) => q.eq("deletedAt", undefined))

.collect();

```

Common Pitfalls

Pitfall 1: Using v.bigint() (Deprecated)

❌ WRONG:

```typescript

export default defineSchema({

counters: defineTable({

value: v.bigint(), // ❌ Deprecated!

}),

});

```

βœ… CORRECT:

```typescript

export default defineSchema({

counters: defineTable({

value: v.int64(), // βœ… Use v.int64()

}),

});

```

Pitfall 2: Missing System Fields in Return Validators

❌ WRONG:

```typescript

export const getUser = query({

args: { userId: v.id("users") },

returns: v.object({

// ❌ Missing _id and _creationTime!

name: v.string(),

email: v.string(),

}),

handler: async (ctx, args) => {

return await ctx.db.get(args.userId);

},

});

```

βœ… CORRECT:

```typescript

export const getUser = query({

args: { userId: v.id("users") },

returns: v.union(

v.object({

_id: v.id("users"), // βœ… Include system fields

_creationTime: v.number(),

name: v.string(),

email: v.string(),

}),

v.null()

),

handler: async (ctx, args) => {

return await ctx.db.get(args.userId);

},

});

```

Pitfall 3: Using string Instead of v.id()

❌ WRONG:

```typescript

export const getMessage = query({

args: { messageId: v.string() }, // ❌ Should be v.id()

returns: v.null(),

handler: async (ctx, args) => {

// Type error: can't use string as Id

return await ctx.db.get(args.messageId);

},

});

```

βœ… CORRECT:

```typescript

export const getMessage = query({

args: { messageId: v.id("messages") }, // βœ… Proper ID type

returns: v.union(

v.object({

_id: v.id("messages"),

_creationTime: v.number(),

content: v.string(),

}),

v.null()

),

handler: async (ctx, args) => {

return await ctx.db.get(args.messageId);

},

});

```

Pitfall 4: Redundant Indexes

❌ WRONG:

```typescript

export default defineSchema({

messages: defineTable({

channelId: v.id("channels"),

authorId: v.id("users"),

})

.index("by_channel", ["channelId"]) // ❌ Redundant!

.index("by_channel_author", ["channelId", "authorId"]),

});

```

βœ… CORRECT:

```typescript

export default defineSchema({

messages: defineTable({

channelId: v.id("channels"),

authorId: v.id("users"),

})

// βœ… Single compound index serves both queries

.index("by_channel_author", ["channelId", "authorId"]),

});

// Use .eq("channelId", id) for channel-only queries (prefix match)

// Use .eq("channelId", id).eq("authorId", authorId) for both

```

Quick Reference

Validator Cheat Sheet

| Type | Validator | TypeScript |

| ----------- | ---------------------------------- | ------------------------ |

| String | v.string() | string |

| Number | v.number() | number |

| Boolean | v.boolean() | boolean |

| Null | v.null() | null |

| 64-bit Int | v.int64() | bigint |

| Bytes | v.bytes() | ArrayBuffer |

| Document ID | v.id("table") | Id<"table"> |

| Array | v.array(v.string()) | string[] |

| Object | v.object({ x: v.number() }) | { x: number } |

| Record | v.record(v.string(), v.number()) | Record |

| Union | v.union(v.string(), v.null()) | string \| null |

| Optional | v.optional(v.string()) | string \| undefined |

| Literal | v.literal("active") | "active" |

Type Import Cheat Sheet

```typescript

// Document and ID types

import { Doc, Id } from "./_generated/dataModel";

// Context types

import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";

// Function builders

import { query, mutation, action } from "./_generated/server";

import {

internalQuery,

internalMutation,

internalAction,

} from "./_generated/server";

// Validators

import { v } from "convex/values";

// Schema builders

import { defineSchema, defineTable } from "convex/server";

```

More from this repository8

🎯
vercel-ai-sdk🎯Skill

Generates AI chat interfaces, streaming responses, and agentic applications using Vercel AI SDK v6 with advanced tool calling, structured output, and model integration patterns.

🎯
convex-performance-patterns🎯Skill

Optimizes Convex performance by guiding denormalization, index design, and concurrency control strategies for efficient data modeling and querying.

🎯
convex-helpers-patterns🎯Skill

Streamlines Convex development by providing battle-tested patterns for triggers, security, relationships, custom functions, rate limiting, and concurrency management.

🎯
typescript-strict-mode🎯Skill

Enforces strict TypeScript type safety by guiding developers to avoid `any`, use precise type annotations, and leverage TypeScript's robust type system effectively.

🎯
convex-fundamentals🎯Skill

Streamlines Convex database operations with best practices, query optimization, and schema design guidance for efficient full-stack JavaScript applications.

🎯
convex-components🎯Skill

Enables authoring isolated, reusable Convex backend components with custom schemas and functions for modular library development and NPM packaging.

🎯
convex-anti-patterns🎯Skill

Identifies and prevents critical Convex development anti-patterns, helping developers avoid common mistakes that cause build failures, errors, and non-deterministic code.

🎯
convex-actions-scheduling🎯Skill

Schedules and orchestrates background jobs, cron tasks, and complex workflows in Convex using actions, external API calls, and multi-step processing.