Documentation
Quickstart

Quickstart

Let's learn how to use Evolu in a few steps.

Define Data

First, we define a database schema: tables, columns, and their types.

💡

Evolu uses Schema (opens in a new tab) for data modeling. Instead of plain JavaScript types like String or Number, we recommend branded types (opens in a new tab). With branded types, we can define and enforce domain rules like NonEmptyString1000 or PositiveInt.

import * as S from "@effect/schema/Schema";
import {
  NonEmptyString1000,
  SqliteBoolean,
  createEvolu,
  id,
  table,
  database,
} from "@evolu/react";
 
const TodoId = id("Todo");
type TodoId = typeof TodoId.Type;
 
const TodoTable = table({
  id: TodoId,
  title: NonEmptyString1000,
  isCompleted: SqliteBoolean,
});
type TodoTable = typeof TodoTable.Type;
 
const Database = database({
  todo: TodoTable,
});
type Database = typeof Database.Type;
 
const evolu = createEvolu(Database);

TypeScript compiler ensures that the title can't be an arbitrary string. It has to be parsed with the NonEmptyString1000 schema. Isn't that beautiful?

Parse Data

import * as S from "@effect/schema/Schema";
import { NonEmptyString1000 } from "@evolu/react";
 
S.decode(NonEmptyString1000)(title);

Learn more about Schema (opens in a new tab). It's like Zod (opens in a new tab), but faster and with better design.

Mutate Data

While Evolu provides the full SQL for queries, the mutation API is tailored for local-first apps to ensure changes can be merged without conflicts and to mitigate the possibility that a developer accidentally makes unwanted changes—for example, an update of all rows in a table. That would generate a lot of CRDT messages that would have to be propagated to all other devices. It's not bad per se; it's just something that shouldn't be necessary with proper database schema design.

// Without React
const { id } = evolu.create("todo", { title, isCompleted: false });
evolu.update("todo", { id, isCompleted: true }, () => {
  // done
});
 
// With React
const { create, update } = useEvolu<Database>();

Note there is no error handling because there is no reason why a mutation should fail. Types ensure correctness, and the local SQLite database is always available. For rare cases where Evolu can fail, use global evolu.subscribeError or useEvoluError React Hook.

To delete a row, set isDeleted to true and filter "deleted" rows in queries.

💡

CRDT without an authoritative server doesn't delete data; it just marks them as deleted. This is not a bug; it's a feature that allows data to be merged without conflicts (overridden data can be restored).

Query Data

Evolu uses type-safe TypeScript SQL query builder Kysely (opens in a new tab), so autocompletion works out-of-the-box. Let's start with a simple Query.

const allTodos = evolu.createQuery((db) => db.selectFrom("todo").selectAll());

Once we have a query, we can load or subscribe to it.

const promise = evolu.loadQuery(allTodos);
const unsubscribe = evolu.subscribeQuery(allTodos)(callback);
💡

Evolu provides React Hooks useQuery and useQueries with the full React Suspense support. For example, we can load a query in the root and use the returned promise elsewhere.

Protect Data

Privacy is essential for Evolu, so all data are encrypted with an encryption key derived from a safely generated cryptographically strong password called mnemonic (opens in a new tab).

// evolu.subscribeOwner
const owner = evolu.getOwner();
if (owner) owner.mnemonic;

Delete Data

Leave no traces on a device.

if (confirm("Are you sure? It will delete all your local data."))
  evolu.resetOwner();

Restore Data

Synced Evolu data can be restored with mnemonic on any device.

evolu.restoreOwner(mnemonic);

And that's all we need to know to work with Evolu. The minimal API is the key to good DX.