import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
messages: defineTable({
author: v.string(),
body: v.string(),
likes: v.number(),
}),
users: defineTable({
name: v.string(),
email: v.string(),
age: v.number(),
}),
});defineTable({
author: v.string(),
body: v.string(),
}).index('by_author', ['author'])defineTable({
author: v.string(),
category: v.string(),
timestamp: v.number(),
})
.index('by_author_and_category', ['author', 'category'])
.index('by_category_and_time', ['category', 'timestamp'])defineTable({
author: v.string(),
status: v.string(),
timestamp: v.number(),
})
.index('by_author', ['author'])
.index('by_status', ['status'])
.index('by_time', ['timestamp'])defineTable({
title: v.string(),
body: v.string(),
author: v.string(),
category: v.string(),
}).searchIndex('search_body', {
searchField: 'body',
filterFields: ['author', 'category'],
})
// Usage in query
const results = await ctx.db.query('posts')
.withSearchIndex('search_body', q =>
q.search('body', 'keyword')
.eq('author', 'Alice')
.eq('category', 'tech')
)
.collect();defineTable({
content: v.string(),
embedding: v.array(v.float64()),
category: v.string(),
userId: v.id('users'),
}).vectorIndex('by_embedding', {
vectorField: 'embedding',
dimensions: 1536, // Must match your embedding model
filterFields: ['category', 'userId'],
})
// Usage in action
const results = await ctx.vectorSearch('documents', 'by_embedding', {
vector: embeddingArray,
limit: 10,
filter: q => q.eq('category', 'news'),
});defineTable({
// Primitives
text: v.string(),
count: v.number(),
bigNumber: v.bigint(),
flag: v.boolean(),
data: v.bytes(),
// References
userId: v.id('users'),
// Optional
nickname: v.optional(v.string()),
metadata: v.optional(v.object({ key: v.string() })),
// Arrays
tags: v.array(v.string()),
scores: v.array(v.number()),
// Nested objects
profile: v.object({
bio: v.string(),
avatar: v.string(),
settings: v.object({
theme: v.string(),
notifications: v.boolean(),
}),
}),
// Records (dynamic keys)
metadata: v.record(v.string(), v.any()),
scores: v.record(v.string(), v.number()),
// Unions
status: v.union(
v.literal('active'),
v.literal('inactive'),
v.literal('pending')
),
// Nullable
parentId: v.nullable(v.id('items')),
})All documents automatically include:
{
_id: Id<'tableName'>, // Unique ID
_creationTime: number, // Unix timestamp (ms)
// ... your fields
}export default defineSchema(
{
messages: defineTable({ ... }),
},
{
schemaValidation: true, // Strict (default)
// schemaValidation: false, // No validation
// schemaValidation: 'warn', // Warnings only
}
);defineTable({
title: v.string(),
slug: v.string(),
content: v.string(),
authorId: v.id('users'),
authorName: v.string(),
tags: v.array(v.string()),
published: v.boolean(),
publishedAt: v.optional(v.number()),
views: v.number(),
likes: v.number(),
})
.index('by_slug', ['slug'])
.index('by_author', ['authorId'])
.index('by_published', ['published', 'publishedAt'])
.searchIndex('search_content', {
searchField: 'content',
filterFields: ['published', 'authorId'],
})defineTable({
name: v.string(),
description: v.string(),
price: v.number(),
category: v.string(),
brand: v.string(),
inStock: v.boolean(),
stockCount: v.number(),
images: v.array(v.string()), // Storage IDs
attributes: v.record(v.string(), v.string()),
tags: v.array(v.string()),
embedding: v.array(v.float64()),
})
.index('by_category', ['category'])
.index('by_brand', ['brand'])
.index('by_category_and_price', ['category', 'price'])
.searchIndex('search_name', {
searchField: 'name',
filterFields: ['category', 'brand', 'inStock'],
})
.vectorIndex('by_embedding', {
vectorField: 'embedding',
dimensions: 768,
filterFields: ['category', 'inStock'],
})defineTable({
email: v.string(),
emailVerified: v.boolean(),
name: v.string(),
profile: v.object({
bio: v.optional(v.string()),
avatar: v.optional(v.string()),
website: v.optional(v.string()),
social: v.optional(v.object({
twitter: v.optional(v.string()),
github: v.optional(v.string()),
})),
}),
settings: v.object({
theme: v.union(v.literal('light'), v.literal('dark')),
notifications: v.object({
email: v.boolean(),
push: v.boolean(),
}),
}),
role: v.union(v.literal('user'), v.literal('admin'), v.literal('moderator')),
createdAt: v.number(),
lastLoginAt: v.optional(v.number()),
})
.index('by_email', ['email'])
.index('by_role', ['role'])withIndex() queries_id, _creationTimev.optional(): Field may be absent (undefined)v.nullable(): Field is present but may be nullv.optional() for backwards compatibilityprofile.bio, settings.theme