Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
waynesutton avatar

Convex Schema Validator

  • 2.7k installs
  • 401 repo stars
  • Updated February 6, 2026
  • waynesutton/convexskills

convex-schema-validator is an agent skill for defining Convex database schemas with typed validators, indexes, optional fields, unions, and migration guidance.

About

convex-schema-validator is a Convex agent skill for authoring convex/schema.ts with defineSchema, defineTable, and convex/values validators that stay aligned with TypeScript document types. It documents validator mappings from v.string, v.number, v.id, v.optional, v.union, and v.literal through to generated types, plus index design for single-field, compound, and sort-friendly queries. Examples show users and tasks tables with references, priority unions, and channel message indexes such as by_channel and by_channel_and_time. The skill instructs agents to fetch current Convex documentation on schemas, indexes, and types instead of assuming stale API details. Migration guidance covers additive changes, backfills, and safe rollout patterns when evolving live tables. Use it when defining new tables, tightening validation, adding indexes for query paths, or planning schema changes that must not break existing Convex functions and clients.

  • Covers defineSchema and defineTable patterns with convex/values validators and TypeScript alignment.
  • Validator reference table maps v.string, v.optional, v.union, v.id, and related helpers to TS types.
  • Index examples include single-field, compound, and time-sorted channel message queries.
  • Points to official Convex schema, index, and types documentation before implementation.
  • Includes migration strategies for optional fields, unions, and evolving production tables.

Convex Schema Validator by the numbers

  • 2,674 all-time installs (skills.sh)
  • +36 installs in the week ending Aug 2, 2026 (Skillselion tracking)
  • Ranked #39 of 911 Databases skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Aug 3, 2026 (Skillselion catalog sync)
At a glance

convex-schema-validator capabilities & compatibility

Capabilities
defineschema and definetable authoring patterns · validator to typescript type mapping reference · single and compound index configuration · optional fields and union literal modeling · schema migration strategy guidance · official convex documentation fetch reminders
Works with
supabase
Use cases
database · api development · refactoring
From the docs

What convex-schema-validator says it does

Before implementing, do not assume; fetch the latest documentation
SKILL.md
.index("by_channel_and_time", ["channelId", "sentAt"])
SKILL.md
npx skills add https://github.com/waynesutton/convexskills --skill convex-schema-validator

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2.7k
repo stars401
Security audit3 / 3 scanners passed
Last updatedFebruary 6, 2026
Repositorywaynesutton/convexskills

How do I model Convex tables with correct validators and indexes so queries stay typed and migrations do not break production data?

Define and validate Convex database schemas with typed validators, indexes, optional fields, unions, and migration strategies.

Who is it for?

Teams on Convex defining or refactoring schema.ts, indexes, and document validation rules.

Skip if: Non-Convex ORMs, raw SQL migrations, or frontend-only work without database schema changes.

When should I use this skill?

User defines Convex tables, adds indexes, uses v.optional or v.union validators, or plans schema migrations.

What you get

A validated convex/schema.ts design with indexes matched to query paths and a safe plan for schema evolution.

  • schema validation report
  • corrected schema definitions

Files

SKILL.mdMarkdownGitHub ↗

Convex Schema Validator

Define and validate database schemas in Convex with proper typing, index configuration, optional fields, unions, and strategies for schema migrations.

Documentation Sources

Before implementing, do not assume; fetch the latest documentation:

  • Primary: https://docs.convex.dev/database/schemas
  • Indexes: https://docs.convex.dev/database/indexes
  • Data Types: https://docs.convex.dev/database/types
  • For broader context: https://docs.convex.dev/llms.txt

Instructions

Basic Schema Definition

// 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()),
    createdAt: v.number(),
  }),
  
  tasks: defineTable({
    title: v.string(),
    description: v.optional(v.string()),
    completed: v.boolean(),
    userId: v.id("users"),
    priority: v.union(
      v.literal("low"),
      v.literal("medium"),
      v.literal("high")
    ),
  }),
});

Validator Types

