README.md

August 31, 2026 ยท View on GitHub

Epicenter

Epicenter

Local-first apps over a store you own.

An app's whole data set is one CRDT document on your machine, complete enough to work with the network off. Sign in on a second device and the two converge. No server holds the only copy, and no app owns your storage.

Honeycrisp, a local-first notes app, is the app built on it today.

Run the apps freely under AGPL-3.0-or-later. What that means.

GitHub stars Apps license: AGPL-3.0 Packages license: AGPL-3.0-or-later Discord

The Store | Status | Trust | Repo Map | Development | License


The Store

The hard problem with local-first apps is synchronization. If each device has its own SQLite file, how do you keep them in sync?

Epicenter's answer: 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 nothing is awaited and nothing needs cache invalidation or race protection. The rich half is in there too: a row's node is a nested type on the row, not a second document with an address of its own.

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 is the asynchronous boundary; reads and writes are synchronous.
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 });

const listed = data.tables.notes.rows;             // synchronous flat rows
const stop = data.tables.notes.subscribe(() => { /* re-read rows */ });

A data definition is one application's declaration of its durable data: pure JSON field descriptors, no storage and no lifecycle of its own. It is release-local and never migrates your data. A row it cannot read is reported beside the rows it can, with the reason and the raw values intact, and an ordinary write repairs it.

The node at content merges per character. Declare its codec with content: plainText() and reach it with data.tables.notes.get(note.id)?.content; Epicenter never looks inside. The database document's kv/tables:<name> shape is recorded in ADR-0257 and its collapse to one document in ADR-0295.

Sync is one Cloudflare Durable Object per (account, application). Being signed in on two devices is the entire sharing model: nothing is paired, invited, or approved.

Read the data package docs | What it replaced, and why

Status

There is one runtime: a desktop SPA in a WebView, over a store the client owns. A host serves bundles and brokers credentials; it owns no application data. A hosted web runtime with a host-owned replica is refused, and so are third-party installed apps, for now.

Honeycrisp is the app running on the store, and its README is the worked example.

Whispering, vocab, skills, and the Epicenter host now compile against the store. The superseded data stack was deleted before they were migrated, deliberately, so old data is not imported into the new model.

Matter edits user-owned Markdown folders directly and keeps a disposable matter.sqlite query mirror beside them. Local Books and Local Mail are headless mirrors that pull a hosted account into local SQLite. Those three do not use the store.

Trust Boundaries

Pick the trust model you want.

PathWhat leaves your device
Signed outNothing. The store is complete on the machine it opened on, and every read comes from a document already in memory.
Signed inYour application's document, as opaque update bytes, to one authority per account.
Hosted EpicenterThat authority is ours, along with account and session data and any hosted feature you enable.
Self-hosted instanceYou control the server, secrets, deployment, and infrastructure boundary.
A provider an app callsWhatever that app sends it: transcript text to an LLM, audio to a transcription provider. Epicenter servers are not in that path.

Signed-in sync sends your data to a trusted server that reads it in plaintext. On hosted Epicenter the authority is ours, so that data sits inside our trust boundary; self-hosting puts it on infrastructure you control, so Epicenter never holds it. See the trust model for the details, including where this is heading with the anchor.

Repo Map

Apps

AppStatusNotes
HoneycrispRuns on the storeLocal-first notes. Folders and notes are rows; a note's body is the node on its row.
MatterRuns, separatelyTyped grid over user-owned Markdown folders. It edits ordinary .md files directly; matter.sqlite is a disposable query mirror.
Local Books, Local MailRun, separatelyHeadless CLI mirrors that pull QuickBooks and Gmail into local SQLite.
APIHosted infrastructurePersonal cloud Worker. Owns the store authority binding, hosted-only billing, and the dashboard.
Self-hostReference deployableCommunity-supported single-partition instance without hosted billing.
Whispering, vocab, skills, EpicenterCompileMigrated onto the store.
Other app foldersResearch and prototypesUseful history and experiments, not the current product lineup.

Packages

These packages carry the main architecture.

