Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Convex Schema Validator

ASecurity

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.

405 stars
0 votes
0 copies
0 views
Added 9/28/2026
ai-agentstypescriptgodatabase

Security Analysis

A100/100

Scanned 9/28/2026

Install to Claude Code

$npx -y skills add waynesutton/builder-skills --skill convex-schema-validator --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convex Schema Validator?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Convex Schema Validator
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/waynesutton-convex-schema-validator-builder-skills/badge)](https://www.skillsdirectory.com/skills/waynesutton-convex-schema-validator-builder-skills)

More formats (shields.io, HTML) on the badges page.

Files
SKILL.md
---
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

Attribution

waynesuttonwaynesutton
View sourceMore from waynesutton →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode that cuts output tokens while keeping technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".

1074701 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

695601 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3351 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

691 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →