Convex Schema Validator

作者 waynesutton82d1ce23d863無授權條款405 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫10 天前更新

Designs convex/schema.ts tables, validators, indexes, and relationships, and keeps the schema honest as data evolves. Use when creating tables, adding fields, choosing index fields, modeling relationships, or when a validator error appears at deploy time.

AI 產生的概覽

指導撰寫 Convex 的 convex/schema.ts 資料表、驗證器、索引與關聯,使其通過部署時驗證。

功能
此技能提供撰寫 Convex schema 檔案的說明:資料表定義、欄位驗證器、索引命名與欄位順序、關聯建模、可辨識聯合,以及搜尋索引與向量索引。內容包含 schema 骨架、驗證器對照表,以及常見錯誤與對應修正。另提供一份檢查清單,用來確認每個 withIndex 呼叫都有相符的索引,且 schema 推送時不會出現驗證錯誤。
適用情境
適用於建立 Convex 資料表或為現有資料表新增欄位、查詢使用 .filter() 而需要改用索引、判斷應內嵌物件還是以 v.id 建立關聯,或 npx convex dev 因 schema 驗證錯誤而失敗時。
執行需求
不含指令碼,僅為說明文件。假定已有 Convex 專案與 convex/schema.ts,並依賴 convex/server 與 convex/values 套件,同時引用 Convex 官方文件網站。

Convex schema validator

Produces a convex/schema.ts that types every document, indexes every query path, and passes validation against the data already in the database. The one rule: every withIndex in a function needs a matching .index() here, named after its fields, queried in field order.

When to reach for this

  • Creating a new table or adding a field to an existing one
  • A query uses .filter() and needs an index instead
  • Deciding whether to embed an object or link with v.id
  • Modeling a document that comes in several shapes
  • npx convex dev fails with a schema validation error

Schema skeleton

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()),  }).index("by_email", ["email"]),
  tasks: defineTable({    userId: v.id("users"),    title: v.string(),    completed: v.boolean(),    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),  })    .index("by_userId", ["userId"])    .index("by_userId_and_completed", ["userId", "completed"]),});

defineTable takes either an object of field validators or a single v.union of v.object validators (see discriminated unions). Every table gets _id and _creationTime for free. Do not declare them.

Validators

ValidatorTypeScript typeNote
v.string()stringUTF-8, under 1 MB
v.number()numberFloat64. Use for timestamps and counts
v.boolean()boolean
v.null()nullundefined is not a Convex value. Return null instead
v.int64()bigintNot v.bigint(), which is deprecated
v.bytes()ArrayBufferUnder 1 MB
v.id("table")Id<"table">Typed reference. Convex does not enforce that the target exists
v.array(t)T[]At most 8192 items
v.object({...}){...}At most 1024 entries. Keys cannot start with $ or _
v.record(k, t)Record<K, T>Dynamic ASCII keys. No v.map or v.set
v.union(a, b)A | BUse v.literal members for enums
v.literal("x")"x"
v.optional(t)T | undefinedField may be absent
v.any()anyLast resort. Loses type safety and validation

Optional versus nullable

v.optional means the key may be missing from the document. v.union(t, v.null()) means the key is always present and may hold null. They are different at validation time.

typescript
items: defineTable({  description: v.optional(v.string()),           // may be absent  deletedAt: v.union(v.number(), v.null()),       // always present, may be null  notes: v.optional(v.union(v.string(), v.null())), // either}),

Use v.optional for fields added after the table had data. Use v.union(..., v.null()) when "explicitly cleared" carries meaning.

Discriminated unions

For a table whose documents come in several shapes, pass a v.union of v.object validators to defineTable. Each member has the same literal key so TypeScript narrows on it.

typescript
events: defineTable(  v.union(    v.object({      kind: v.literal("signup"),      userId: v.id("users"),      email: v.string(),    }),    v.object({      kind: v.literal("purchase"),      userId: v.id("users"),      orderId: v.id("orders"),      amount: v.number(),    }),  ),).index("by_kind", ["kind"]),

Put the discriminant (kind) first in any index on a union table so queries can scope to one shape. Prefer this over one wide object full of v.optional fields.

Indexes

Naming and field order

Name the index after its fields in order: by_field1_and_field2. Querying must follow the same order: equality on a prefix of the fields, then at most one range on the next field.

typescript
messages: defineTable({  channelId: v.id("channels"),  authorId: v.id("users"),  sentAt: v.number(),})  .index("by_channelId", ["channelId"])  .index("by_channelId_and_authorId", ["channelId", "authorId"])  .index("by_channelId_and_sentAt", ["channelId", "sentAt"]),
typescript
// Valid: equality on channelId, range on sentAtawait ctx.db  .query("messages")  .withIndex("by_channelId_and_sentAt", (q) =>    q.eq("channelId", args.channelId).gt("sentAt", args.since),  )  .order("desc")  .take(50);

You cannot skip channelId and filter on sentAt alone with that index. Add by_sentAt if that query exists. _creationTime is appended to every index automatically, so results within an equal prefix sort by creation time.

Reserved names: by_id and by_creation_time. Limits: 32 indexes per table, 16 fields per index.

When to add one

  • Any field a function passes to withIndex, .eq, or a range comparison
  • Every foreign key (userId, channelId, orgId) on the child table
  • The sort field for a paginated list, prefixed by the scoping field
  • Not for fields you only read after fetching the document
  • Not for tiny tables where a .collect() then in memory filter is fine

If a query uses .filter(), that is the signal to add an index and switch to withIndex.

Relationships

Link documents with v.id("table") on the child. Do not nest growing arrays of objects inside the parent.

typescript
// Good: one to many via a foreign keyposts: defineTable({  authorId: v.id("users"),  title: v.string(),}).index("by_authorId", ["authorId"]),
comments: defineTable({  postId: v.id("posts"),  authorId: v.id("users"),  body: v.string(),}).index("by_postId", ["postId"]),
// Many to many via a join tablepostTags: defineTable({  postId: v.id("posts"),  tagId: v.id("tags"),})  .index("by_postId", ["postId"])  .index("by_tagId", ["tagId"]),

Embed with v.object or a small v.array only when the data is bounded, always loaded with the parent, and updated together. A user's settings object is a good embed. A user's posts array is not: it hits the 8192 item cap and every post edit rewrites the user document.

System fields

_id: Id<"table"> and _creationTime: number (ms since epoch) exist on every document. Include them in return validators when a function returns whole documents:

typescript
returns: v.array(  v.object({    _id: v.id("tasks"),    _creationTime: v.number(),    userId: v.id("users"),    title: v.string(),    completed: v.boolean(),  }),),

Do not add your own createdAt unless you need a value that differs from insertion time.

Search and vector indexes

Declared on the table like regular indexes. filterFields must be top level fields.

typescript
articles: defineTable({  title: v.string(),  body: v.string(),  category: v.string(),  embedding: v.array(v.number()),})  .searchIndex("search_body", {    searchField: "body",    filterFields: ["category"],  })  .vectorIndex("by_embedding", {    vectorField: "embedding",    dimensions: 1536,    filterFields: ["category"],  }),

Query search indexes with withSearchIndex in queries. Vector search runs only in actions via ctx.vectorSearch.

Common mistakes

MistakeWhy it breaksDo instead
withIndex("by_userId") with no matching .index()Deploy failsDeclare the index in the schema first
Index named by_user on ["userId", "status"]Hides what it covers, easy to misuseby_userId_and_status
Querying status on by_userId_and_status without userIdIndex prefix ruleAdd by_status or include userId
Adding newField: v.string() to a table with rowsExisting documents fail validationv.optional, backfill, then require
posts: v.array(v.object(...)) on users8192 cap, write conflicts on every editSeparate posts table with by_authorId
v.bigint()Deprecatedv.int64()
Declaring _id or _creationTime in defineTableRejectedThey are automatic
v.any() to move fastNo validation, no typesModel the shape, or a v.union of the real cases
v.union(v.string(), v.null()) for a new fieldOld documents lack the key entirelyv.optional(v.string())
Storing a DateNot a Convex valuev.number() ms timestamp

Checklist

  • Schema lives in convex/schema.ts and exports defineSchema(...) as default
  • Every table has explicit field validators, no v.any() unless justified
  • Every withIndex call in convex/ has a matching .index() with fields in the name
  • Foreign keys are v.id("table") with an index on the child table
  • No unbounded arrays of objects embedded in a parent document
  • Fields added to tables with data are v.optional
  • Enums and polymorphic shapes use v.union of v.literal or v.object members
  • Return validators include _id and _creationTime when returning whole documents
  • npx convex dev pushes without a schema validation error

Docs

來源與署名

來源:waynesutton/builder-skills位於skills/convex-schema-validator提交82d1ce2

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架