A protocol for trusted clients over storage that does not need to understand their data.
Interocitor is a protocol and client library for trusted endpoints, not a database server. Each endpoint owns its local row state, applies application policy, and merges row changes. The remote mailbox behaves like a hard drive: it stores and returns row artifacts and durable files, but it does not query records, interpret protected contents, or resolve conflicts. A deployment may add a separate server-readable control plane for routing and access policy; that policy does not merge or query protected application records.
With a non-null key source, clients encrypt payloads before the remote receives them. The storage provider can make data available without receiving plaintext. The comparisons below are about that boundary. On the device the posture is the opposite: every key-bearing endpoint is fully trusted, and the local row store is not encrypted at rest. See the security model.
Interocitor is young software under active development. A release may change
the public API, the local-store format, or the remote artifact layout; the
release commit on main says when it does. Pin exact versions and read that
commit before upgrading.
What stands behind it today, and what does not:
- Merge, clock ordering, compaction, and multi-endpoint sync are covered by fixed-scenario browser suites. There is no randomized or property-based convergence testing yet.
- The memory, WebDAV, and S3 adapters run through the full browser sync suite. The Cloudflare adapter is exercised by the worker's own tests, one core capability test, and the shipped example applications, but has no browser-through-worker sync suite. The Google Drive adapter has no automated coverage.
- The cryptography is WebCrypto AES-GCM and ECDSA behind a small wrapper. No independent security audit has been performed.
- Recovery, pairing, and reset paths are tested against the failures the authors thought of. Nothing here has been hardened by a user base yet.
Treat it as something to try, read, and help mature. Do not treat it as the only copy of data you cannot afford to lose.
Interocitor is a small set of guarantees: every trusted device holds a full local copy of the rows, files stay byte-exact and digest-verified, and the remote stores what it cannot read. Those guarantees are enough for a family of products that people already trust today:
| Build something like | Because | Where it stands |
|---|---|---|
| Linear | Speed. Every row is already on the device, so lists, filters, and keyboard-driven edits never wait on a round trip. Scale by giving each team or project its own mesh and keeping its credentials as a connected store in the parent, so a session opens only what it needs. | Behind Linear on workspace-wide search, reporting, and per-issue permissions, which need a server that reads the data. Ahead on offline work and on a vendor that cannot read your issues. |
| Obsidian | The folder tree is rows that merge; note bodies are durable files named by a digest, so structure converges and content stays exact. | Obsidian Sync also encrypts end to end. Here the storage is yours and the vault is a library you embed; note bodies still replace as whole files rather than merging. |
| Signal | The mailbox stores and returns encrypted artifacts. With a mesh key it never receives message plaintext, and object names are keyed hashes. | Less secure than Signal: private chat is more than encryption, and there is no ratchet, no sealed sender, no safety numbers, and no key rotation at all — rotating means creating a new mesh — so a copied key opens the whole history. Signal also encrypts its local database under an OS-keychain key; the local rows here are not encrypted at rest. Ahead of XChat at the remote boundary: nothing the operator holds is readable without the mesh key. |
| Cryptomator | Files are encrypted before the WebDAV, Google Drive, or iCloud folder adapter sees them, on storage the user already pays for and owns. | Equal at the remote boundary, since both encrypt before the folder adapter sees a byte and both leave sizes visible. Opposite on the device: Cryptomator's product is an encrypted local vault, while the local row store here is not encrypted at rest. Behind Cryptomator as a drop-in virtual drive; ahead when the files belong to an application with rows that need to merge. |
| Bitwarden | A portable key, device pairing, and recovery phrases are built in, so a vault syncs across devices without a custodial server. | Bitwarden's server also never sees the vault, and its client vault is encrypted at rest under a master-password KDF and locks when idle; the default here is a local store that is plaintext at rest with no lock concept. It offers emergency access and organization administration that Interocitor leaves to the host to build; in exchange there is no custodial account or server at all. |
| A field-data app | Inspections and surveys are written offline and merged field by field later; two workers on one report keep both sets of edits. | Behind a hosted form platform on server-side dashboards and exports, which need plaintext. Ahead on two workers merging one report and on a remote that never sees the survey. |
| Trusted automation | A worker or agent is just another trusted endpoint. It holds the key, reads task rows, does the work, and writes results back, from a browser, a server process, or a script. | An agent holding the key is as trusted as a person holding it; there is no narrower server-enforced view for it. Behind a server-mediated integration on least privilege, ahead on no server code and no plaintext passing through one. |
| A case-file system | A file sealed under an extra key lives inside a shared mesh: the rows stay shared, only the key holders open the bytes, and the seal guards the object against overwrite or deletion. | Behind a document-management system on server-enforced per-document permissions and read audit. Ahead on a server that cannot read the sealed file and cannot overwrite or delete it without the key. |
The common thread: the users can each hold a full copy, the server should not be able to read it, and there is no server code to write, host, or defend.
It is the wrong tool when the product needs server-side queries or reporting over plaintext, per-row access control inside one dataset, or central transactions. Every endpoint holds the entire mesh it opens, so large or many-audience datasets are split into meshes rather than filtered per row, and the remote can still see sizes, timing, and request identity. See data boundaries before deciding.
Interocitor is one library that can stand in for several tools, because each of them is a local store plus some way to move data. Interocitor keeps the local API you already know and takes the server out of the trust boundary. That is the whole difference, and it cuts both ways.
| Play the role of | How Interocitor plays it | What is different | Reach for the original when |
|---|---|---|---|
| Dexie | A typed table store over IndexedDB with where, subscribe, and useLiveQuery |
Narrower queries; rows merge per field; sync and encryption come with it | You do not need to sync "own" data |
| TanStack DB | Reactive collections and live queries feeding React, with sync built in | No server that understands the schema; the remote is a mailbox; scale by splitting meshes | A trusted backend already owns the data |
| Firebase / Firestore | A multi-device synced store with offline as the default and no backend to write | The remote holds ciphertext; access is per mesh; the worker only admits, meters, and audits | The server must read, query, or report on the data |
| A distributed database | The core in a server process with a memory or custom local store, over WebDAV, S3-compatible storage, or a NAS | Storage cannot read it; clients merge and compact; no server-side queries | You need central transactions or plaintext reporting |
| Automerge / Yjs | Rows and tables instead of one mutable document; each field is the merge unit, each row change is the artifact | No document tree, no in-document history, no text CRDT; queries, indexes, and tombstones instead | Collaborators edit one document, canvas, or text body |
As Dexie. table, where, subscribe, and useLiveQuery will feel
familiar. Queries cover one indexed field with equals, ranges, startsWith,
anyOf, and orderBy. There are no compound or multi-entry indexes, no
collection chaining, no bulk operations, and no versioned migrations. In
exchange rows merge per field with hybrid logical clock ordering, and syncing
to a remote that holds only ciphertext is already there when you want it.
As TanStack DB. You get reactive collections and live queries without a server that owns the canonical data. The client replica is canonical and the remote is a mailbox. Filtering runs on the endpoint, and scale comes from splitting meshes and keeping their credentials as connected stores. The merge and a file surface are part of the library.
As Firebase. You get a store that syncs across devices, works offline by default, and needs no backend code. The remote cannot read it, so there are no server queries and the unit of access is the mesh, not the row. Realtime is an optional invalidation signal that tells clients to pull. Auth is your host's identity provider. The Cloudflare worker sits where rules sit in the request path, but it decides only who may touch a mesh, how many bytes, and what gets logged. It never decides which rows.
As a distributed database. Interocitor is not a browser library. The core runs wherever you give it a local store: memory, or your own, in a server process. Point it at WebDAV, an S3-compatible bucket, or a NAS, and a mesh behaves much like a small distributed database whose storage cannot read it. Trusted workers and agents join as endpoints and share the same rows and files as the browsers.
As Automerge or Yjs. Those libraries replicate one mutable document: a
JSON-like tree or a set of shared types that you change in place and whose
whole history travels with it. Interocitor replicates rows. You do not mutate
a replica; you issue add, patch, replace, and delete against a table,
and each column merges on its own clock. The document boundary that Automerge
asks you to design becomes a table with indexes, where, and subscriptions,
and history is compacted into snapshots instead of kept forever. The trade is
that there is no rich-text or sequence CRDT: a note body is a durable file
that replaces whole, not a document that merges character by character.
Encryption, storage adapters, pairing, and recovery are part of the library
rather than something to assemble around a document store.
On complexity. Interocitor is lower level than any of these, in the way Rust is lower level than a garbage-collected language. You name the key source, which endpoints hold the key, who compacts, which meshes a session opens, and which files carry a seal. That is the price of no server code, no plaintext rows on the remote, and deterministic convergence regardless of arrival order. Most of these choices are deployment policy the runtime cannot verify, with server-managed compaction as the one checked case.
Both sections live on the docs site as Applications.
Interocitor exposes two related surfaces with different availability guarantees:
| Surface | Behavior | Remote storage |
|---|---|---|
| CRDT rows | Reads and writes use a caller-supplied local store. An outbox carries encoded changes when transport is available. | Changes and snapshots are merged and compacted by clients. |
| Durable files | putFile, getFile, openFile, and deleteFile call the remote adapter directly. There is no core file cache or offline queue. |
File bytes stay under a hidden object name until overwritten or deleted. |
With a non-null key source, row payloads and file bytes are encrypted before upload. Storage still observes transport metadata such as object names, sizes, timing, and request identity. See the security model for the complete trust boundary.
There is no export API, no import API, and no migration tool, because there is
nothing to expose that you do not already hold. Rows are yours through
tableNames() and query(). Files are yours through the FileRef columns
those rows carry. Write the loop, shape the output however the destination
wants it, and put a button on it. Import is the same loop backwards.
That is the stance: transform what you want, how you want, with the whole API available and no blessed format in the way. A library-owned exporter would only be our guess at your schema, and it would date faster than your application does.
Two things are worth knowing before you write that loop. query() returns
live rows, so a dump built from it omits tombstones; read from your own
LocalStore reference if a deleted row has to survive the round trip. And a
file is discoverable only through a row that references it, because the remote
object name is a keyed hash of the path and carries no path of its own. Keep
every file referenced by a row and everything stays reachable.
| Package | Start here |
|---|---|
@interocitor/core |
Engine, schemas, adapters, pairing, recovery, and file APIs |
@interocitor/web |
Browser local stores, credential custody, and image helpers |
@interocitor/react |
Context and reactive row/image hooks |
@interocitor/workers |
Cloudflare Worker runtime with D1 plus configurable durable file-body storage |
| InterocitorSwift | Apple-platform runtime with local SQLite rows and Core-compatible encrypted mailbox sync |
interocitor |
Python core for headless workers and protocol integrations |
The browser package is the recommended entry point for browser applications; it supplies the local-store and credential-store implementations used with the core engine.
Use a macOS 14 or later release host with Xcode or Xcode Command Line Tools, Swift 5.10 or later, Python 3.11 or later, and a Playwright-supported even Node.js line: 22.12 or later in the 22.x line, 24.x, or 26.x. If the Node installation does not include Corepack, install it before enabling the repository's pinned Yarn version. Then prepare an isolated Python environment and the Chromium and WebKit binaries used by the browser suites:
if ! command -v corepack >/dev/null 2>&1; then
npm install --global corepack@latest
fi
corepack enable
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e "packages/interocitor-python[test]"
yarn install --immutable
yarn test:e2e:install
yarn preflightyarn preflight owns the JavaScript static, build, unit, package, browser, and
example suites, followed by Python protocol tests and Swift integration plus
Core interoperability. Use yarn validate for the shorter JavaScript gate
while iterating; it still runs the Todo browser suite and therefore requires
the installed Chromium binary.
- Plain-language questions and answers
- Public site architecture and content owners
- Terminology
- Protocol flows
- Core adapter contract
- Read an existing mesh without joining as a device
- Use an S3 mailbox from a browser
- Pair a device
- Publish and use a recovery phrase
- Recovery API reference
- Compaction
- Cloudflare runtime options
- Cloudflare security guardrails
- Cloudflare operations and maintenance
docs/examples/todomvc— up to three local-first clients, inspectable mailbox files, compaction, and a best-effort endpoint diff journal.docs/examples/chat— two-client encrypted chat with an inspectable in-page mailbox and a 15-message application limit.docs/examples/board— two local-first board clients make independent card changes, then exchange encrypted rows.docs/examples/family-locator— protected latest-known location rows with explicit privacy, freshness, and safety limits.examples/todo-webdav— smallest two-tab row-sync demo with an inspectable local mailbox.examples/biometric-keys— browser credential custody with WebAuthn.examples/todo-cloudflare-do— Cloudflare Worker, D1, R2, and optional realtime invalidation.
Niki Tonsky’s “Local, first, forever” independently explored the same broad idea: let CRDT data travel through commodity file-sync storage—“just bring your own cloud.” Interocitor was developed independently and turns that pattern into an application data layer with structured rows, durable files, pairing, recovery, and compaction.
Run the mailbox backend on Cloudflare, let users connect Google Drive, point WebDAV at a home NAS, or connect directly to an S3-compatible bucket. Each whole-mailbox backend carries the same artifacts; with a non-null key source, protected payloads are encrypted before the storage adapter receives them.
MIT