ADR-010: Workspace federation: leve, opt-in, registry-based
Date: 2026-05-24 Status: Accepted Author: Rômulo Febasi
Context
The product premise (README, line 1) is “for the developer who, alone and AI-augmented, juggles more projects than a team used to”. v0.1 ships a perfect single-workspace experience but is silent about the plural: when the same user maintains three or four workspaces (per client, per domain, per audience), there is no way to:
- discover them (“which workspaces exist on this machine?”),
- query across them (“who owns the payments API: in any of my workspaces?”),
- expose them as one surface to the AI assistant via MCP.
syn today behaves like git: command runs in the workspace it can find
walking up from cwd. Outside any workspace, every command but
init/setup errors out with Error::NoWorkspace. That is correct for
the workspace-local use case and wrong for the multi-workspace one.
The user’s proposal was: have syn init register the workspace in a
global SQLite owned by syn, expose CLI + MCP queries over all of
them, and add syn remote add <git-url> for remote integration.
This ADR decomposes that proposal critically and locks in the chosen shape before issues are opened.
Decision
Federation lands in v0.2 as seven layered tickets (SYN-75 … SYN-79
plus SYN-90 … SYN-92 for the identity card). The architecture is
leve por design: the global database stores metadata pointers
to workspaces, never the content of their entities; the identity of
each workspace lives in a versioned SYNAPSE.md in its own root.
F1: Workspace registry (SYN-75)
A SQLite file at the etcetera-resolved global path
(~/Library/Application Support/synapse/workspaces.db on macOS,
$XDG_DATA_HOME/synapse/workspaces.db on Linux) tracks every workspace
the user has ever syn init-ed on this machine.
Schema (single table; small, append-friendly):
CREATE TABLE workspaces (
id TEXT PRIMARY KEY, -- short slug derived from root path
name TEXT NOT NULL, -- display name (defaults to dir basename)
root TEXT NOT NULL UNIQUE, -- absolute, canonicalised path
git_remote TEXT, -- auto-detected from .git/config on register
federated INTEGER NOT NULL DEFAULT 0, -- privacy gate (SYN-79)
last_seen TEXT NOT NULL, -- ISO-8601, refreshed on every syn command
registered_at TEXT NOT NULL,
notes TEXT
) STRICT;
syn init writes the row (idempotent on root). Every command that
runs successfully refreshes last_seen. git_remote is auto-detected
from .git/config on registration (the workspace root is often a git
repo; if not, the column stays NULL).
F2: Federated read commands (SYN-77)
--all flag on read commands (search, info, graph) iterates the
registry, opens each workspace in turn, runs the existing
core::* use case, and merges results. Output always carries the
workspace identifier so the consumer (human or AI) knows where each hit
came from.
$ syn search "api pagamentos" --all
client-alfa/ api/pagamentos API Pagamentos
client-beta/ note/decisao-pgto Decisão de Pagamentos
personal/ (no hits)
Federated queries are read-only. Writes always target the local
workspace (the one Workspace::find resolves from cwd).
F3: MCP federation (SYN-78)
The MCP server exposes three new tools alongside the existing four:
list_workspaces: registry contents.federated_search:search_knowledgeacross opt-in workspaces.get_entity_from(workspace, type, id): read a specific entity from a non-current workspace.
The existing search_knowledge, get_entity, list_by_type,
project_graph continue to act on the current workspace only.
Adding federated_* cousins instead of overloading existing tools
keeps the v0.1 contract intact (BR-11.1).
F4: Git remote awareness (folded into SYN-75)
Captured automatically on registration. No syn remote add in v0.2, that command is reserved for F6 (cross-machine federation), which
stays in the backlog (SYN-210).
F5/F6: Cross-machine sync and remote federation
Kept in the backlog (SYN-209, SYN-210). They require design decisions this ADR explicitly defers:
- Sync model (CRDT? git-driven push/pull? merge conflict handling?)
- Authentication for cloning remote workspaces
- Cache invalidation strategy
- Performance bounds for N-workspace federation across N-machine sync
Privacy gate (SYN-79)
Federation is opt-in per workspace. Default federated = 0 on
register. Activation is explicit:
syn ws include <name> # set federated = 1
syn ws exclude <name> # set federated = 0
This addresses the “client A workspace cannot leak into client B
queries” concern. --all skips workspaces where federated = 0.
F7: Workspace identity card SYNAPSE.md (SYN-90/91/92)
The registry knows that a workspace exists. It does not know what
it is about. Without that, list_workspaces over MCP returns paths
and slugs: useless for routing a question.
Each workspace ships a single identity file at its root:
<workspace>/
├── SYNAPSE.md ← identity card (NEW)
├── .synapse/ ← tool area (existing)
├── projects/ ← entities (existing)
├── people/
└── …
Why root, not .synapse/
.synapse/is the “tool area”: disposable index, config, caches. The manifest is content: versioned, diffed, shared, owned by the human.- A root-level
SYNAPSE.mdmirrors theREADME.mdconvention, obvious, discoverable, hand-editable. .gitignorerules that hide.synapse/(recommended) do not accidentally hide the manifest.
Shape
YAML frontmatter for queryable fields, free Markdown body for the human/AI brief. Same parser, same atomicity, same i18n discipline as every other entity in the codebase:
---
# identity
name: Client Alfa Workspace
description: |
Knowledge base for Cliente Alfa: payments integration, vendor
contracts, team contacts. Focus on B2B payments and webhook reliability.
# categorisation (consumed by MCP list_workspaces routing)
topics: [payments, webhooks, b2b, vendor-management]
tags: [client-alfa, pagamentos]
# stakeholders (referenced as person slugs IF they exist locally)
maintainer: maria-silva
language: pt-BR
# advisory privacy hints: the real privacy gate is registry.federated
visibility: private # private | shared | public
shareable: false
# meta
last_curated: 2026-05-24
---
## Quick brief
This workspace covers the Client Alfa engagement: the payments
integration, the focal contacts, and active migrations. Use this
workspace when asked about Alfa, Acme Corp (the vendor), or webhook
v2 work.
## Out of scope
- Personal projects (those live in `~/personal-brain`)
- Cliente Beta work (separate workspace, `~/work/client-beta`)
Cache, not source of truth
SYNAPSE.md is the source of truth: same rule as BR-1.4 for
entities. The registry caches name, description, topics,
tags, language on:
syn init(initial create + write the file)syn ws refresh [<name>](manual; or auto when the file mtime beats the cachedlast_seen)- the watcher (SYN-65), when it lands
workspaces.db gains columns: description TEXT, topics TEXT
(JSON array), tags TEXT (JSON array), language TEXT,
manifest_updated TEXT. Everything optional, NULL on legacy rows
from before this ticket.
Read and write surfaces
CLI (SYN-91):
syn ws edit [<name>]: openSYNAPSE.mdin$EDITOR. On save, re-cache.syn ws refresh [<name>]: re-readSYNAPSE.md, update cache. No args = refresh all known workspaces.syn ws topic add|rm <topic>: append-only mutation of thetopicslist, atomic viaWorkspace::tx.syn ws tag add|rm <tag>: same fortags.
MCP (SYN-92):
get_workspace_manifest(workspace): returns the full document (frontmatter + body) for an opt-in workspace.add_workspace_topic(workspace, topic): proposes an append. Lands in the pending queue (BR-12.3, SYN-60). Same for tag.list_workspaces(cousin of SYN-78) now returns{id, name, description, topics, tags, language}per row: the routing signal the AI needs.
Why “AI can curate the card”
The AI assistant (interacting with the user across sessions) is in
the unique position to notice what a workspace is actually
becoming about. If, over weeks of Claude Code sessions, an audit
topic emerges in 80% of the conversations rooted on a workspace, the
AI proposes add_workspace_topic(workspace, "audit"). The user
sanctions (BR-12.3) and the next session benefits from the sharper
routing. The manifest evolves with use, not with one-shot curation.
Shareable in the future
Since SYNAPSE.md lives in the workspace folder, it travels with
the directory. When SYN-210 (remote federation via git) lands, a
clone of team/brain-shared.git immediately exposes its manifest, no extra protocol needed. The card is the contract.
Trade-offs
- +
list_workspacesover MCP becomes routing-useful: the AI picks the right workspace before even searching. - + Identity is versioned in git alongside content. Diffs of
SYNAPSE.mdare part of the project history. - + Same parsing/validation discipline as the rest: no new format, no new tooling.
- − One more file per workspace. Mitigated by
syn initbootstrapping it automatically. - − Cache invalidation. Mitigated by mtime check on every
command that touches the registry, plus explicit
syn ws refresh.
Consequences
- + Solves the user-stated problem with a small surface (one table, one flag, three MCP tools, four CLI flags).
- + Preserves BR-1.4 (“nada vive só no índice”): the global DB
has no content of its own, only pointers. Wiping
workspaces.dbis recoverable by re-runningsyn initin each root. - + Preserves the v0.1 CLI/MCP contract: federation lives behind
--alland behind new MCP tools; existing behaviour unchanged. - + Privacy by default: opt-in flag keeps confidential workspaces out of cross-queries.
- − Federated query cost is O(N workspaces). Acceptable for realistic N (≤ ~20); above that needs a persistent index, that’s a separate ticket if/when demand surfaces.
- − Two databases to reason about (
workspaces.db+ each workspace’sindex.db). Mitigated by giving the registry a single, trivial schema. - − Moved-or-deleted workspaces leave stale entries. Handled by
syn ws gc(sweep paths that no longer exist; non-destructive, warns before removing).
Alternatives considered
- Federation pesada (content mirror in global DB). Rejected. Doubles storage, reintroduces sync/consistency between workspace and global, violates BR-1.4, and a deleted workspace orphans its rows. The leve registry has none of these problems and the query-on-the-fly path is fast enough.
- No registry at all; scan
~for.synapse/. Rejected. Slow, fragile (skips workspaces outside~), no place to storefederatedflag orgit_remoteauto-detected. - Federation always-on (no opt-in). Rejected. Clients with
confidential data deserve a default of “do not leak”.
--alldefaulting to “all you opted in” is a safer default than “every workspace I find”: even on a single-user machine. syn remote add <git-url>in v0.2. Rejected. The user proposed it; it’s a great idea, but it pulls in clone caches, auth, sync semantics. Tracked in SYN-210 (backlog) and revisited when local federation has flown in production for a while.