Patterns
Evolu is using Kysely, the type-safe SQL query builder for TypeScript.
Kysely is not an ORM. It does not have the concept of relations. Kysely is a query builder—it builds the SQL you tell it to, nothing more, nothing less.
However, there is a way to nest related rows in queries. It’s described here, and Evolu supports it.
TL;DR: JSON type with subselects. With this combination, we can write efficient queries with nested relations. Evolu automatically parses stringified JSONs to typed objects and ensures that no regular strings are mistakenly parsed as JSON.
Deferred Sync with Local-Only Tables
Tables prefixed with underscores (_
) are local-only—they’re never synced. This is ideal for device-specific or temporary data.
Imagine editing a JSON-rich text document. Syncing the entire document on every keystroke would be inefficient. While advanced CRDTs like Peritext could solve this, reliable implementations don’t exist yet.
Instead, we can use Evolu’s local-only tables. Saving large JSON blobs on every keystroke isn’t a problem since Evolu uses Web Workers—so it won’t block the main thread. In React Native, InteractionManager.runAfterInteractions
will be used (coming soon).
Is postMessage slow? No, not really. It depends. surma.dev/things/is-postmessage-slow
When it's time to sync, we move data from the local-only table to the synced table. There’s no special API—just set isDeleted
to true
and insert into the regular table. Evolu batches mutations in a microtask and runs them in a transaction, ensuring atomicity and no data loss.
// Both `update` and `insert` run within a transaction.
evolu.update("_todo", { id: someId, isDeleted: true });
// This mutation starts syncing immediately.
evolu.insert("todo", { title });
So when should syncing happen? While a manual "sync" button is possible, it’s not great UX. A better approach is to use a heuristic to detect user intent—such as page visibility, route changes, or input blur events. We can’t rely on unload
, as it’s unreliable. Evolu will release a helper for this soon.
New Evolu: This doc is from an earlier version of Evolu. We removed the heuristics.
JSON
SQLite supports JSON, but it stores and returns it as a string. Evolu improves DX by automatically stringifying and parsing JSON values.
const SomeJson = json(
object({ foo: NonEmptyString1000, bar: SqliteBoolean }),
"SomeJson",
);
const TodoTable = {
id: TodoId,
title: NonEmptyString1000,
json: SomeJson,
};
// No need to call JSON.stringify
evolu.insert("todo", { title, json: { foo: "a", bar: false } });
const rows = useQuery(allTodos);
// Evolu automatically parses JSONs into typed objects.
if (rows[0]) console.log(rows[0].json.foo);
Evolu’s automatic JSON parsing is based on heuristics: if a value looks like a
stringified JSON, it attempts to parse it. To avoid accidental parsing, always
use Evolu Type for plain string columns. See how the NonEmptyString1000
schema is implemented to ensure type safety and avoid surprises.