Claude Agent Skill · by Waynesutton

Convex Schema Validator

Install Convex Schema Validator skill for Claude Code from waynesutton/convexskills.

convexschemavalidationtypescriptindexesmigrations
Install
Terminal · npx
$npx skills add https://github.com/waynesutton/convexskills --skill convex-schema-validator
Works with Paperclip

How Convex Schema Validator fits into a Paperclip company.

Convex Schema Validator drops into any Paperclip agent that handles convex and schema work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md400 lines
Expand
---name: convex-schema-validatordisplayName: Convex Schema Validatordescription: Defining and validating database schemas with proper typing, index configuration, optional fields, unions, and migration strategies for schema changesversion: 1.0.0author: Convextags: [convex, schema, validation, typescript, indexes, migrations]--- # 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 ```typescript// convex/schema.tsimport { 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 | Validator | TypeScript Type | Example ||-----------|----------------|---------|| `v.string()` | `string` | `"hello"` || `v.number()` | `number` | `42`, `3.14` || `v.boolean()` | `boolean` | `true`, `false` || `v.null()` | `null` | `null` || `v.int64()` | `bigint` | `9007199254740993n` || `v.bytes()` | `ArrayBuffer` | Binary data || `v.id("table")` | `Id<"table">` | Document reference || `v.array(v)` | `T[]` | `[1, 2, 3]` || `v.object({})` | `{ ... }` | `{ name: "..." }` || `v.optional(v)` | `T \| undefined` | Optional field || `v.union(...)` | `T1 \| T2` | Multiple types || `v.literal(x)` | `"x"` | Exact value || `v.any()` | `any` | Any value || `v.record(k, v)` | `Record<K, V>` | Dynamic keys | ### Index Configuration ```typescriptexport 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 ```typescriptexport 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 ```typescriptexport 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 ```typescriptexport 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: ```typescriptexport 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 ```typescript// Beforeusers: defineTable({  name: v.string(),  email: v.string(),}) // After - add as optional firstusers: defineTable({  name: v.string(),  email: v.string(),  avatarUrl: v.optional(v.string()), // New optional field})``` #### Backfilling Data ```typescript// convex/migrations.tsimport { 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 ```typescript// Step 1: Backfill all null values// Step 2: Update schema to requiredusers: defineTable({  name: v.string(),  email: v.string(),  avatarUrl: v.string(), // Now required after backfill})``` ## Examples ### Complete E-commerce Schema ```typescript// convex/schema.tsimport { 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 ```typescript// convex/products.tsimport { query, mutation } from "./_generated/server";import { v } from "convex/values";import { Doc, Id } from "./_generated/dataModel"; // Use Doc type for full documentstype Product = Doc<"products">; // Use Id type for referencestype 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 index2. **Wrong index field order** - Fields must be queried in order defined3. **Using v.any() excessively** - Lose type safety benefits4. **Not making new fields optional** - Breaks existing data5. **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