Revised direction (supersedes earlier any-sync-shim plan).
We will not try to embed Anytype on top of GrooveGo. Anytype's client is not designed to be a thin/pluggable frontend and fighting it is a dead end. Instead:
GrooveGo kernel ← identity + membership + replication + trust
↓
Object Graph Layer ← Anytype-inspired: objects, types, relations
↓
UI (web / desktop) ← our own, thin, built on the layer above
The AnyType-VPS/ folder stays in the repo as a reference implementation of the UX we're targeting and as the deployment playbook we'll gradually replace. It is not the product.
The previous version proposed shimming the any-sync wire protocol and keeping the Anytype client untouched. Three problems:
- Anytype is tightly coupled to any-sync internals. It is not a thin client. Wire-protocol drift between client versions would break us on every Anytype release.
- GrooveGo's substrate isn't ready to be anyone's backbone yet. Today it only has a libp2p device identity (peer.ID = pubkey hash), a string-keyed workspace list with no signed membership, no CRDT module in
internal/sync/, no per-object scoping. Putting a shim on top of that hides the real gap instead of closing it. - The ceiling is low. Even if the shim worked, the result is "self-hosted Anytype." The higher-value target is "a semantically rich shared knowledge graph with real trust guarantees" — which requires us to own the kernel and the graph layer.
These are decided before any kernel code is written. They are the points the plan previously left under-specified, and getting any of them wrong later is expensive.
Device-first internally, user-first externally.
- Every signed op on the wire carries a device signature (low-ceremony, fast, no hardware/passphrase each write).
- A user identity is a root signing key that signs device authorization certs. Cert =
{userID, deviceID, capabilities, notBefore, notAfter}, signed by the user's current root key. - When verifying an op, peers check
device-sig(op)ANDcert(device) ← user-sig. The membership log stores users, not devices; devices are emergent delegates underneath. - Consequence: rotating a device key doesn't touch membership; rotating a user key rewrites device certs and gossips a rotation record. Revoking a device revokes one cert. Revoking a user is a membership op.
Event log primary, state snapshot is a derived cache. History is never the optional thing.
- Ops:
Invite,Accept,Remove,ChangeRole,RotateWorkspaceKey. Every op is signed and causally ordered by vector clock. - Authority rules (concrete, not "role-based" hand-waving):
admincan issueInvite,Remove,ChangeRole,RotateWorkspaceKey.membercan issueAccept(of an invite addressed to them) and ops against objects they authored.- Founder of a workspace is the initial
admin; admin count can never drop to zero (last admin cannot self-remove without promoting).
- An op is valid iff the signer had the required role at the op's causal frontier. This is replayable: given the log prefix, validity is a pure function.
- State snapshot (
{userID → role}) is computed by replaying the log; it is cache-only and can be thrown away.
Objects are workspace-scoped. Object IDs are globally unique but logically owned by exactly one workspace.
ObjectID = UUIDv7— globally unique so that future cross-workspace references don't collide, but each object has a single owner workspace that holds its op log.- An object cannot be edited from two workspaces. References from another workspace are read-only links (pinned to
{objectID, minVersion}), not co-ownership. - Cross-workspace / "global" objects are explicitly out of scope until after Phase 6. Their complexity (dedupe, multi-log conflict, shared membership) is deferred; the plan works without them.
Aligns encryption with membership — which was Groove's real strength, not the CRDT itself.
- Workspace-log encryption: every op is sealed with a symmetric workspace key before it hits GossipSub. Non-members see ciphertext only.
- Workspace key rotation is mandatory on
Removeand on any key-compromise event. Rotation op emits a new key wrapped to each remaining member's user key (X25519 sealed box). Ops authored before rotation remain readable (old key is retained for replay); ops after rotation are only readable by the new-key set. - Per-object wrapping (optional): sensitive objects carry a per-object symkey, itself wrapped to the workspace key. Allows "sensitive subsets" without a second workspace.
- At-rest: Badger store is encrypted with a device-local key derived from OS keychain / passphrase. Workspace keys are never written in plaintext.
- Key material is owned by
internal/identity/andinternal/membership/; no other module holds a plaintext key.
Every op must be replayable from an empty node and produce identical state.
This is the one sentence the whole kernel is designed around. It forces:
- no wall-clock timestamps in merge logic (use vector clocks + content hashes for tie-breaks),
- no hidden state outside the op log,
- no op whose effect depends on the order of receipt (only on causal order),
- deterministic iteration over sets (sorted by stable IDs before emitting derived ops).
Every PR against the kernel is checked against this invariant. A failing replay test is a blocker.
Hand-rolled signed op-based CRDT for the membership log; Automerge for object content.
- Membership log is small, security-critical, audit-heavy, and has explicit authority rules ("admin at causal frontier"). Off-the-shelf CRDTs are permissionless-merge by design — the validity check would have to wrap the CRDT, which fights the abstraction. Hand-rolled = the merge function is the rule set.
- Object content (text, block trees, lists) is large, free-form, high-conflict, and benefits from a mature CRDT.
automerge-gocarries that load. - Cost of hand-rolling membership: ~1–2 weeks of code + tests. Mitigated by the small surface (≈500 LOC), exhaustive testability, and the fact that signed-op-on-causal-DAG is well-trodden territory (Matrix room state, Keybase sigchains, Scuttlebutt feeds).
- We are not writing an "ad-hoc" CRDT. The pattern is: signed ops, vector-clock causal order, deterministic tie-breaks by content hash, validity as a pure function of
(log_prefix, op).
Canonical JSON (RFC 8785 / JCS) for all kernel ops; protobuf is rejected.
- Choice favors transparency, debugging, and no codegen step over compactness.
- Implementation requirement: Go's stdlib
encoding/jsonis not byte-stable enough for hash-and-sign. Map keys are sorted, but number formatting, Unicode escapes, and trailing-zero handling diverge from RFC 8785. Use a JCS implementation (e.g.github.com/gibson042/canonicaljson-go) or implement RFC 8785 ourselves. Any code path that produces bytes-to-be-signed must go through the canonicalizer; bytes-to-be-displayed may use stdlib. - Determinism gate:
canonicalize(unmarshal(canonicalize(x))) == canonicalize(x)— round-trip stable. The replay test suite (Phase 4) checks this for every op kind. - Files larger than ~64 KB do not go inline in JSON ops; they live as content-addressed blobs (
/groove/files/1.0.0) and the op carries the hash only.
Non-negotiable for anything else to be safe to build on. These restate the requirements §2 decisions are answering.
Current state: libp2p device identity only (Ed25519 per host, peer.ID = pubkey hash). Must add:
- User identity distinct from device identity (a user = a root signing key + a display name + metadata object).
- Device identity bound to a user via a signed device-authorization certificate.
- Signing-key rotation — new key signs a rotation record that chains to the previous key.
- Revocation — signed revocation records gossiped on workspace topics; receivers update their trust view.
Current state: workspace.Manager stores {name → Workspace} with no signed ACL, no join/leave history.
Must add:
- Workspace membership list as a signed CRDT state (set of {userID, role, addedBy, addedAt}).
- Signed membership changes — every add/remove/role-change is a signed op by an authorized member, verifiable by everyone.
- History of joins/leaves — append-only, auditable. A removed member's past ops remain valid up to their removal timestamp.
Current state: no CRDT, internal/sync/ is empty; replication model not yet chosen.
Must add:
- Per-workspace logs (one op log per workspace, not a global log). One GossipSub topic per workspace.
- Per-object scoping — every op names the object it touches; replication filters can subscribe to object subsets.
- Clear replication boundaries — which peers hold which logs is explicit and driven by membership, not best-effort gossip.
- Per-object encryption (optional) — workspace symmetric key for the common case; per-object keys for sensitive subsets.
Must add:
- Stable IDs — UUIDv7 (or ULID) for objects; IDs never reused, never reassigned.
- Versioning — each object carries a vector-clock version; references pin to
{objectID, minVersion}so links don't half-break offline. - Predictable conflict rules — documented per-CRDT-type merge semantics (LWW-register, OR-set, RGA for text, etc.). No ad-hoc resolution.
- Schema discipline — object-type schemas are themselves versioned CRDT objects; schema changes are backward-compatible or bumped explicitly.
┌──────────────── UI layer (later — our own, thin) ─────────────────┐
│ Web (progressive) and desktop shell. No Anytype client. │
└──────────────────────────────┬────────────────────────────────────┘
▼
┌──────────────── Object Graph Layer (Anytype-inspired) ────────────┐
│ objects · types · relations · blocks · views │
│ thin — translates UI intents to kernel ops │
└──────────────────────────────┬────────────────────────────────────┘
▼
┌──────────────── GrooveGo kernel ──────────────────────────────────┐
│ identity/ user + device keys, rotation, revocation │
│ membership/ signed ACL CRDT, join/leave history │
│ workspace/ per-workspace log, symmetric key, lifecycle │
│ objects/ per-object op log, stable IDs, versioning │
│ sync/ CRDT engine (Automerge or hand-rolled), merge rules │
│ store/ Badger — ops, blobs, vector clocks │
│ transport/ libp2p host + GossipSub per workspace │
│ trust/ peer / org / federation graph (later phases) │
└───────────────────────────────────────────────────────────────────┘
Each phase leaves the system runnable and testable. Phases 1–4 are the kernel the user's feedback calls out as mandatory; 5–7 sit on top.
- Audit current
internal/node|workspace|store|sync|transport|presence. ProduceKERNEL-GAP.md: for each of the four requirements in §3, list what exists, what's missing, what's wrong. - Pick CRDT foundation: Automerge-go vs. hand-rolled per-object CRDTs. Recommendation: hand-rolled for the membership log (small, deterministic, auditable) + Automerge for rich object content (text, block trees).
- Write
KERNEL-SPEC.mdwith the §2 decisions frozen and the concrete record formats:- Identity:
User,Device,DeviceCert,KeyRotation,Revocation(fields, signatures, byte layout). - Membership: op wire format for
Invite/Accept/Remove/ChangeRole/RotateWorkspaceKey; authority-check pseudocode. - Objects: op envelope
{wsID, objectID, version, schemaVersion, opBody, deviceSig, userCertRef}; ciphertext framing. - Encryption: workspace key wrap format (sealed-box layout), per-object wrap format, rotation op payload.
- Identity:
- Pressure-test the spec with scenarios before freezing it. Walk through each on paper and confirm determinism and correctness:
- Device-less user joins from a new laptop (user key exists, no device cert yet).
- Admin Alice invites Bob while offline; Bob accepts; Alice and Carol reconcile on reconnect.
- Two admins concurrently remove the same member; vector clocks converge to a single
Remove. - Admin removes Bob; Bob (offline) authored an op just before removal; replayed op must be accepted, Bob's ops after removal must be rejected by all peers.
- User rotates root key; old device certs must remain verifiable for past ops but not accept new ones.
- Workspace key rotation on
Remove— removed member's retained copy of old ciphertext still decrypts; new ops don't. - Replay test: wipe a peer, re-sync from others, derived state (membership snapshot, object tree) byte-identical to the original.
- Only freeze
KERNEL-SPEC.mdafter all seven scenarios pass on paper.
Delivers requirement 3.1, implementing decision §2.0.1.
internal/identity/:User,Device,DeviceCert,KeyRotation,Revocation.- Local keystore in Badger, encrypted at rest with a device-local key (OS keychain / passphrase) per §2.0.4.
- CLI:
groove id init,groove id add-device,groove id rotate,groove id revoke <device>. - libp2p peer.ID remains the transport identifier; every kernel op carries
device-sig(op)+ a reference to the signer's currentDeviceCert. Verifiers check both. - Tests (all must pass the replay invariant §2.0.5): rotation survives a round trip; revoked device's future ops are rejected; past ops remain valid; two peers replaying the same identity log arrive at byte-identical state.
Delivers requirement 3.2, implementing decision §2.0.2.
internal/membership/: signed event log is primary storage; state snapshot{userID → role}is a rebuildable cache.- Ops:
Invite,Accept,Remove,ChangeRole,RotateWorkspaceKey. Every op signed and vector-clock ordered. Validity is a pure function of(log prefix, op)— no wall-clock reads. - Authority rules per §2.0.2; last-admin self-removal is blocked; two concurrent
Removes of the same user converge to one. - Rewrite
internal/workspace/manager.go: workspaces keyed by content-addressed workspace ID (hash of genesis op), not a string. Carrymembership.Log+ currentworkspaceKey. - Join flow: invite = signed capability token; joiner presents it; host verifies, appends
Accept, hands back the current workspace key wrapped to the joiner's user key. - Tests: membership history replays deterministically across peers (byte-identical state from empty); forged op is rejected by all peers; scenarios 2–4 from Phase 0 pass end-to-end.
Delivers requirements 3.3 and the encryption model §2.0.4. Implements decision §2.0.3.
- Per-workspace GossipSub topic
/groove/ws/<wsID>/ops/1.0.0. No global op log. - Per-object op log inside a workspace — ops tagged
{wsID, objectID, version}.objectIDis UUIDv7, globally unique, owned by exactly one workspace (§2.0.3). Cross-workspace references are read-only links and deferred. - Every op sealed with the workspace key before gossiping — no plaintext ever leaves the process.
RotateWorkspaceKey(introduced in Phase 2) is wired here: rotation emits a new key wrapped per-member (X25519 sealed box). Old key retained for replay of pre-rotation ops; new ops encrypted with new key only.- Per-object wrapping supported for sensitive subsets (per-object symkey wrapped to workspace key).
- Tests: non-member receives ciphertext only; removed member cannot decrypt post-removal ops; pre-removal ops remain readable by replay; key rotation is deterministic (same log → same current key on every peer).
Delivers requirement 3.4 and enforces §2.0.5.
internal/objects/:ObjectID(UUIDv7),Version(vector clock),Ref({objectID, minVersion}),Op(envelope from Phase 0).internal/sync/: Automerge for object content; hand-rolled CRDT for object metadata (type, relations). Merge rules per field kind documented inKERNEL-SPEC.md. No wall-clock inputs anywhere in merge.- Schema registry: object-type schemas are themselves workspace objects. Ops reference
schemaVersion; validation is a pure function of(schema, op). - Replay test suite is the gate for this phase: for every kernel op kind, wipe a peer, re-sync from the log, assert byte-identical derived state (membership snapshot, object tree, schema registry, workspace key). A failing replay is a blocker per §2.0.5.
- Tests: disjoint offline edits → commutative & idempotent merge; reference survives rename offline; schema bump with new required field doesn't invalidate old objects; scenario 7 from Phase 0 passes end-to-end.
→ Checkpoint. After Phase 4, GrooveGo is a real Groove-like kernel. Everything above this line was the user's feedback's "must have." Nothing below is safe to build without it.
pkg/graph/:Object,Type,Relation,Block,View— thin Go types backed by kernel CRDTs. No UI here.- JSON-RPC (or gRPC) local daemon API so any frontend can drive it:
ObjectCreate,ObjectUpdate,ObjectQuery,RelationSet,TypeDefine,ViewRender. - Reference tools ship as object types:
Note,Task,Channel(chat),Poll,Whiteboard.
- Web UI first (progressive, works in any browser, thin). Desktop wraps it via Wails / Tauri-equivalent later.
- UI speaks only the Phase-5 daemon API. No Anytype client.
- Presence, chat, and calls reuse existing
internal/presence/and GossipSub side-topics.
- Import from an Anytype self-host: read MongoDB object trees, replay as kernel ops into a workspace. This is the useful direction.
- Export to Anytype: one-way dump in Anytype's space format for users who want out.
- No live shim, no wire-protocol emulation. If a user wants Anytype, they run Anytype.
Now a natural extension of Phase 1 once identity is real:
internal/trust/: signed peer endorsements, orgs (named peer sets with rotating signing keys), federation links.- Surfaced as first-class object types in the graph layer.
GrooveGO/groove-go/
├── internal/
│ ├── identity/ # NEW Phase 1
│ ├── membership/ # NEW Phase 2
│ ├── workspace/ # REWRITE Phase 2 (content-addressed IDs, ACL-backed)
│ ├── objects/ # NEW Phase 4
│ ├── sync/ # FILL IN Phase 4 (currently empty)
│ ├── store/ # extend for identity + membership + objects
│ ├── transport/ # per-workspace topics Phase 3
│ ├── trust/ # NEW Phase 8
│ ├── node | presence | apps | web # existing
├── pkg/
│ ├── protocol/ # wire types Phase 0 spec
│ └── graph/ # NEW Phase 5 object-graph API
└── cmd/groove/ # CLI — new subcommands per phase
AnyType-VPS/ # reference implementation + deployment playbook
├── GROOVE-BACKEND-PLAN.md # this file
├── anytype-*.md # kept as reference, not the product
- Can GrooveGo be used? Yes, as a foundation. Not today as a drop-in backbone.
- Concrete next step: Phase 0 —
KERNEL-GAP.md, thenKERNEL-SPEC.mdwith the §2 decisions frozen, then the seven scenario walk-throughs. No code until all seven pass on paper. - The payoff: if Phases 0–4 land, the result is a deterministic, identity-aware, membership-scoped replicated log system. At that point the object-graph layer is almost easy, the UI is a projection (not a dependency), and chat / workflows / knowledge graph all ride the same substrate.
Do Phase 0 now. Deliverables, in order:
GrooveGO/groove-go/KERNEL-GAP.md— current state vs. the four requirements in §3.GrooveGO/groove-go/KERNEL-SPEC.md— §2 decisions frozen, record formats defined, authority rules in pseudocode, encryption framing, determinism invariant at the top.- Seven scenario walk-throughs against the draft spec. Any scenario that doesn't resolve cleanly sends us back to #2. Only then does Phase 1 start.