Repository navigation
Releases: openlfcp/.github
Release list
OpenLFCP MVP 0.1.0
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.
-...