# Evolu Documentation
# Indexes
Are your queries taking too long to run? Measure their performance using the `logQueryExecutionTime` option:
```ts
const allTodos = evolu.createQuery(
(db) => db.selectFrom("todo").orderBy("createdAt").selectAll(),
{
logQueryExecutionTime: true,
// logExplainQueryPlan: false,
},
);
```
<Note>
While indexes may not be needed during early development, they are crucial for
production performance. Use the `logQueryExecutionTime` and
`logExplainQueryPlan` options in `createQuery` to measure and analyze
performance.
</Note>
For deeper insights into how SQLite indexes work under the hood, read [this in-depth guide](https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346).
<Heading level={2} id="usage">
Usage
</Heading>
```ts
const evolu = createEvolu(evoluReactWebDeps)(Schema, {
// ...
indexes: (create) => [create("todoCreatedAt").on("todo").column("createdAt")],
});
```
Evolu handles this automatically—it will create any new indexes you define and drop those no longer present in the `indexes` array.
<Heading level={2} id="recommendations">
Recommendations
</Heading>
SQLite offers a powerful [CLI tool](https://sqlite.org/cli.html#index_recommendations_sqlite_expert_) for index recommendations.
To use it:
1. Download the "Precompiled Binaries" [here](https://www.sqlite.org/download.html).
2. Open your database or create a new one.
3. Run `.expert` and paste in the SQL of the query you're analyzing.
You can get the query SQL using the `logQueryExecutionTime` option in `createQuery`, which logs the full SQL statement for easy copy-paste.
# Migrations
Traditional centralized databases are versioned and updated using migration scripts. However, this approach isn't practical for local-first databases. Migration scripts can be slow when processing large amounts of local data, and if a migration fails, there's no centralized database to fix—each failed instance would need to be repaired individually.
For these reasons, Evolu embraces a version-less append-only schema—the same pattern as [GraphQL schema design](https://graphql.org/learn/schema-design/). Facebook adopted this pattern to maintain a single endpoint for countless clients, including those that had not been updated for years. Evolu applies this principle to local-first databases, ensuring compatibility across all versions.
## Append only schema
Once an Evolu app is released, the existing database schema must remain unchanged. That means:
- **No renaming tables**
- **No renaming columns**
- **No changing column types**
This is important because there's always a chance that some data has already been created using the previous schema. Changing it would break compatibility.
Instead of migrations, simply add new tables and columns to the existing schema. Newer app code must keep already existing tables and columns in the schema (don't delete them) and continue using them when receiving data from previous app versions. For example, if you replace a string `address` column with an `addressId` column (because we have a new address table), the app should use `addressId` when present, and fall back to `address` when it's not.
This append-only approach raises an important question: if new tables and columns can be added over time and obsolete ones can stop being used at all, how should Evolu apps handle that? The answer is another [GraphQL schema design](https://graphql.org/learn/schema-design/) pattern—nullability.
## Nullability
To understand how Evolu handles nullability, let's look at a simple schema:
```ts {{ title: 'schema.ts' }}
const TodoId = id("Todo");
const Schema = {
todo: {
id: TodoId,
title: NonEmptyString100,
isCompleted: nullOr(SqliteBoolean),
},
};
```
In this schema, `id` and `title` columns are not nullable, while `isCompleted` is nullable (it may have been added later, or it's just not required when a new todo is inserted).
However, even though `title` is not nullable—which is enforced in mutations—Evolu treats it as nullable when writing queries. This forces developers to explicitly define the shape they want.
```ts {{ title: 'query.ts' }}
// Evolu uses Kysely for type-safe SQL (https://kysely.dev/).
const todosQuery = evolu.createQuery((db) =>
db
// Type-safe SQL: try autocomplete for table and column names.
.selectFrom("todo")
.select(["id", "title", "isCompleted"])
// Soft delete: filter out deleted rows.
.where("isDeleted", "is not", sqliteTrue)
// Like with GraphQL, all columns except id are nullable in queries
// (even if defined without nullOr in the schema) to allow schema
// evolution without migrations. Filter nulls with where + $narrowType.
.where("title", "is not", null)
.$narrowType<{ title: kysely.NotNull }>()
// Columns createdAt, updatedAt, isDeleted are auto-added to all tables.
.orderBy("createdAt"),
);
```
This approach ensures data integrity even when CRDT messages arrive out of order. For example, a message deleting a todo might arrive before the message creating it. By explicitly filtering for the shape the current app expects, only valid rows—or rows the current version of the app can handle—are selected from the database.
Just like schemas, queries must be append-only as well. If we stop querying obsolete columns or tables, older data using those columns will suddenly disappear from the results.
# Time Travel
Evolu does not delete data—it only marks it as deleted. This is a fundamental design choice because **local-first is a distributed system**. There is no central authority (if there is, it’s not truly local-first).
Imagine this scenario: you delete a piece of data on a disconnected device, while another device updates that same data. Should the update be discarded? To enforce true deletion across all devices—even future ones—would require complex logic to reject the data forever, without exposing the original data (for security reasons). This is possible (and planned for Evolu), but it's not trivial.
By retaining all data, Evolu enables **time travel**. All mutations—including deletes and overrides—are stored in the `evolu_history` table.
Here’s how to query the history of a specific column:
```ts
const titleHistoryQuery = evolu.createQuery((db) =>
db
.selectFrom("evolu_history")
.select(["value", "timestamp"])
.where("table", "==", "todo")
.where("id", "==", idToIdBytes(id))
.where("column", "==", "title")
// `value` isn't typed; this is how we can narrow its type.
.$narrowType<{ value: (typeof Schema)["todo"]["title"]["Type"] }>()
.orderBy("timestamp", "desc"),
);
const handleHistoryClick = () => {
void evolu.loadQuery(titleHistoryQuery).then((rows) => {
const rowsWithTimestamp = rows.map((row) => ({
value: row.value,
timestamp: timestampToDateIso(timestampBytesToTimestamp(row.timestamp)),
}));
alert(JSON.stringify(rowsWithTimestamp, null, 2));
});
};
```
This API isn’t fully type-safe, but it’s not a concern. Evolu Schemas are append-only. Once an app is released, do not rename or change the type of an existing table or column — only add new tables or columns to evolve your schema; changing existing columns or types breaks compatibility with historical data.
# Get started with the library
This guide will get you up and running with Evolu Library.
<Note>
Requirements: `TypeScript 5.7` or later with the `strict` flag enabled in
`tsconfig.json` file.
</Note>
## Installation
```bash
npm install @evolu/common
```
## Learning path
We recommend learning Evolu Library in this order:
### 1. Result – Error handling
Start with [`Result`](https://evolu.dev/docs/api-reference/common/Result/type-aliases/Result), which provides a type-safe way to handle errors without exceptions. It's the foundation for composable error handling throughout Evolu.
### 2. Task – Asynchronous operations
Learn [`Task`](https://evolu.dev/docs/api-reference/common/Task/interfaces/Task), which represents asynchronous computations in a lazy, composable way.
### 3. Type – Runtime validation
Understand the [`Type`](https://evolu.dev/docs/api-reference/common/Type) system for runtime validation and parsing. This enables you to enforce constraints at compile-time and validate untrusted data at runtime.
### 4. Dependency injection
Explore the [dependency injection pattern](https://evolu.dev/docs/dependency-injection) used throughout Evolu for decoupled, testable code.
### 5. Conventions
Review the [Evolu conventions](https://evolu.dev/docs/conventions) to understand the codebase style and patterns.
## Exploring the API
After understanding the core concepts, explore the full API in the [API reference](https://evolu.dev/docs/api-reference/common). All code is commented and test files are written to be read as examples—they demonstrate practical usage patterns and edge cases.
# Get started with local-first
This guide will get you all set up and ready to use Evolu.
<Note>
Requirements: `TypeScript 5.7` or later with the `strict` and
`exactOptionalPropertyTypes` flags enabled in `tsconfig.json` file.
</Note>
## Installation
Evolu offers SDKs for a variety of frameworks, including React, Svelte, React Native, Expo, and others. Below, you can see how to install the SDKs for each framework.
<SinglePlatformCodeGroup>
```bash {{ title: 'React' }}
npm install @evolu/common @evolu/react @evolu/react-web
```
```bash {{ title: 'React Native' }}
npm install @evolu/common @evolu/react @evolu/react-native \
@op-engineering/op-sqlite react-native-quick-crypto
```
```bash {{ title: 'Expo' }}
npm install @evolu/common @evolu/react @evolu/react-native \
expo-sqlite react-native-quick-crypto
```
```bash {{ title: 'Svelte' }}
npm install @evolu/common @evolu/web @evolu/svelte
```
```bash {{ title: 'Vue' }}
npm install @evolu/common @evolu/web @evolu/vue
```
```bash {{ title: 'Vanilla JS' }}
npm install @evolu/common @evolu/web
```
</SinglePlatformCodeGroup>
<ConditionalPlatformAlert platform={["Expo"]} type="warning">
**Expo Go is not supported** as it requires native dependencies
(`react-native-quick-crypto`). Use `expo-sqlite` for standard performance or
`expo-op-sqlite` for better performance.
</ConditionalPlatformAlert>
<ConditionalPlatformAlert platform={["Expo", "React Native"]} type="info">
Make sure to follow the [react-native-quick-crypto setup
instructions](https://github.com/margelo/react-native-quick-crypto#installation)
for proper native dependency configuration. See the [react-expo
example](https://github.com/evoluhq/evolu/tree/main/examples/react-expo) for
required polyfills.
</ConditionalPlatformAlert>
## Define schema
First, define your app database schema—tables, columns, and types.
Evolu uses [Type](https://evolu.dev/docs/api-reference/common/Type) for data modeling. Instead of plain JS types like string or number, we recommend using branded types to enforce domain rules.
```ts
// Primary keys are branded types, preventing accidental use of IDs across
// different tables (e.g., a TodoId can't be used where a UserId is expected).
const TodoId = Evolu.id("Todo");
type TodoId = typeof TodoId.Type;
// Schema defines database structure with runtime validation.
// Column types validate data on insert/update/upsert.
const Schema = {
todo: {
id: TodoId,
// Branded type ensuring titles are non-empty and ≤100 chars.
title: Evolu.NonEmptyString100,
// SQLite doesn't support the boolean type; it uses 0 and 1 instead.
isCompleted: Evolu.nullOr(Evolu.SqliteBoolean),
},
};
```
<Note>
Evolu automatically adds [system
columns](https://evolu.dev/docs/api-reference/common/local-first/variables/SystemColumns-1):
`createdAt`, `updatedAt`, `isDeleted`, and `ownerId`.
</Note>
## Create Evolu
After defining the schema, create an Evolu instance for your environment.
<SinglePlatformCodeGroup>
```ts {{ title: 'React', language: 'tsx' }}
const evolu = createEvolu(evoluReactWebDeps)(Schema, {
name: SimpleName.orThrow("your-app-name"),
transports: [{ type: "WebSocket", url: "wss://your-sync-url" }], // optional, defaults to free.evoluhq.com
});
// Wrap your app with <EvoluProvider>
<EvoluProvider value={evolu}>
{/* ... */}
</EvoluProvider>
// Create a typed React Hook returning an instance of Evolu
const useEvolu = createUseEvolu(evolu);
// Use the Hook in your app
const { insert, update } = useEvolu();
```
```ts {{ title: 'React Native', language: 'tsx' }}
const evolu = createEvolu(evoluReactNativeDeps)(Schema, {
name: SimpleName.orThrow("your-app-name"),
transports: [{ type: "WebSocket", url: "wss://your-sync-url" }], // optional, defaults to free.evoluhq.com
});
// Wrap your app with <EvoluProvider>
<EvoluProvider value={evolu}>
{/* ... */}
</EvoluProvider>
// Create a typed React Hook returning an instance of Evolu
const useEvolu = createUseEvolu(evolu);
// Use the Hook in your app
const { insert, update } = useEvolu();
```
```ts {{ title: 'Expo', language: 'tsx' }}
const evolu = createEvolu(evoluReactNativeDeps)(Schema, {
name: SimpleName.orThrow("your-app-name"),
transports: [{ type: "WebSocket", url: "wss://your-sync-url" }], // optional, defaults to free.evoluhq.com
});
// Wrap your app with <EvoluProvider>
<EvoluProvider value={evolu}>
{/* ... */}
</EvoluProvider>
// Create a typed React Hook returning an instance of Evolu
const useEvolu = createUseEvolu(evolu);
// Use the Hook in your app
const { insert, update } = useEvolu();
```
```ts {{ title: 'Svelte'}}
const evolu = Evolu.createEvolu(evoluSvelteDeps)(Schema, {
name: Evolu.SimpleName.orThrow("your-app-name"),
transports: [{ type: "WebSocket", url: "wss://your-sync-url" }], // optional, defaults to free.evoluhq.com
});
```
```ts {{ title: 'Vue', language: 'ts' }}
const evolu = createEvolu(evoluWebDeps)(Schema, {
name: SimpleName.orThrow("your-app-name"),
transports: [{ type: "WebSocket", url: "wss://your-sync-url" }], // optional, defaults to free.evoluhq.com
});
export default defineComponent({
setup() {
provideEvolu(evolu);
const { insert, update } = evolu;
return { insert, update };
},
});
```
```ts {{ title: 'Vanilla JS' }}
const evolu = createEvolu(evoluWebDeps)(Schema, {
name: SimpleName.orThrow("your-app-name"),
transports: [{ type: "WebSocket", url: "wss://your-sync-url" }], // optional, defaults to free.evoluhq.com
});
```
</SinglePlatformCodeGroup>
## Mutate data
<SinglePlatformCodeGroup>
```ts {{ title: 'React', language: 'tsx' }}
const { insert, update } = useEvolu();
const result = insert("todo", {
title: "New Todo",
isCompleted: Evolu.sqliteFalse,
});
if (result.ok) {
update("todo", { id: result.value.id, isCompleted: Evolu.sqliteTrue });
}
```
```ts {{ title: 'React Native', language: 'tsx' }}
const { insert, update } = useEvolu();
const result = insert("todo", {
title: "New Todo",
isCompleted: Evolu.sqliteFalse,
});
if (result.ok) {
update("todo", { id: result.value.id, isCompleted: Evolu.sqliteTrue });
}
```
```ts {{ title: 'Expo', language: 'tsx' }}
const { insert, update } = useEvolu();
const result = insert("todo", {
title: "New Todo",
isCompleted: Evolu.sqliteFalse,
});
if (result.ok) {
update("todo", { id: result.value.id, isCompleted: Evolu.sqliteTrue });
}
```
```ts {{ title: 'Svelte', language: 'ts' }}
const result = evolu.insert("todo", {
title: "New Todo",
isCompleted: Evolu.sqliteFalse,
});
if (result.ok) {
evolu.update("todo", { id: result.value.id, isCompleted: Evolu.sqliteTrue });
}
```
```ts {{ title: 'Vue', language: 'ts' }}
const { insert, update } = evolu;
const result = insert("todo", {
title: "New Todo",
isCompleted: Evolu.sqliteFalse,
});
if (result.ok) {
update("todo", { id: result.value.id, isCompleted: Evolu.sqliteTrue });
}
```
```ts {{ title: 'Vanilla JS' }}
const result = evolu.insert("todo", {
title: "New Todo",
isCompleted: Evolu.sqliteFalse,
});
if (result.ok) {
evolu.update("todo", { id: result.value.id, isCompleted: Evolu.sqliteTrue });
}
```
</SinglePlatformCodeGroup>
## Query data
Evolu uses type-safe TypeScript SQL query builder [Kysely](https://github.com/koskimas/kysely), so autocompletion works out-of-the-box.
Let's start with a simple `Query`.
```ts
const allTodos = evolu.createQuery((db) => db.selectFrom("todo").selectAll());
```
Once we have a query, we can load or subscribe to it.
<SinglePlatformCodeGroup>
```ts {{ title: 'React' }}
// ...
const todos = useQuery(allTodos);
```
```ts {{ title: 'React Native' }}
// ...
const todos = useQuery(allTodos);
```
```ts {{ title: 'Expo' }}
// ...
const todos = useQuery(allTodos);
```
```ts {{ title: 'Svelte' }}
// Query once
const todosOnce = await evolu.loadQuery(allTodos);
// todosOnce.rows for all entries
// Subscribe to changes, automatically filled when the Data changes
const todos = queryState(evolu, () => allTodos);
// todos.rows for all entries
// Note this only works in .svelte or .svelte.js / .svelte.ts files due to the Svelte compiler
```
```ts {{ title: 'Vue' }}
const todos = useQuery(allTodos);
```
```ts {{ title: 'Vanilla JS' }}
// Query once
const todos = await evolu.loadQuery(allTodos);
const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
const rows = evolu.getQueryRows(allTodos);
// do something with rows
});
```
</SinglePlatformCodeGroup>
## Delete data
To delete a row, set `isDeleted` to `sqliteTrue` (1). Evolu uses **soft deletes** instead of permanent deletion.
<SinglePlatformCodeGroup>
```ts {{ title: 'React', language: 'tsx' }}
const { update } = useEvolu();
// Mark a todo as deleted
update("todo", { id: todoId, isDeleted: Evolu.sqliteTrue });
```
```ts {{ title: 'React Native', language: 'tsx' }}
const { update } = useEvolu();
// Mark a todo as deleted
update("todo", { id: todoId, isDeleted: Evolu.sqliteTrue });
```
```ts {{ title: 'Expo', language: 'tsx' }}
const { update } = useEvolu();
// Mark a todo as deleted
update("todo", { id: todoId, isDeleted: Evolu.sqliteTrue });
```
```ts {{ title: 'Svelte', language: 'ts' }}
// Mark a todo as deleted
evolu.update("todo", { id: todoId, isDeleted: Evolu.sqliteTrue });
```
```ts {{ title: 'Vue', language: 'ts' }}
const { update } = evolu;
// Mark a todo as deleted
update("todo", { id: todoId, isDeleted: Evolu.sqliteTrue });
```
```ts {{ title: 'Vanilla JS' }}
// Mark a todo as deleted
evolu.update("todo", { id: todoId, isDeleted: Evolu.sqliteTrue });
```
</SinglePlatformCodeGroup>
When querying, filter out deleted rows:
```ts
const activeTodos = evolu.createQuery((db) =>
db
.selectFrom("todo")
.selectAll()
// Filter out deleted rows
.where("isDeleted", "is not", Evolu.sqliteTrue)
.orderBy("createdAt"),
);
```
<Warn>
Evolu does not permanently delete data—it marks them as deleted to support
merging across devices and time travel. This is essential for local-first
systems where devices sync asynchronously. See [Time
Travel](https://evolu.dev/docs/time-travel) to learn how to recover deleted data.
</Warn>
## Protect data
**Privacy is essential for Evolu**, so all data are **encrypted** with an encryption key derived from a cryptographically strong secret (which can be represented as a [mnemonic](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)) or provided by an external hardware device.
<SinglePlatformCodeGroup>
```ts {{ title: 'React', language: 'tsx' }}
// ...
const evolu = useEvolu();
const owner = use(evolu.appOwner);
console.log(owner.mnemonic);
// this will print the mnemonic in the console
```
```ts {{ title: 'React Native', language: 'tsx' }}
// ...
const evolu = useEvolu();
const owner = use(evolu.appOwner);
console.log(owner.mnemonic);
// this will print the mnemonic in the console
```
```ts {{ title: 'Expo', language: 'tsx' }}
// ...
const evolu = useEvolu();
const owner = use(evolu.appOwner);
console.log(owner.mnemonic);
// this will print the mnemonic in the console
```
```ts {{ title: 'Svelte' }}
const owner = appOwnerState(evolu);
// use owner.current in your templates
```
```ts {{ title: 'Vue', language: 'ts' }}
const evolu = useEvolu();
const owner = await evolu.appOwner;
console.log(owner.mnemonic);
```
```ts {{ title: 'Vanilla JS' }}
const owner = await evolu.appOwner;
console.log(owner.mnemonic);
```
</SinglePlatformCodeGroup>
## Purge data
To clear all local data from the device (this is different from soft deletes):
```ts
evolu.resetAppOwner();
```
<Note>
This removes all data from the local database. This is not a soft delete—it's
a complete reset.
</Note>
## Restore data
To restore synced data on any device:
```ts
evolu.restoreAppOwner(mnemonic);
```
To learn more about Evolu, explore our [playgrounds](https://evolu.dev/docs/playgrounds) and [examples](https://evolu.dev/docs/examples).
# Documentation
Evolu is both a **TypeScript library** and a **local-first platform**. Choose your path below.
## TypeScript library
For anyone who wants to write TypeScript code that scales. Built on proven design patterns like Result, dependency injection, immutability, and more. Created by someone who spent years with functional programming, but then decided to go back to the simple and idiomatic TypeScript code—no pipes, no black-box abstractions, no unreadable stacktraces.
[**Get started with the library** →](https://evolu.dev/docs/library)
## Local-first platform
A complete platform for building apps where users own their data. Works offline-first with sync via self-hostable or cloud relays. End-to-end encrypted by default. Built on SQLite with a scalable sync protocol designed for real-world use. No vendor lock-in, no data hostage situations.
[**Get started with local-first** →](https://evolu.dev/docs/local-first)
# Playgrounds
export const sections = [];
# Privacy
Privacy is fundamental to local-first software, and Evolu takes it seriously. Unlike traditional client-server applications where data lives on someone else's servers, Evolu ensures that data remains under the user's complete control while providing the synchronization and backup benefits needed.
## End-to-end encryption by default
Everything in Evolu is encrypted end-to-end. This means:
- Data is stored in encrypted SQLite on the device
- Data is always encrypted when it leaves the device
- The Evolu Relay receives only encrypted data
- Only devices with the correct encryption keys can decrypt the data
- Even if someone intercepts the data in transit or gains access to the relay, they see only meaningless encrypted bytes
The encryption happens automatically—developers don't need to configure it, and users don't need to think about it.
## API design prevents data leaks
Evolu's API is designed to prevent developers from accidentally leaking sensitive data:
- **No public data options**: There is no API to mark data as "public" or "unencrypted"
- **No configuration required**: Developers cannot disable encryption or create security vulnerabilities through misconfiguration
## Traffic analysis protection
To prevent traffic analysis attacks, Evolu uses message padding (PADMÉ)—a [technique](https://lbarman.ch/blog/padme/) that pads binary messages to obscure their actual size. Combined with end-to-end encryption, this ensures that traffic analysis cannot reveal information about data or usage patterns.
## Relay blindness by design
The Evolu Relay is completely blind to user data. What the relay sees:
- **OwnerId**: A unique identifier for the data owner (but not who that owner is)
- **Timestamps**: When changes occurred (for synchronization ordering)
- **Encrypted binary blobs**: The actual data, completely encrypted and padded to obscure size
- **IP addresses**: Network addresses of connecting clients (standard for any network service unless using Tor or similar privacy networks)
The relay functions purely as a message buffer for synchronization and backup—it stores and forwards encrypted messages without any ability to decrypt, analyze, or understand them.
## Timestamp metadata & activity privacy
Relays and collaborators can see timestamps (user activity). This does not increase risk compared to any real‑time messaging system where traffic timing is observable.
### Why timestamps are visible
Real‑time communication inherently reveals that “something happened” (bytes were transferred). Even if we hid explicit timestamps, an observer could record packet timing. Relying on participants not to store this information is unsafe, so explicitly exposing timestamp metadata doesn't add additional risk.
### Mitigating activity leakage
If maximum privacy is required (e.g., hiding interaction cadence), an application can implement a local write queue:
1. Write changes immediately to a local‑only table
2. Periodically and randomly flush messages to sync tables
- **Trade-off:** It breaks real-time collaboration.
## Post-quantum resistance
### Evolu Relay
The Evolu Relay is post-quantum safe, so "harvest now, decrypt later" attacks (where adversaries collect encrypted data today to decrypt with future quantum computers) are not possible. Unlike public-key cryptography systems that use asymmetric encryption (which quantum computers could potentially break), the relay uses only symmetric encryption.
The Evolu Relay never sees or stores public keys—it only handles symmetrically encrypted data. Symmetric encryption algorithms are considered quantum-safe.
### Collaboration
For collaboration, asymmetric cryptography is required, and asymmetric cryptography can be vulnerable to quantum attacks. Detailed documentation will be provided soon.