Epicenter: Local-First Apps Over a Yjs Store You Own
Open-source, local-first apps.
At a glance
- What is it?
- Epicenter turns an application's entire data set into one Yjs CRDT document on your machine, with synchronous reads and one Cloudflare Durable Object per account for sync. Honeycrisp, a notes app, is the app running on it today.
- Who is it for?
- Adopt Epicenter if you are building a desktop app whose data must work offline and stay user-owned, and you accept the AGPL-3.0-or-later terms and a single-desktop runtime. Do not adopt it if you need a hosted web runtime with a host-owned replica, third-party installed apps, or automatic migration of data from an older stack: the README says the superseded data stack was deleted and old data is not imported.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The synchronization problem Epicenter is built around
If every device keeps its own SQLite file, keeping those files in sync is the hard part. Epicenter's answer is that a database is one Yjs document, replayed in full before any handle exists, and the surface over it is synchronous. A read is a property access, not a round trip, so the README states nothing is awaited and nothing needs cache invalidation or race protection. That single decision shapes everything else: the asynchronous boundary is opening the database, and after that reads and writes are plain calls.
The audience is developers building desktop apps where the data set should survive the network being off and should not belong to a server. The README frames it as local-first apps over a store you own, and the one app running on it today is Honeycrisp, a local-first notes app. The repository also contains Matter, Local Books and Local Mail, but those do not use the store, so they are not evidence about the store's behaviour.
How a database, a row and rich content are shaped
A data definition is one application's declaration of its durable data: pure JSON field descriptors, with no storage and no lifecycle of its own. The README says it is release-local and never migrates your data. The database document's kv and tables:<name> shape is recorded in ADR-0257, and its collapse to one document in ADR-0295, both under docs/adr.
The rich half of a row lives on the row itself. A row's node is a nested type on the row, not a second document with an address of its own. The node at a plainText field merges per character, and Epicenter never looks inside it: you declare the codec and reach it through the row. A row the definition cannot read is reported beside the rows it can, with the reason and the raw values intact, and the README says an ordinary write repairs it. That is a deliberate failure mode: bad data is surfaced, not silently dropped.
Sync is one Cloudflare Durable Object per (account, application). Being signed in on two devices is the entire sharing model, per the README: nothing is paired, invited, or approved.
Installing Epicenter and opening your first database
The README does not give a standalone install command for the substrate. The repository is a Bun workspace with a bun.lock and bunfig.toml at the top level, and the package.json declares a workspaces catalog, so the documented path is to work from the repository. The README points to packages/data/README.md for the data package docs and to apps/honeycrisp as the worked example.
Once the repository is set up, opening a database is the asynchronous boundary. This example comes from the README:
import { openDatabase } from '@epicenter/data/browser';
import { defineData, defineTable, field, plainText } from '@epicenter/data/definition';
const notesDefinition = defineData({
id: 'com.example.notes',
kv: {},
tables: {
notes: defineTable({
title: field.string(),
pinned: field.boolean(),
folderId: field.nullable(field.string()),
content: plainText(),
}),
},
});Opening returns a result object, and the README's example throws on a non-null error before touching the data:
const { data, error } = await openDatabase(notesDefinition, { generation: 1 });
if (error !== null) throw error;
const note = data.tables.notes.create({ title: 'Hello', pinned: false, folderId: null });After that, reads are synchronous. The README shows listing rows and subscribing to changes, where the callback re-reads the rows:
const listed = data.tables.notes.rows; // synchronous flat rows
const stop = data.tables.notes.subscribe(() => { /* re-read rows */ });To reach the rich content of a row, the README uses data.tables.notes.get(note.id)?.content, and notes that the node merges per character. The documentation does not describe a rollback path for a definition change.
Trust boundaries and what leaves the device
The README lays out the trust model as a table, and it is worth reading before choosing a deployment. Signed out, nothing leaves the device: the store is complete on the machine it opened on, and every read comes from a document already in memory. Signed in, your application's document goes as opaque update bytes to one authority per account. On hosted Epicenter that authority is the project's, along with account and session data and any hosted feature you enable. A self-hosted instance puts the server, secrets, deployment and infrastructure boundary under your control.
The README is direct about the consequence: signed-in sync sends your data to a trusted server that reads it in plaintext. Self-hosting moves that plaintext onto infrastructure you control. Separately, when an app calls a provider, whatever that app sends leaves through that path: transcript text to an LLM, audio to a transcription provider. Epicenter servers are not in that path. The trust model document is docs/trust-model.md.
Where Epicenter is the wrong tool
There is one runtime: a desktop SPA in a WebView, over a store the client owns. A host serves bundles and brokers credentials and owns no application data. The README says a hosted web runtime with a host-owned replica is refused, and so are third-party installed apps, for now. If your product needs a browser tab as the primary surface, or needs to run apps it did not ship, this is not the substrate for that.
Migration is the other hard boundary. The README states the superseded data stack was deleted before Whispering, vocab, skills and the Epicenter host were migrated, deliberately, so old data is not imported into the new model. There is no compatibility bridge to lean on. A data definition is release-local and never migrates your data, so schema change is your problem to solve at the application level. And the sharing model is thin by design: two devices converge when both are signed in to the same account, and the README describes no pairing, invitation or approval flow, so per-document sharing is not something you can build on top without going outside the documented model.
Matter, Local Books and Local Mail as a different approach
The clearest alternative inside the same repository is Matter. It edits user-owned Markdown folders directly and keeps a disposable matter.sqlite query mirror beside them. The difference in approach is concrete: Matter treats ordinary .md files as the source of truth and rebuilds a SQLite index for queries, while the Epicenter store treats one Yjs document as the source of truth and materializes tables from it. If your users already have a folder of Markdown they expect to keep as files, Matter's model matches that expectation and the store's does not.
Local Books and Local Mail go the other direction. They are headless CLI mirrors that pull a hosted account into local SQLite: a local copy for querying, not a local-first document that converges across devices. All three of these apps do not use the store, so choosing between them is really choosing between three data models, not three front ends. The repository description itself says CRDT-powered tables that materialize to SQLite and markdown, which is the direction the store is heading rather than a description of what Matter does today.
Licence, maintenance and upgrade cost
The README says apps run freely under AGPL-3.0-or-later and links to a What that means section. The repository carries a LICENSE file and a licenses/ directory, and package.json declares "license": "SEE LICENSE IN LICENSE", so the exact terms for packages should be read from those files rather than assumed from the badges. The README's badges label apps as AGPL-3.0 and packages as AGPL-3.0-or-later. If you plan to ship a modified version as a network service, the AGPL is the clause that matters, and that is a question for your own counsel, not for this article.
On maintenance, the last push to the repository was on 2026-09-18, and the repository is not archived. The most recent release listed is v7.11.0, titled Whispering v7.11.0: Local Transcription Expansion + Windows Stability, dated 2025-12-27, with v7.10.0 and v7.9.0 before it in December 2025. Those releases are for Whispering, not for the store, and the README states Whispering now compiles against the store. Upgrade cost is dominated by the deliberate deletion of the superseded data stack: the README does not document an import path from it, so anyone holding data in the old model is starting fresh. The repository uses Changesets (.changeset/ is a top-level entry), which is the mechanism the project uses to record version changes.
Editorial conclusion
Adopt Epicenter if you are building a desktop app whose data must work offline and stay user-owned, and you accept the AGPL-3.0-or-later terms and a single-desktop runtime. Do not adopt it if you need a hosted web runtime with a host-owned replica, third-party installed apps, or automatic migration of data from an older stack: the README says the superseded data stack was deleted and old data is not imported. Verify first that the sync authority you would use is one you control, by reading docs/trust-model.md alongside apps/self-host, and check whether your app's rich content fits a single node on a row.
Frequently asked questions
How do I install Epicenter?
The README does not give a standalone install command for the substrate. The repository is a Bun workspace with bun.lock and bunfig.toml at the top level, and the README points to packages/data/README.md for the data package docs and apps/honeycrisp as the worked example.
How do I set up Epicenter and open a database?
You define a data definition with defineData and defineTable, then call openDatabase, which is the asynchronous boundary; the README's example throws if the returned error is not null. After that, reads such as data.tables.notes.rows are synchronous.
How do I use Epicenter for an app's data?
An app's whole data set is one CRDT document on your machine, complete enough to work with the network off. You create rows through data.tables.<name>.create, read them synchronously, and subscribe to changes, re-reading rows in the callback.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/epicenterhq-epicenter)