Local-first platform designed for privacy, ease of use, and no vendor lock-in

Get started · 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 * as Evolu from "@evolu/react";
 
// Create TodoId schema.
const TodoId = Evolu.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 = S.Schema.To<typeof TodoId>;
 
// Create TodoTable schema.
const TodoTable = S.struct({
  id: TodoId,
  // Note we can enforce NonEmptyString1000.
  title: Evolu.NonEmptyString1000,
  // SQLite doesn't support the boolean type, so Evolu uses
  // SqliteBoolean (a branded number) instead.
  isCompleted: S.nullable(Evolu.SqliteBoolean),
});
type TodoTable = S.Schema.To<typeof TodoTable>;
 
// Create database schema.
const Database = S.struct({
  todo: TodoTable,
});
 
// Create Evolu.
const evolu = Evolu.create(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", Evolu.cast(true))
    .orderBy("createdAt"),
);
 
// Load the query. Batched and cached by default.
const allTodosPromise = evolu.loadQuery(allTodos);
 
// React Hooks.
const { useEvolu, useQuery } = evolu;
 
// Use the query in React reactively (it's updated on a mutation).
const { rows, row } = useQuery(allTodos);
 
// Create a todo.
const { create } = useEvolu();
create("todo", {
  title: S.parseSync(Evolu.NonEmptyString1000)("Learn Effect"),
});
 
// Update a todo.
const { update } = useEvolu();
update("todo", { id, isCompleted: true });
 
// Delete all data on a device.
useEvolu().resetOwner();
 
// Restore all data on a different defice.
useEvolu().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).