> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Schema

> Define your tables, fields, types, indexes, and validators. The single source of truth for your app's data shape.

The schema is your app's data contract. It's a single TypeScript file (`convex/schema.ts`) that defines every table, every field, and every index. Convex enforces it at the database level — writes that don't match the schema are rejected.

vly generates and updates the schema automatically as you add features. You can also edit it by hand for fine-grained control.

## The basics

```typescript convex/schema.ts theme={null}
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  tasks: defineTable({
    title: v.string(),
    completed: v.boolean(),
    ownerId: v.id("users"),
    dueAt: v.optional(v.number()),
  }),

  users: defineTable({
    email: v.string(),
    name: v.string(),
    avatarUrl: v.optional(v.string()),
  }),
});
```

That's the whole file for a simple two-table app. `defineSchema` takes a map from table name to `defineTable` call. Each `defineTable` defines the fields.

## Field validators

Use the `v` validator namespace to declare each field's type:

| Validator | TypeScript equivalent | Use for |
| - | - | - |
| `v.string()` | `string` | Text |
| `v.number()` | `number` | Integers, floats, timestamps |
| `v.boolean()` | `boolean` | Flags |
| `v.id("users")` | `Id<"users">` | Foreign key reference |
| `v.array(v.string())` | `string[]` | Arrays |
| `v.object({ a: v.string() })` | `{ a: string }` | Nested objects |
| `v.union(v.literal("a"), v.literal("b"))` | `"a" \| "b"` | Enum / union |
| `v.optional(v.string())` | `string \| undefined` | Optional fields |
| `v.null()` | `null` | Explicit null |
| `v.bytes()` | `ArrayBuffer` | Binary data |
| `v.any()` | `any` | Avoid; loses type safety |

A more realistic example with all of these:

```typescript convex/schema.ts theme={null}
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  tasks: defineTable({
    title: v.string(),
    completed: v.boolean(),
    ownerId: v.id("users"),
    assigneeId: v.optional(v.id("users")),
    dueAt: v.optional(v.number()),
    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
    tags: v.array(v.string()),
    metadata: v.optional(v.object({
      source: v.string(),
      importedAt: v.number(),
    })),
  }),
});
```

## Indexes

Indexes make queries fast. Add them with `.index()` on the table:

```typescript convex/schema.ts theme={null}
tasks: defineTable({
  title: v.string(),
  completed: v.boolean(),
  ownerId: v.id("users"),
})
  .index("by_owner",                ["ownerId"])
  .index("by_owner_and_completed",  ["ownerId", "completed"])
  .index("by_completed_and_owner",  ["completed", "ownerId"])
```

A few rules of thumb:

<CardGroup cols={2}>
  <Card title="Index every field you filter or sort by" icon="zap">
    `.withIndex()` queries are O(log n). Without an index, queries fall back to `.filter()` which is O(n) — slow on large tables.
  </Card>

  <Card title="Compound indexes follow query order" icon="list">
    `["ownerId", "completed"]` accelerates queries that filter by `ownerId` (or `ownerId` + `completed`). It does *not* accelerate filtering by `completed` alone.
  </Card>

  <Card title="Index naming convention" icon="tag">
    `by_<field>` for single-column. `by_<a>_and_<b>` for compound. Consistency helps query authors find the right index fast.
  </Card>

  <Card title="Don't over-index" icon="alert-triangle">
    Each index slows down writes slightly. Add them when a query is slow, not preemptively.
  </Card>
</CardGroup>

## Search indexes

For full-text search, add a search index:

```typescript theme={null}
.searchIndex("search_title", {
  searchField: "title",
  filterFields: ["ownerId", "completed"],
})
```

Then query with `.withSearchIndex()`. See [Full-text search](/convex/full-text-search).

For semantic / vector search, add a vector index:

```typescript theme={null}
.vectorIndex("by_embedding", {
  vectorField: "embedding",
  dimensions: 1536, // OpenAI ada-002 / text-embedding-3-small
  filterFields: ["ownerId"],
})
```