PackageRoleLicense
@epicenter/dataThe store: one Yjs document per application, a synchronous surface over it, and the transport that carries it.AGPL-3.0-or-later
@epicenter/data/definitionThe inert data-definition vocabulary: JSON field descriptors, row addresses, and nonconformance.AGPL-3.0-or-later
@epicenter/sqliteNeutral embedded-SQLite driver with Browser, Bun, and Durable Object adapters. It owns no product schema.AGPL-3.0-or-later
@epicenter/syncThe WebSocket subprotocol vocabulary both halves of a handshake must agree on.AGPL-3.0-or-later
@epicenter/uiShared Svelte component library used by multiple apps.AGPL-3.0-or-later
@epicenter/serverShared Hono server library composed by the hosted API and the self-host reference deployable.AGPL-3.0-or-later

Architecture

The server side is split into one shared library and two deployable folders:

packages/server
  shared Hono library
  route composition for auth, sessions, store sync, blobs,
  and provider-backed inference and transcription

apps/api
  hosted personal Cloudflare Worker
  composes packages/server with a Better Auth principal resolver
  owns hosted-only dashboard and billing code

apps/self-host
  self-hosted single-partition instance reference deployable
  composes packages/server with the instance principal resolver
  community-supported
  no hosted billing surface

Full architecture walkthrough | Trust model

Development

Use Bun in this repo.

git clone https://github.com/EpicenterHQ/epicenter.git
cd epicenter
bun install

Every app starts from the repo root. bun dev:<app> runs every process the app needs; for apps that talk to the hosted API, that includes the API worker on localhost:8787. bun dev:<app>:ui runs the app's frontend alone when that split exists, and bun dev:api runs just the backend. Bare bun dev is bun dev:honeycrisp, and bun run with no arguments lists every target.

CommandStartsApp port
bun dev:honeycrispAPI + Honeycrisp desktop5175
bun dev:honeycrisp:uiHoneycrisp in the browser, no API or Tauri shell5175
bun dev:apiHosted API worker alone8787
bun dev:api-dashboardAPI + dashboard UI5178
bun dev:landingLanding site, standalone4321
bun dev:matterMatter desktop, standalone5180
bun dev:posthog-reverse-proxyPostHog reverse proxy Workerwrangler default
bun dev:self-hostSelf-host server (needs INSTANCE_TOKEN)8787

bun dev:whispering, bun dev:vocab, bun dev:skills, and bun dev:epicenter still exist, and those apps compile against the store.

The API needs local Postgres and Infisical; see apps/api/README.md. Rust is needed for Tauri apps such as Honeycrisp and Matter. Local Books and Local Mail run their own multi-process dev flows; their READMEs document them.

bun run check is the gate. It runs lint, typecheck, every workspace test, and the structural checks, and it is the same gate CI runs, so a green local run predicts a green pull request. Formatting is handled separately by the autofix workflow.

bun run check

Run the pieces on their own while you work:

bun run format          # rewrite formatting (CI autofixes this for you)
bun run lint:check
bun run typecheck
bun run test
bun run check:structure # doc paths, catalog pins, API paths, licenses, UI boundary, boot purity

Two checks sit outside the gate on purpose. bun run check:doc-hygiene flags specs and ADRs that time has made stale, so it belongs to review rather than to merge. bun run smoke:local boots the API against local services.

Design Notes

Durable decisions and their reasoning live in docs/adr/. Specs in specs/ are in-flight design scaffolding rather than current truth; when a spec and an ADR disagree, the ADR wins. Start with docs/README.md.

Contributing

Contributions are welcome. Good entry points are docs, local-first infrastructure, Svelte interfaces, migrating a broken app onto the store, and small changes that make the repo easier to understand.

Read the Contributing Guide

Contributors coordinate in Discord.

License

Everything is AGPL-3.0-or-later. An MIT toolkit tier existed until 2026-08 and was dissolved; versions already published under MIT stay MIT for those versions.

See the root LICENSE, FINANCIAL_SUSTAINABILITY.md, and the licensing strategy for the full model.


Contact: github@bradenwong.com | Discord | @braden_wong_

Your data outlives the app that wrote it. Local-first, open source, built on Yjs.