Repository navigation
OpenLFCP MVP 0.1: release notes
Status: Final. OpenLFCP MVP 0.1.0, released 2026-10-06 (LFCP-072).
What this is
OpenLFCP MVP 0.1 is a secure vertical slice of the Local-First
Collaboration Protocol. Two people share tasks from their Obsidian vaults:
- only the tasks they choose leave the vault;
- the tasks travel as end-to-end encrypted, signed LFCP Data Units through a
self-hosted reference server that never sees their content; - everything else in the notes stays local.
It implements the OpenLFCP MVP 0.1 subset of LFCP-WIRE-01 at
spec: mvp-0.1-baseline.8. It is not a full LFCP-WIRE-01 implementation; the
deferred features are listed in
deferred-wire-01-features.md. The
specifications remain Working Drafts.
MVP reference software: not for data you need to protect.
Components
| Repository | Version | What it provides |
|---|---|---|
spec |
mvp-0.1-baseline.8 (da3977f); 509c1c1 adds the owner's ADR approvals |
LFCP-WIRE-01, SHARED-OBJECTS-PROFILE-01 and MARKDOWN-REFS-01 (Working Drafts); test vectors, the Automerge reference corpus, schemas; ADRs 0001–0007 |
sdk-ts |
npm 0.1.0 (4e1b02f) |
The TypeScript SDK: core, crypto, wire, storage (memory, IndexedDB, Node), Shared Objects on Automerge, the sync client with invitations, Key Packages, Snapshots and epoch rotation |
sdk-rs |
0.1.0 (c9132b1) |
An independent Rust implementation of the protocol core and the Shared Objects profile (feature shared-objects) |
server |
0.1.0 (896bbab) |
The reference LFCP server in Rust: WebSocket sessions, Control Coordinator, durable SQLite store, first-run pairing; never decodes Shared Objects |
obsidian |
0.1.0 (e8e212f) |
The Obsidian plugin: Markdown ↔ Shared Object projection and the collaboration commands |
examples |
0.1.0 (943a6ad) |
lfcp-todo (a headless client) and the cross-language conformance harness |
These are the commits of rc7 (rc7-manifest.json),
and each repository tags its commit v0.1.0; in this repository, v0.1.0
is the commit that records rc7 and completes these notes. rc7 passed all
eight local release gates on its first run
(rc-verification.md). It is rc6 with the versions set
to 0.1.0 and the pins moved to them, and rc6 is green in GitHub CI, the
server's Windows and Docker jobs and the plugin's platform smoke on macOS,
Windows and Linux included.
The sdk-ts packages (@openlfcp/core, crypto, storage, wire,
storage-node, storage-idb, shared-objects, client) are released on
npm as 0.1.0 under the dist-tag latest, published by hand by the owner
from sdk-ts 4e1b02f; next names 0.1.0 too. The release candidate
0.1.0-rc.1 (sdk-ts 98efaab) stays on npm. See
npm-publish-checklist.md.
Highlights
- No plaintext mode. Every change travels as a Data Unit:
- ChaCha20-Poly1305 under a per-actor key derived from the Resource's
DEK; - signed with Ed25519 (strict verification), in canonical COSE.
The server stores and forwards opaque bytes, and tests check that task
text never appears in what it stores.
- ChaCha20-Poly1305 under a per-actor key derived from the Resource's
- Signed Control Plane. Genesis, grants, revocations, Key Epochs and
claims form a linear signed chain. Clients evaluate every capability
themselves, and forks are detected and block security-sensitive actions. - Invitations.
- One-time
lfcp://join/…links with Read or Read + Write. - The joining device claims access through the coordinator and receives
the key by HPKE. - The link is a bearer secret, and the UI never logs or stores it.
- One-time
- Revocation and key rotation. Revoking a reader starts a new Data
Epoch. Writes from beyond the cutoff are quarantined everywhere. - Local-first. Edits work offline and sync when the server is
reachable. Restarts never reuse an actor sequence. - Markdown stays Markdown.
- A shared task is an ordinary task line with an
lfcp-refcomment, on a
child line by default. - Only owned fields sync: title, status, dates, priority, tags.
- Edits change the smallest possible span, line endings are kept, and
conflicts show beside the text, never inside it.
- A shared task is an ordinary task line with an
- Two interoperable SDKs. sdk-ts and sdk-rs pass the official vectors
and exchange fresh objects in every direction. The compatibility matrix
isdocs/conformance/compatibility-matrix.md.
Obsidian commands
Create collaboration · Join collaboration · Share task under cursor ·
Insert shared object · Invite collaborator · Resource status · Detach
shared task · Resolve shared task conflict
Demos
-
Two vaults in real Obsidian (obsidian 3a4f935): Alice and Bob share
one Task between two vaults through the reference server. Walkthrough:
obsidian: docs/demos/two-vault-demo.md.node scripts/demo-vaults.mjs
(0a9ad70) prepares both vaults outside the repository. The same
storyline runs as the two-vault E2E, and prints a narrated reference
transcript with:LFCP_REQUIRE_LIVE=1 LFCP_E2E_NARRATE=1 pnpm vitest run test/e2e/two-vaults.test.ts --reporter=verbose
-
Headless Todo (examples 2ca182f): one command runs the whole
storyline withlfcp-todo, prints a readable transcript and checks the
outcome. Seeexamples: docs/demos/headless-todo.md.
Security notes
- A crafted Automerge change that skipped its actor's next sequence number
could crash an sdk-rs receiver, and corrupt a JS document. It is now
rejected before the engine (Shared Objects §14.1, baseline.6) in both
SDKs. - Automerge input is bounded before the engine sees it, in both SDKs
(Shared Objects §11.1, §11.2, §13.1; baseline.7 and baseline.8):- a change is one uncompressed chunk within exact limits (16,384
values per column, 262,144 predecessor entries, 4 MiB of strings,
1,024 dependencies and other actors), checked from its run headers
without expanding them; writers send raw changes (H1); - a Snapshot is checked against local limits of at least 32 MiB and
262,144 values, its deflated columns inflated under a running cap; - a change naming an actor the document lacks is refused;
- no object of a document is deeper than 256 levels: Automerge JS traps
about 6,500 levels below the root and stops for the whole process; - sdk-rs ca7f7f4, a3c7513, 18fe352, 50cb6bf; sdk-ts b6aa491, 68ce58d,
and b110fdd, which stops a client from retrying content that traps
the engine.
- a change is one uncompressed chunk within exact limits (16,384
- A client keeps its receive limit under its own maximum (at least
8 MiB), whatever the server's READY advertises (M1; sdk-rs b1d08bd,
sdk-ts 9b61644), and asks for at most 256 epochs per
KEY_PACKAGE_GET(sdk-ts 3069c33). - Hardening from the pre-release review:
- a
DATA_HAVEannouncing a huge span of sequences no longer hangs a
client: Have ranges stay intervals end to end (H2; sdk-ts 9d5edf7,
sdk-rs 6aab900); - a received change nesting maps thousands deep no longer overflows the
sdk-rs stack. An object's values nest at most 64 levels; a deeper one
isINVALID_FIELD_TYPE(M7; sdk-rs bb43a78); - merging another replica in sdk-rs goes through the same §14.1
admission and engine guard as received changes (M8; sdk-rs b99f85c).
- a
- Untrusted text is parsed in linear time. A collaborator's long Task title
could freeze the Obsidian editor through super-linear Task-suffix
regexes; the suffix, code spans and comments are now scanned in linear
time (H3, M9; obsidian c73c46d, 0bded76). Writer URLs are checked in
linear time too (sdk-ts af2954d). - Server hardening (the full list is in the security review, "Follow-up:
server hardening"):-
DATA_GETwith more than 256 ranges andKEY_PACKAGE_GETwith more
than 256 distinct epochs are refused (MALFORMED_MESSAGE) before any
lookup. Repeated epochs and overlapping ranges are read once (H4;
server eb1323f). -
A connection that is not
READYwithinhandshake_timeout_ms
(default 10 s) is closed, andPINGdoes not extend the deadline.
BeforeREADY, a connection may send at most 16 messages, none of
them decoded beyond 64 KiB. HTTP headers have a read timeout (M2, M5;
server 65f148d). -
max_connections(default 1024) caps concurrent connections. Past
it, the server answers HTTP 503 withRetry-After(M2; server
625dbd9). -
At most 1024 admin challenges are outstanding, each for 5 minutes;
past that, the server answers 429 (M3; server a7f3461). -
Coordinator slots exist only for hosted Resources (M4; server
11e1fb2). -
A state directory the server creates is owner-only (0700), and the
database files are 0600, tightened at every start (L5; server
83f9a33). An existing directory, such as the Docker volume, keeps its
mode. -
The first-run pairing code is written to
<state_dir>/setup-code
(mode 0600), never to stdout or the Docker logs (M6; server 7e61710).
Under Docker, read it with:docker compose -f deploy/compose.yaml cp lfcp-server:/var/lib/lfcp/setup-code - | tar -xO
-
- The server is a synchronization peer, not the root of trust. Clients
verify signatures, capabilities, epochs and AEAD themselves. - The pre-release security review is
security-review-mvp-0.1.md: findings by
severity, what was fixed, what is routed, and the dependency audit:
pnpm auditandcargo audit(RustSec, 1,290 advisories, 2026-10-06)
find no vulnerability in sdk-ts, sdk-rs, the server or examples, and
one moderate advisory in a development-only typings dependency of the
plugin that is never bundled.
H1–H4 are fixed, and H5 and H6 are accepted as known limitations
(below).
Known limitations
- Not full LFCP-WIRE-01. See
deferred-wire-01-features.md. - The protocol decisions in ADRs 0004 to 0007 were made by the
orchestrator while the owner was away; the project owner approved them
on 2026-10-06. - Desktop platform smoke (LFCP-068): manual run on macOS only. The
automated platform smoke passes on macOS, Windows and Linux. The manual
checklist in real Obsidian passed on macOS on 2026-10-06 (Obsidian
1.14.4, after fix obsidian faaf020; record in
obsidian: docs/devel/testing/platform-smoke-runs.md). The manual
Windows and Linux runs and the cross-OS pair (two operating systems
overwss://) were not run for 0.1.0. - The Docker deployment of the server (LFCP-055) is verified by the server
CI'sdockerjob, which builds the image and runsdeploy/check.sh
(non-root, health,wss://through the Caddy proxy, restart on the same
volume, a destroyed volume), and by hand on macOS with Docker Desktop on
2026-10-06 (deploy/check.shend to end, first-run pairing overhttps://
through Caddy included; server f1fcf29). It has not been verified on a
production host. - Mobile (iOS, Android) is not tested.
- sdk-rs copies a document once per apply call as rollback insurance; a
batch of changes counts once, but a Data Unit applied on its own costs a
copy. This is fine at MVP sizes. - Two different Automerge changes with the same actor and sequence number
are refused (ACTOR_EQUIVOCATION). That happens only after a writer
equivocated or misbehaved. The refusal is safe: nothing is merged and
nothing crashes. But in that rare case one replica stops showing that
collaborator's later edits, with no notice. The project owner decided to
ship this in MVP 0.1 and to adopt "hold and retry" in the next baseline,
tracked as POST-001 in BACKLOG-MVP-0.1.md:
see open-decision-actor-seq-collision.md. - Invitations are copied as links; there is no QR code. A copied link
stays on the system clipboard. - The reference server is for known users. Run it with the allow-list
hosting policy, behind the Caddy proxy (see the security review):- Hosting is open by default, and there are no storage quotas (H5).
- Replies to
DATA_GET,KEY_PACKAGE_GETandCONTROL_GETare built
fully in memory, not streamed. The outbound queue counts messages, not
bytes: up to 256 ×max_message_bytesper connection (H6). - After
READY, a message is decoded in full, up to
max_message_bytes(M5). - There are no rate limits on WebSocket sessions or the admin HTTP API.
The connection cap is global, not per IP; use the proxy's limits. - Admin request bodies (at most 16 KiB) have no read timeout of their
own. - An unauthenticated flood can fill the admin challenge cap and delay an
administrator's login by up to 5 minutes. - Windows: no owner-only state files. On Windows the server does not
tighten the permissions of its state files (server ID, setup code, the
database): they inherit the state directory's ACL, so put the state
directory where only the server's user can read it, such as under that
user's profile. On Unix the directory is created 0700 and the files
0600 (server 83f9a33; Windows start fixed in server 4cfa5f4).
- Decrypted shared tasks are stored unencrypted on each device (IndexedDB
or SQLite), and Obsidian'ssecretStorageis shared by every plugin on
the device.
Upgrading
This is the first release; there is nothing to upgrade from. Pins are
explicit everywhere:
spec.lockin each implementation;sdk-ts.lockin the plugin;sdk-rs.lockin the server;server.lockin the plugin's CI.
A later spec baseline (mvp-0.1-baseline.9 …) is adopted deliberately,
never by moving a tag.