ValidatorTypeScript TypeExample
v.string()string"hello"
v.number()number42, 3.14
v.boolean()booleantrue, false
v.null()nullnull
v.int64()bigint9007199254740993n
v.bytes()ArrayBufferBinary data
v.id("table")Id<"table">Document reference
v.array(v)T[][1, 2, 3]
v.object({}){ ... }{ name: "..." }
v.optional(v)`T \undefined`
v.union(...)`T1 \T2`
v.literal(x)"x"Exact value
v.any()anyAny value
v.record(k, v)Record<K, V>Dynamic keys

Index Configuration

export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
    sentAt: v.number(),
  })
    // Single field index
    .index("by_channel", ["channelId"])
    // Compound index
    .index("by_channel_and_author", ["channelId", "authorId"])
    // Index for sorting
    .index("by_channel_and_time", ["channelId", "sentAt"]),
    
  // Full-text search index
  articles: defineTable({
    title: v.string(),
    body: v.string(),
    category: v.string(),
  })
    .searchIndex("search_content", {
      searchField: "body",
      filterFields: ["category"],
    }),
});

Complex Types

export default defineSchema({
  // Nested objects
  profiles: defineTable({
    userId: v.id("users"),
    settings: v.object({
      theme: v.union(v.literal("light"), v.literal("dark")),
      notifications: v.object({
        email: v.boolean(),
        push: v.boolean(),
      }),
    }),
  }),

  // Arrays of objects
  orders: defineTable({
    customerId: v.id("users"),
    items: v.array(v.object({
      productId: v.id("products"),
      quantity: v.number(),
      price: v.number(),
    })),
    status: v.union(
      v.literal("pending"),
      v.literal("processing"),
      v.literal("shipped"),
      v.literal("delivered")
    ),
  }),

  // Record type for dynamic keys
  analytics: defineTable({
    date: v.string(),
    metrics: v.record(v.string(), v.number()),
  }),
});

Discriminated Unions

export default defineSchema({
  events: defineTable(
    v.union(
      v.object({
        type: v.literal("user_signup"),
        userId: v.id("users"),
        email: v.string(),
      }),
      v.object({
        type: v.literal("purchase"),
        userId: v.id("users"),
        orderId: v.id("orders"),
        amount: v.number(),
      }),
      v.object({
        type: v.literal("page_view"),
        sessionId: v.string(),
        path: v.string(),
      })
    )
  ).index("by_type", ["type"]),
});

Optional vs Nullable Fields

export default defineSchema({
  items: defineTable({
    // Optional: field may not exist
    description: v.optional(v.string()),
    
    // Nullable: field exists but can be null
    deletedAt: v.union(v.number(), v.null()),
    
    // Optional and nullable
    notes: v.optional(v.union(v.string(), v.null())),
  }),
});

Index Naming Convention

Always include all indexed fields in the index name:

export default defineSchema({
  posts: defineTable({
    authorId: v.id("users"),
    categoryId: v.id("categories"),
    publishedAt: v.number(),
    status: v.string(),
  })
    // Good: descriptive names
    .index("by_author", ["authorId"])
    .index("by_author_and_category", ["authorId", "categoryId"])
    .index("by_category_and_status", ["categoryId", "status"])
    .index("by_status_and_published", ["status", "publishedAt"]),
});

Schema Migration Strategies

Adding New Fields
// Before
users: defineTable({
  name: v.string(),
  email: v.string(),
})

// After - add as optional first
users: defineTable({
  name: v.string(),
  email: v.string(),
  avatarUrl: v.optional(v.string()), // New optional field
})
Backfilling Data
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";

export const backfillAvatars = internalMutation({
  args: {},
  returns: v.number(),
  handler: async (ctx) => {
    const users = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("avatarUrl"), undefined))
      .take(100);

    for (const user of users) {
      await ctx.db.patch(user._id, {
        avatarUrl: `https://api.dicebear.com/7.x/initials/svg?seed=${user.name}`,
      });
    }

    return users.length;
  },
});
Making Optional Fields Required
// Step 1: Backfill all null values
// Step 2: Update schema to required
users: defineTable({
  name: v.string(),
  email: v.string(),
  avatarUrl: v.string(), // Now required after backfill
})

