schema-builder

Convex Schema Builder

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "schema-builder" with this command: npx skills add get-convex/agent-skills/get-convex-agent-skills-schema-builder

Convex Schema Builder

Build well-structured Convex schemas following best practices for relationships, indexes, and validators.

When to Use

  • Creating a new convex/schema.ts file

  • Adding tables to existing schema

  • Designing data model relationships

  • Adding or optimizing indexes

  • Converting nested data to relational structure

Schema Design Principles

  • Document-Relational: Use flat documents with ID references, not deep nesting

  • Index Foreign Keys: Always index fields used in lookups (userId, teamId, etc.)

  • Limit Arrays: Only use arrays for small, bounded collections (<8192 items)

  • Type Safety: Use strict validators with v.* types

Schema Template

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

export default defineSchema({ tableName: defineTable({ // Required fields field: v.string(),

// Optional fields
optional: v.optional(v.number()),

// Relations (use IDs)
userId: v.id("users"),

// Enums with union + literal
status: v.union(
  v.literal("active"),
  v.literal("pending"),
  v.literal("archived")
),

// Timestamps
createdAt: v.number(),
updatedAt: v.optional(v.number()),

}) // Index for queries by this field .index("by_user", ["userId"]) // Compound index for common query patterns .index("by_user_and_status", ["userId", "status"]) // Index for time-based queries .index("by_created", ["createdAt"]), });

Common Patterns

One-to-Many Relationship

export default defineSchema({ users: defineTable({ name: v.string(), email: v.string(), }).index("by_email", ["email"]),

posts: defineTable({ userId: v.id("users"), title: v.string(), content: v.string(), }).index("by_user", ["userId"]), });

Many-to-Many with Junction Table

export default defineSchema({ users: defineTable({ name: v.string(), }),

projects: defineTable({ name: v.string(), }),

projectMembers: defineTable({ userId: v.id("users"), projectId: v.id("projects"), role: v.union(v.literal("owner"), v.literal("member")), }) .index("by_user", ["userId"]) .index("by_project", ["projectId"]) .index("by_project_and_user", ["projectId", "userId"]), });

Hierarchical Data

export default defineSchema({ comments: defineTable({ postId: v.id("posts"), parentId: v.optional(v.id("comments")), // null for top-level userId: v.id("users"), text: v.string(), }) .index("by_post", ["postId"]) .index("by_parent", ["parentId"]), });

Small Bounded Arrays (OK to use)

export default defineSchema({ users: defineTable({ name: v.string(), // Small, bounded collections are fine roles: v.array(v.union( v.literal("admin"), v.literal("editor"), v.literal("viewer") )), tags: v.array(v.string()), // e.g., max 10 tags }), });

Validator Reference

// Primitives v.string() v.number() v.boolean() v.null() v.id("tableName")

// Optional v.optional(v.string())

// Union types (enums) v.union(v.literal("a"), v.literal("b"))

// Objects v.object({ key: v.string(), nested: v.number(), })

// Arrays v.array(v.string())

// Records (arbitrary keys) v.record(v.string(), v.boolean())

// Any (avoid if possible) v.any()

Index Strategy

Single-field indexes: For simple lookups

  • by_user: ["userId"]

  • by_email: ["email"]

Compound indexes: For filtered queries

  • by_user_and_status: ["userId", "status"]

  • by_team_and_created: ["teamId", "createdAt"]

Remove redundant: by_a_and_b usually covers by_a

Checklist

  • All foreign keys have indexes

  • Common query patterns have compound indexes

  • Arrays are small and bounded (or converted to relations)

  • All fields have proper validators

  • Enums use v.union(v.literal(...)) pattern

  • Timestamps use v.number() (milliseconds since epoch)

Migration from Nested to Relational

If converting from nested structures:

Before:

users: defineTable({ posts: v.array(v.object({ title: v.string(), comments: v.array(v.object({ text: v.string() })), })), })

After:

users: defineTable({ name: v.string(), }),

posts: defineTable({ userId: v.id("users"), title: v.string(), }).index("by_user", ["userId"]),

comments: defineTable({ postId: v.id("posts"), text: v.string(), }).index("by_post", ["postId"]),

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

convex-helpers-guide

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

function-creator

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

components-guide

No summary provided by upstream source.

Repository SourceNeeds Review