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