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)


  • 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


import * as S from "@effect/schema/Schema";
import {
  // can be also imported from "@evolu/react"
} from "@evolu/common";
// Without React
import { createEvolu } from "@evolu/common-web";
// With React
import {
} 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) =>
    // SQLite doesn't support the boolean type, but we have `cast` helper.
    .where("isDeleted", "is not", cast(true))
// 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.
// Restore all data on a different device.
// 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.
// Restore all data on a different device.


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).