Examples

Complete E-commerce Schema

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    email: v.string(),
    name: v.string(),
    role: v.union(v.literal("customer"), v.literal("admin")),
    createdAt: v.number(),
  })
    .index("by_email", ["email"])
    .index("by_role", ["role"]),

  products: defineTable({
    name: v.string(),
    description: v.string(),
    price: v.number(),
    category: v.string(),
    inventory: v.number(),
    isActive: v.boolean(),
  })
    .index("by_category", ["category"])
    .index("by_active_and_category", ["isActive", "category"])
    .searchIndex("search_products", {
      searchField: "name",
      filterFields: ["category", "isActive"],
    }),

  orders: defineTable({
    userId: v.id("users"),
    items: v.array(v.object({
      productId: v.id("products"),
      quantity: v.number(),
      priceAtPurchase: v.number(),
    })),
    total: v.number(),
    status: v.union(
      v.literal("pending"),
      v.literal("paid"),
      v.literal("shipped"),
      v.literal("delivered"),
      v.literal("cancelled")
    ),
    shippingAddress: v.object({
      street: v.string(),
      city: v.string(),
      state: v.string(),
      zip: v.string(),
      country: v.string(),
    }),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index("by_user", ["userId"])
    .index("by_user_and_status", ["userId", "status"])
    .index("by_status", ["status"]),

  reviews: defineTable({
    productId: v.id("products"),
    userId: v.id("users"),
    rating: v.number(),
    comment: v.optional(v.string()),
    createdAt: v.number(),
  })
    .index("by_product", ["productId"])
    .index("by_user", ["userId"]),
});

Using Schema Types in Functions

// convex/products.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { Doc, Id } from "./_generated/dataModel";

// Use Doc type for full documents
type Product = Doc<"products">;

// Use Id type for references
type ProductId = Id<"products">;

export const get = query({
  args: { productId: v.id("products") },
  returns: v.union(
    v.object({
      _id: v.id("products"),
      _creationTime: v.number(),
      name: v.string(),
      description: v.string(),
      price: v.number(),
      category: v.string(),
      inventory: v.number(),
      isActive: v.boolean(),
    }),
    v.null()
  ),
  handler: async (ctx, args): Promise<Product | null> => {
    return await ctx.db.get(args.productId);
  },
});

Best Practices

  • Never run npx convex deploy unless explicitly instructed
  • Never run any git commands unless explicitly instructed
  • Always define explicit schemas rather than relying on inference
  • Use descriptive index names that include all indexed fields
  • Start with optional fields when adding new columns
  • Use discriminated unions for polymorphic data
  • Validate data at the schema level, not just in functions
  • Plan index strategy based on query patterns

Common Pitfalls

1. Missing indexes for queries - Every withIndex needs a corresponding schema index 2. Wrong index field order - Fields must be queried in order defined 3. Using v.any() excessively - Lose type safety benefits 4. Not making new fields optional - Breaks existing data 5. Forgetting system fields - _id and _creationTime are automatic

References

  • Convex Documentation: https://docs.convex.dev/
  • Convex LLMs.txt: https://docs.convex.dev/llms.txt
  • Schemas: https://docs.convex.dev/database/schemas
  • Indexes: https://docs.convex.dev/database/indexes
  • Data Types: https://docs.convex.dev/database/types

Related skills

How it compares

Pick convex-schema-validator for Convex schema pre-deploy checks; pick general TypeScript linters when validation does not need Convex-specific index and union rules.

FAQ

Where should I read current Convex schema rules?

Fetch docs.convex.dev/database/schemas, indexes, and types rather than assuming outdated syntax.

How do optional fields work in Convex?

Wrap validators with v.optional so documents may omit the field while TypeScript types include undefined.

When do I need compound indexes?

When queries filter or sort on multiple fields together, such as channelId plus sentAt for message timelines.

Is Convex Schema Validator safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Databasesdatabases

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.