See [Vector search](/convex/vector-search).

## Built-in fields

Convex adds two fields to every document automatically:

| Field | Type | Purpose |
| - | - | - |
| `_id` | `Id<"tableName">` | Unique document ID |
| `_creationTime` | `number` | Milliseconds since epoch |

These are auto-set on insert and can be queried like any other field. `_creationTime` is indexable; sorting by it gives you newest-first ordering for free.

```typescript theme={null}
const recentTasks = await ctx.db
  .query("tasks")
  .order("desc")
  .take(10);
// orders by _creationTime descending
```

## Foreign keys

Use `v.id("tableName")` for references between tables:

```typescript theme={null}
tasks: defineTable({
  ownerId: v.id("users"),
})
```

This is enforced — you can only insert a task with an `ownerId` that points to an existing `users` row. The TypeScript type `Id<"users">` is also branded, so you can't accidentally pass a `tasks` ID where a `users` ID is expected.

## How vly handles schema evolution

When you ask vly to add a field:

```text theme={null}
Add a `priority` field to tasks (low / medium / high, default 'medium').
```

vly:

1. Updates the schema file with the new validator.
2. Generates a migration script that backfills existing rows to the default.
3. Updates affected queries and mutations to handle the new field.
4. Updates UI components to display / edit the field.
5. Runs the migration in the next deploy.

You see the migration in the build output, and it's reversible from [version history](/building/version-history).

## Hand-editing patterns

When you want fine control:

<Tabs>
  <Tab title="Tighten validation">
    ```typescript theme={null}
    // before:
    email: v.string(),

    // after — only accept lowercase emails:
    email: v.string(), // validation done at the mutation layer:
    // if (!isLowercaseEmail(email)) throw new Error("...");
    ```

    Convex's schema validators are type-level; for runtime business rules, validate inside mutations.
  </Tab>

  <Tab title="Add an index for a slow query">
    ```typescript theme={null}
    tasks: defineTable({...})
      .index("by_team_and_priority", ["teamId", "priority"]);
    ```

    Then update the query to use it:

    ```typescript theme={null}
    .withIndex("by_team_and_priority",
      (q) => q.eq("teamId", teamId).eq("priority", "high"))
    ```
  </Tab>

  <Tab title="Soft delete">
    ```typescript theme={null}
    tasks: defineTable({
      title: v.string(),
      deletedAt: v.optional(v.number()), // null when active
    })
      .index("by_active", ["deletedAt"]);
    ```

    Then queries filter on `deletedAt === undefined`.
  </Tab>
</Tabs>

## Common pitfalls

<AccordionGroup>
  <Accordion title="Querying a non-indexed field" icon="alert-triangle">
    `.filter()` walks every row. On a 100k-row table, this is multiple seconds. Always check for `.withIndex()` opportunities.
  </Accordion>

  <Accordion title="Using v.any()" icon="alert-triangle">
    Loses type safety. The TypeScript inference downstream becomes `any`, which infects every consumer. Use a `v.union(...)` of literals instead, or an explicit `v.object({...})`.
  </Accordion>

  <Accordion title="Forgetting to index foreign keys" icon="alert-triangle">
    Whenever you query `where teamId = X`, you need `by_team` on `teamId`. Foreign keys are not auto-indexed.
  </Accordion>

  <Accordion title="Schema drift between branches" icon="alert-triangle">
    If two branches add different fields to the same table, merging is messy. vly uses [version history](/building/version-history) for snapshotting, but for big schema changes, prefer to land them sequentially.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={3}>
  <Card title="Queries" icon="search" href="/convex/queries">
    Read patterns: indexes, filters, ordering, pagination.
  </Card>

  <Card title="Mutations" icon="pencil" href="/convex/mutations">
    Transactional writes.
  </Card>

  <Card title="Indexes" icon="zap" href="/convex/indexes">
    Deep dive on index design.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.