local‑first all the things
Local-first platform
SQLite on every platform, synced end‑to‑end encrypted without an authoritative server. Relays, which you can host yourself, only store and pass on changes they can't read.
TypeScript library
The library Evolu itself is built on, for any TypeScript project: Result for typed errors, Type for runtime validation, and Task for structured concurrency, in idiomatic TypeScript.
Simple, strict, secure, honest
Local-first platform
import {
createQueryBuilder,
id,
NonEmptyTrimmedString100,
nullOr,
SqliteBoolean,
sqliteTrue,
type KyselyNotNull,
} from "@evolu/common";
// Any Standard Schema library can be used.
export const AppSchema = {
todo: {
// Primary keys are branded types, so a TodoId can't be used where another
// table's ID is expected.
id: id("Todo"),
title: NonEmptyTrimmedString100,
// SQLite doesn't support the boolean type; it uses 0 and 1 instead.
isCompleted: nullOr(SqliteBoolean),
},
};
const createAppQuery = createQueryBuilder(AppSchema);
export const todosQuery = createAppQuery((db) =>
db
// Type-safe SQL: try autocomplete for table and column names.
.selectFrom("todo")
.select(["id", "title", "isCompleted"])
// Soft delete: filter out deleted rows.
.where("isDeleted", "is not", sqliteTrue)
// All columns except id are nullable in queries, so the schema can evolve
// without migrations. Filter nulls with where + $narrowType.
.where("title", "is not", null)
.$narrowType<{ title: KyselyNotNull }>()
.orderBy("createdAt"),
);
Apps that keep data on their servers stop working offline, make users wait on spinners, and take their data with them when the company behind them changes its plans. Local‑first apps read and write on the device, so they respond instantly, work offline, and keep working for as long as their users need them.
Browsers, React Native, and Electron, with React, Vue, and Svelte bindings. Plain SQLite, compiled from sqlite.org's verified source.
Kysely queries whose results update when local or synced data changes.
Define columns with Evolu Type or any Standard Schema library, such as Zod.
Relays store and forward encrypted changes they can't read.
The relay is MIT-licensed and self-hostable. Use several at once, so sync survives an outage.
Every synced change is kept, so you can travel in time. The last writer wins per column.
TypeScript library
import {
assertEqual,
assertErr,
localizeTypes,
NonEmptyTrimmedString100,
} from "@evolu/common";
import { cs } from "@evolu/common/intl";
// A failed validation returns its error as plain data, not a thrown Error.
const result = NonEmptyTrimmedString100.fromUnknown("");
assertErr(result);
assertEqual(result.error, { type: "MinLength1", min: 1, value: "" });
// formatError turns the data into a message.
assertEqual(
NonEmptyTrimmedString100.formatError(result.error),
'The value "" does not meet the minimum length of 1.',
);
// A localized copy validates the same and changes only the messages. Every
// error the Type can return needs a formatter, or it doesn't compile.
const { cs: czech } = localizeTypes(
{ Title: NonEmptyTrimmedString100 },
{
cs: {
String: cs.formatStringError,
Trimmed: cs.formatTrimmedError,
MinLength1: cs.formatMinLengthError,
MaxLength100: cs.formatMaxLengthError,
},
},
);
assertEqual(czech.Title.formatError(result.error), "Text nesmí být prázdný.");
It's written test‑first: its tests are the runnable specification, and most modules have full test coverage. The source and unit tests ship in the npm package, and every example in its API reference is type‑checked and run, so developers and agents learn from verified code.
Typed errors. No try/catch. Exhaustive error handling.
Runtime types with typed errors and formatters. Every constraint shows in its type.
Structured concurrency with dependency injection. Plain async functions, no generators.
Helpers for Array, Object, Set, String, Eq, Order, Time, and more.
Resource management with the JavaScript using keyword.
Invariants types can't express are asserted at runtime. Use them in your tests too.
One documented style for names, errors, and dependencies, so every module reads the same.
Tree-shakable, with few dependencies. Types, Tasks, and logging in 9.3 kB.
Built with Evolu
- Trezor SuiteIn production
SatoshiLabs integrates Evolu with local authentication backed by Trezor.
- LinkyIn production
A Bitcoin wallet and messaging app in one.
- PaykyIn development
A local-first payment terminal for bank transfers, cash, and Bitcoin Lightning.
Made by Daniel Steigerwald
When I started Evolu, I knew I didn't know everything, so I built on popular functional programming libraries. That was a compromise, because depending on code I don't understand is a kind of vendor lock‑in too. So I wrote my own TypeScript library. For structured concurrency and resource management, I studied how other languages, frameworks, and libraries handle them. It was an incredible journey, and I stand behind how Evolu works today.
I've also learned that tests can show bugs but never prove their absence. Formal verification can, so future versions of Evolu will be formally verified.