Local-First Platform Designed for Privacy, Ease of Use, and No Vendor Lock-In
"If it doesn't work, if the app developer goes out of business and shuts down their servers, then it's not local-first." Martin Kleppmann
Documentation · GitHub repository (opens in a new tab)
Features
- SQLite (opens in a new tab) in all browsers, Electron, and React Native
- CRDT (opens in a new tab) for merging changes without conflicts
- End-to-end encrypted sync and backup
- Free Evolu sync and backup server, or you can run your own
- Typed database schema (with branded types like
NonEmptyString1000
,PositiveInt
, etc.) - Typed SQL via Kysely (opens in a new tab)
- Reactive queries with full React Suspense support
- Real-time experience via revalidation on focus and network recovery
- No signup/login, only bitcoin-like mnemonic (12 words)
- Ad-hoc migration
- Sqlite JSON support with automatic stringifying and parsing
- Support for Kysely Relations (opens in a new tab) (loading nested objects and arrays in a single SQL query)
- Local-only tables (tables with _ prefix are not synced)
- Evolu Solid/Vue/Svelte soon
Overview
import * as S from "@effect/schema/Schema";
import {
NonEmptyString1000,
SqliteBoolean,
cast,
database,
id,
table,
// can be also imported from "@evolu/react"
} from "@evolu/common";
// Without React
import { createEvolu } from "@evolu/common-web";
// With React
import {
createEvolu,
useEvolu,
useQuery,
} from "@evolu/react";
const TodoId = id("Todo");
// It's branded string: string & Brand<"Id"> & Brand<"Todo">
// TodoId type ensures no other ID can be used where TodoId is expected.
type TodoId = typeof TodoId.Type;
const TodoTable = table({
id: TodoId,
// Note we can enforce NonEmptyString1000.
title: NonEmptyString1000,
// SQLite doesn't support the boolean type, so Evolu uses SqliteBoolean instead.
isCompleted: S.nullable(SqliteBoolean),
});
type TodoTable = typeof TodoTable.Type;
const Database = database({
todo: TodoTable,
});
type Database = typeof Database.Type;
const evolu = createEvolu(Database);
// Create a typed SQL query. Yes, with autocomplete and type-checking.
const allTodos = evolu.createQuery((db) =>
db
.selectFrom("todo")
.selectAll()
// SQLite doesn't support the boolean type, but we have `cast` helper.
.where("isDeleted", "is not", cast(true))
.orderBy("createdAt"),
);
// Load the query. Batched and cached by default.
const allTodosPromise = evolu.loadQuery(allTodos);
// React Helper Functions
// Use the query in React reactively (it's updated on a mutation).
const { rows } = useQuery(allTodos);
// Create a todo.
const { create } = useEvolu<Database>();
create("todo", {
title: S.decodeSync(NonEmptyString1000)("Learn Effect"),
});
// Update a todo.
const { update } = useEvolu<Database>();
update("todo", { id, isCompleted: true });
// Delete all data on a device.
useEvolu().resetOwner();
// Restore all data on a different device.
useEvolu().restoreOwner(mnemonic);
// All other Frameworks
// Create a todo.
evolu.create("todo", {
title: S.decodeSync(NonEmptyString1000)("Learn Effect"),
});
// Update a todo.
evolu.update("todo", { id, isCompleted: true });
// Delete all data on a device.
evolu.resetOwner();
// Restore all data on a different device.
evolu.restoreOwner(mnemonic);
Community
The Evolu community is on GitHub Discussions (opens in a new tab).
To chat with other community members, you can join the Evolu Discord (opens in a new tab).