Update Convex with no payload to be just like convex with payload but without payload

This commit is contained in:
Gabriel Brown
2026-06-21 15:35:42 -05:00
parent 13b8b36c4c
commit fba73a92ce
130 changed files with 15637 additions and 32018 deletions
@@ -0,0 +1,401 @@
---
name: convex-schema-validator
displayName: Convex Schema Validator
description: Defining and validating database schemas with proper typing, index configuration, optional fields, unions, and migration strategies for schema changes
version: 1.0.0
author: Convex
tags: [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.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()),
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
```typescript
export 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
```typescript
export 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
```typescript
export 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
```typescript
export 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:
```typescript
export 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
// Before
users: defineTable({
name: v.string(),
email: v.string(),
});
// After - add as optional first
users: defineTable({
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string()), // New optional field
});
```
#### Backfilling Data
```typescript
// convex/migrations.ts
import { v } from 'convex/values';
import { internalMutation } from './_generated/server';
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 required
users: defineTable({
name: v.string(),
email: v.string(),
avatarUrl: v.string(), // Now required after backfill
});
```
## Examples
### Complete E-commerce Schema
```typescript
// convex/schema.ts
import { 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.ts
import { v } from 'convex/values';
import { Doc, Id } from './_generated/dataModel';
import { mutation, query } from './_generated/server';
// Use Doc type for full documents
type Product = Doc<'products'>;
// Use Id type for references
type 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 index
2. **Wrong index field order** - Fields must be queried in order defined
3. **Using v.any() excessively** - Lose type safety benefits
4. **Not making new fields optional** - Breaks existing data
5. **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