convex-schema-validator — скилл для Claude Code: схема Convex
Проектирует таблицы, валидаторы, индексы и связи в convex/schema.ts. Пригодится при ошибках проверки схемы и развитии модели данных.
npx skills add waynesutton/builder-skills --skill convex-schema-validator -a claude-codeЧто делает convex-schema-validator
Скилл готовит convex/schema.ts для таблиц Convex, типов полей, связей и индексов. Он помогает добавить поле к существующим данным, описать документы разных форм через v.union и разобраться с ошибкой при запуске npx convex dev.
Для каждого withIndex нужен соответствующий .index() с именем по полям и тем же порядком полей. Равенства должны идти по префиксу индекса, а сравнение диапазона допускается только для следующего поля. Если запрос использует .filter(), это повод проверить, не нужен ли индекс и withIndex.
Скилл различает отсутствующее поле через v.optional и обязательное поле со значением null. Связи строятся через v.id("table"); ссылка не проверяет существование целевого документа, поэтому это стоит учитывать в логике записи данных.
Какие задачи решает
- описать таблицу в
convex/schema.ts - добавить поле к таблице с существующими документами
- сопоставить
withIndexс объявлением.index() - заменить
.filter()на индексный запрос - смоделировать связь через
v.id("table")
Примеры запросов агенту
- Проверь
convex/schema.tsи добавь индексы для всех вызововwithIndex. - Добавь поле в существующую таблицу Convex так, чтобы проверка старых данных прошла.
- Переделай запрос с
.filter()наwithIndexи объяви нужный индекс.
Как установить convex-schema-validator
1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-schema-validator -a claude-code2Вручную: положить файлы
~/.claude/skills/convex-schema-validator/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/convex-schema-validator/
1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-schema-validator -a codex2Вручную: положить файлы
.agents/skills/convex-schema-validator/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-schema-validator -a cursor2Вручную: положить файлы
.cursor/skills/convex-schema-validator/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install waynesutton/builder-skills convex-schema-validator2Вручную: положить файлы
.github/skills/convex-schema-validator/SKILL.md1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-schema-validator -a gemini-cli2Командой платформы
gemini skills install https://github.com/waynesutton/builder-skills --path skills/convex-schema-validatorВручную: положить файлы
.devin/skills/convex-schema-validator/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-schema-validator -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о convex-schema-validator
Как установить convex-schema-validator в Claude Code?
Выполните npx skills add waynesutton/builder-skills --skill convex-schema-validator -a claude-code, затем повторите npx skills add waynesutton/builder-skills --skill convex-schema-validator -a claude-code. Файл скилла расположен в ~/.claude/skills/convex-schema-validator/SKILL.md.
Что делает convex-schema-validator?
Скилл помогает проектировать таблицы Convex, валидаторы, индексы и связи в convex/schema.ts. Он также разбирает ошибки проверки схемы при развёртывании.
Чем v.optional отличается от поля с null?
v.optional(t) разрешает не записывать ключ в документ. v.union(t, v.null()) требует ключ, но позволяет сохранить в нём null.
Исходный текст
SKILL.md · Apache-2.0
---
name: convex-schema-validator
description: 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.
---
# 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.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()),
}).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
| Validator | TypeScript type | Note |
| --- | --- | --- |
| `v.string()` | `string` | UTF-8, under 1 MB |
| `v.number()` | `number` | Float64. Use for timestamps and counts |
| `v.boolean()` | `boolean` | |
| `v.null()` | `null` | `undefined` is not a Convex value. Return `null` instead |
| `v.int64()` | `bigint` | Not `v.bigint()`, which is deprecated |
| `v.bytes()` | `ArrayBuffer` | Under 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 \| B` | Use `v.literal` members for enums |
| `v.literal("x")` | `"x"` | |
| `v.optional(t)` | `T \| undefined` | Field may be absent |
| `v.any()` | `any` | Last 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 sentAt
await 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 key
posts: 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 table
postTags: 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
| Mistake | Why it breaks | Do instead |
| --- | --- | --- |
| `withIndex("by_userId")` with no matching `.index()` | Deploy fails | Declare the index in the schema first |
| Index named `by_user` on `["userId", "status"]` | Hides what it covers, easy to misuse | `by_userId_and_status` |
| Querying `status` on `by_userId_and_status` without `userId` | Index prefix rule | Add `by_status` or include `userId` |
| Adding `newField: v.string()` to a table with rows | Existing documents fail validation | `v.optional`, backfill, then require |
| `posts: v.array(v.object(...))` on `users` | 8192 cap, write conflicts on every edit | Separate `posts` table with `by_authorId` |
| `v.bigint()` | Deprecated | `v.int64()` |
| Declaring `_id` or `_creationTime` in `defineTable` | Rejected | They are automatic |
| `v.any()` to move fast | No validation, no types | Model the shape, or a `v.union` of the real cases |
| `v.union(v.string(), v.null())` for a new field | Old documents lack the key entirely | `v.optional(v.string())` |
| Storing a `Date` | Not a Convex value | `v.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
- https://docs.convex.dev/llms.txt
- https://docs.convex.dev/database/schemas
- https://docs.convex.dev/database/indexes
- https://docs.convex.dev/database/types
- https://docs.convex.dev/database/reading-data
© waynesutton · Apache-2.0 · waynesutton/builder-skills