crosscode architecture
your checkout teammate's checkout
file edit file edit
| |
daemon (per checkout) daemon
| publish one file |
+------> coordination service <---------+
(Postgres + websocket)
| |
apply / merge / conflict apply / merge / conflict
|
your coding agent <-- MCP tools + pre-edit hook, only on a conflict
four moving parts, and the smallest one is the point.
daemon (apps/daemon)
one daemon per checkout, started by crosscode start, restarted automatically if it
crashes. it watches tracked files (with a hard denylist for .env*, *.pem, *.key, and
similar), debounces about 300ms per file so an edit is sent once it settles, diffs against
the shadow ref, and publishes one file at a time. in the other direction it receives
changes and applies them.
it binds only to loopback and writes a mode-0600 connection descriptor under the
checkout's git directory; the cli and the mcp server connect to that.
the shadow ref. refs/crosscode/shadow points at a commit whose tree is the last state
both sides agreed on. that single ref gives the merge base, the answer to "have i edited
this since we synced?", undo (git checkout refs/crosscode/shadow -- <path>), and content
storage in git's own object store. it never moves HEAD and never shows up in git log.
the sender advances its shadow when it publishes, which is what stops a change being
rebroadcast forever.
apply rule for an incoming change to path P, with L = local disk and S = shadow:
L == Sand the sender built fromS→ write it, silently.- otherwise → 3-way merge against the sender's base, resolved by content hash out of git's object store. clean → write silently. conflict → surface it.
- only if that base blob is genuinely missing → catch up from the cursor, then retry.
all four clauses are load-bearing; the earlier draft of this rule lost data on concurrent
edits, which is why spike/ exists. binary files are never merged, so concurrent binary
edits are always a conflict. a conflicted path is quarantined, neither published nor
applied, until it is resolved.
deferral and pausing. a file the user or their agent touched in the last ~10 seconds is never written to; the write is deferred. syncing pauses entirely during a rebase, merge, or bisect, and resyncs afterwards.
when HEAD moves. switching branch is a different room: the daemon resets the shadow to
the new HEAD, drops what was in flight, and rejoins. a commit or a pull on the same
branch is subtler: the branch is unchanged and no git operation is in progress, but the
tree the shadow points at is no longer what both sides agreed on. the daemon has to notice
and rebase the shadow onto the new HEAD without discarding uncommitted work that is
genuinely local, and without republishing everything each time somebody commits.
what it deliberately does not do is touch your working tree to make git pull succeed.
if a teammate commits and pushes changes you are still holding uncommitted, git refuses the
pull ("your local changes would be overwritten") even though the content is identical.
crosscode could clear that by checking files out for you, and checking files out on your
behalf around a commit is exactly the magic invariant 1 exists to forbid. instead it tells
your agent a pull is waiting, and your agent, which is allowed to run git, clears the way.
coordination service (apps/service)
a store-and-forward relay on supabase-hosted postgres. six tables carry the product:
users, projects, project_members, invites, replicas, file_versions. two more
support it: device_codes holds short-lived pending sign-ins, and terms_acceptances
records which version of the terms and privacy notice each account has accepted. presence
is in-memory in the websocket gateway, not a table.
routes: POST /v1/projects, POST /v1/invites, POST /v1/invites/:code/redeem,
POST /v1/replicas, GET /v1/changes?since=, POST /v1/changes, and the websocket at
/v1/stream. sign-in adds POST /v1/auth/github/device, /device/token and
/device/bind; terms acceptance adds GET /v1/legal, POST /v1/legal/acceptances and
GET /v1/legal/acceptances. the device routes, with GET /health and /healthz, are the
only ones exempt from the bearer check, because their whole job is to serve a caller who
has no session yet. redeeming an
invite verifies the invitee actually has access to the repo, using the invitee's own github
token passed as x-crosscode-github-token. row level security is on from the first
migration; device_codes carries no policy at all, which denies it to every role that does
not bypass rls.
the service assigns sequence numbers and fans changes out. it does not merge, classify, or
inspect anything. about 7 days of changes are replayable (HISTORY_RETENTION_DAYS in
apps/service/src/store.ts); a replica whose cursor is older than that is told to resync
from full content rather than handed a partial history.
a room is one project plus one branch name. different branches do not sync, because different branches mean you did not want to.
clients (apps/cli, apps/mcp-server)
neither holds sync state. both talk to the local daemon's loopback http api.
the cli is five commands: start, invite, join, status, stop.
the mcp server is four tools (status, conflicts, resolve, pause) and one pre-edit
hook, reached as crosscode-mcp hook. every tool response carries any pending conflicts, so
an agent finds out about one on its next call regardless of what that call was for; the hook
only moves that moment earlier, to before a write rather than after. see
mcp-clients.md.
where a conflict goes
nowhere near a crosscode ui, because there isn't one. a conflict is handed to the user's own
coding agent as ours / theirs / ancestor, and the agent merges it and calls resolve.
the crosscode skill is what teaches it to do that quietly.
invariants
- uncommitted working-tree files are the entire scope. commits, branches, the index, and remotes are never touched, and nothing here pushes.
- never write a file the user or their agent touched in the last ~10 seconds.
- tracked files only, minus the secret denylist.
- same project and same branch, or no sync at all.
- everything is undoable with one command.
- stop crosscode and the repository is an ordinary git repository.
see protocol.md for the wire shapes and PLAN.md for what is built so far.
view raw markdown · generated from
docs/architecture.md at build time, do not hand-edit this page.