Schema Evolution
Versioned streams with declarative migration functions. When you change a data shape, old clients receive migrated data automatically.
Versioning a stream
Add version and migrate to your stream options:
export const todos = live.stream('todos', async (ctx) => {
return db.todos.all();
}, {
merge: 'crud',
key: 'id',
version: 3,
migrate: {
// v1 -> v2: add priority field
1: (item) => ({ ...item, priority: item.priority ?? 'medium' }),
// v2 -> v3: rename 'done' to 'completed'
2: (item) => {
const { done, ...rest } = item;
return { ...rest, completed: done ?? false };
}
}
}); How it works
The Vite plugin includes the stream version in the client stub. On reconnect, the client sends its version. If the server is ahead, migration functions chain in order (v1 → v2 → v3). If versions match, no migration runs.
Each key in the migrate object is the version the data is migrating from. The function transforms a single item from that version to the next. Migrations compose sequentially - a client on v1 connecting to a v3 server runs both migration 1 and migration 2.
Options
| Option | Default | Description |
|---|---|---|
version | - | Current schema version number |
migrate | - | Object mapping source version numbers to (item) => item migration functions |
When to use schema evolution
Schema evolution is useful when you deploy server changes while clients may still be running older code. Instead of forcing a page reload, the server transparently migrates data to the shape the client expects.
Common scenarios:
- Adding a new field with a default value
- Renaming a field
- Changing a field type (e.g. string → object)
- Removing a deprecated field
Schema evolution works with all merge strategies. The migration function is applied per-item for array-based strategies (crud, latest, presence, cursor) and to the entire value for set.
Was this page helpful?