Build a multi-user code review flow
Two instances paired in one command, code-review records flowing both ways consent-gated. The complete two-person federation walkthrough.
By the end of this tutorial, two instances — call them alice and bob — will be paired, share a workspace, and exchange code-review records over a single federation link. Alice opens a review request; Bob publishes a verdict; both surface in a shared three-pane view.
This is the smallest end-to-end multi-user flow Syncropel supports.
It uses the composed spl federation invite command shipped in the
standard binary — no extra setup, the records and threads are created
as the flow runs.
Allow ~15-20 minutes for the happy path.
What you'll build
┌──────────────────────────┐ ┌──────────────────────────┐
│ alice (submitter) │ │ bob (reviewer) │
│ │ │ │
│ th_review_inbox ──────────────────────────► th_review_inbox │
│ (request published) │ │ (request appears) │
│ │ │ │
│ th_review_done ◄─────────────────────────── th_review_done │
│ (verdict appears) │ │ (verdict published) │
└──────────────────────────┘ └──────────────────────────┘Three threads (th_review_inbox, th_review_active,
th_review_done) flow over a single federation pair, gated by
consent on a code namespace.
Prerequisites
- Two instances. The simplest setup: one running on the default port
(
9100) and a second on9201in a separate terminal. Both can live on the same machine; that's what this tutorial uses. - Identity initialized on each —
spl identity showshould report a DID; if not,spl identity generate. spl versionreports0.27.2or newer (whencode-review-pairtemplate +spl federation inviteboth ship).
For convenience this tutorial gives each instance its own data
directory and uses --insecure-localhost so they can pair without
TLS certificates. For real cross-host setups, both instances should
serve TLS instead.
1. Boot two instances
In terminal A (alice):
SYNCROPEL_HOME="$HOME/.syncro-alice" \
spl serve --port 9100 --insecure-localhostIn terminal B (bob):
SYNCROPEL_HOME="$HOME/.syncro-bob" \
spl serve --port 9201 --insecure-localhostVerify each is healthy:
curl -s http://127.0.0.1:9100/health
curl -s http://127.0.0.1:9201/healthEach should return a JSON {"status": "ok", ...}.
2. Confirm both instances are ready
There is nothing to scaffold. The code-review flow runs on records and threads directly — the threads are created as records flow into them. A workspace is only the optional surface you would install to view those records side by side; see Authoring a workspace if you want to build that view afterwards.
In a third terminal, confirm each instance is reachable:
SPL_SERVE_URL=http://127.0.0.1:9100 spl status
SPL_SERVE_URL=http://127.0.0.1:9201 spl statusBoth should report a healthy instance. You now have two instances ready to be paired.
3. Pair the instances
Alice initiates the pair:
SPL_SERVE_URL=http://127.0.0.1:9100 \
spl federation invite http://127.0.0.1:9201 \
--grant-namespace code \
--grant-thread th_review_inbox \
--grant-thread th_review_active \
--grant-thread th_review_doneThe first time, the handshake will pause for actor-in-the-loop approval on bob's side. Switch to bob's terminal and approve:
SPL_SERVE_URL=http://127.0.0.1:9201 \
spl aitl list
# copy the pending request ID
SPL_SERVE_URL=http://127.0.0.1:9201 \
spl aitl approve <id>Back in alice's terminal, the invite command finishes:
Invited did:sync:instance:bob-...
Pair ID: fed_<sha256>
Peer DID: did:sync:instance:bob-...
Consent grants: 1
- code cg_<id>
Thread allows: 3
- th_review_inbox
- th_review_active
- th_review_done
Send the recipient this URL so they can rediscover the pair:
http://127.0.0.1:9201/.well-known/syncropelThe pair is live. From now on spl sync consults the pair store
automatically.
4. Open a review request (alice)
Alice publishes a code_review.request.v1 record on the inbox
thread. For records with custom body shapes (anything beyond the
built-in intend/know/do/learn shortcuts), POST /v1/records
on the instance is the canonical surface:
ALICE_DID=$(curl -s http://127.0.0.1:9100/.well-known/syncropel | jq -r .did)
curl -s -X POST http://127.0.0.1:9100/v1/records \
-H 'Content-Type: application/json' \
-d "{
\"thread\": \"th_review_inbox\",
\"actor\": \"$ALICE_DID\",
\"act\": \"INTEND\",
\"data_type\": \"SCALAR\",
\"parents\": [],
\"clock\": 0,
\"body\": {
\"kind\": \"code_review.request.v1\",
\"_v\": 1,
\"title\": \"Refactor auth middleware\",
\"branch\": \"feature/auth-rewrite\",
\"summary\": \"Migrate session tokens to encrypted-at-rest storage.\",
\"diff_url\": \"https://example.com/pulls/42\"
}
}"The instance assigns the canonical record id (SHA-256 of the
content-addressed fields) and returns it. Setting the right clock
manually is fine for a tutorial; in real adapters the SDKs handle
clock sequencing for you (see TypeScript
SDK or Python
SDK).
To get the record to bob's side, run a sync pull:
SPL_SERVE_URL=http://127.0.0.1:9201 \
spl sync th_review_inbox from did:sync:instance:alice-...(Substitute alice's DID — spl identity show on alice's instance
prints it. You can also derive it from the invite output's pair
record on bob's side via spl federation list.)
After the pull completes, bob's inbox thread carries alice's request:
SPL_SERVE_URL=http://127.0.0.1:9201 \
spl thread records th_review_inbox5. Publish a verdict (bob)
Bob reviews the request (in real life: looks at the diff, makes a
decision) and publishes a code_review.verdict.v1 record:
BOB_DID=$(curl -s http://127.0.0.1:9201/.well-known/syncropel | jq -r .did)
curl -s -X POST http://127.0.0.1:9201/v1/records \
-H 'Content-Type: application/json' \
-d "{
\"thread\": \"th_review_done\",
\"actor\": \"$BOB_DID\",
\"act\": \"KNOW\",
\"data_type\": \"SCALAR\",
\"parents\": [],
\"clock\": 0,
\"body\": {
\"kind\": \"code_review.verdict.v1\",
\"_v\": 1,
\"request_title\": \"Refactor auth middleware\",
\"verdict\": \"approved\",
\"comments\": \"Migration plan looks safe. Two minor nits inline.\"
}
}"To get the verdict back to alice:
SPL_SERVE_URL=http://127.0.0.1:9100 \
spl sync th_review_done from did:sync:instance:bob-...Alice's th_review_done now carries the verdict:
SPL_SERVE_URL=http://127.0.0.1:9100 \
spl thread records th_review_done6. Render in Studio (optional)
The code-review-pair template ships with a three-pane view
(Inbox / In review / Verdicts). To see it rendered, point a
browser at each instance's Studio surface (the path depends on your
deployment — typically /local against the local instance).
The Inbox pane folds code_review.request.v1 records. The In
review pane folds code_review.assignment.v1 (a record bob would
emit when claiming a request). The Verdicts pane folds
code_review.verdict.v1.
What you just demonstrated
- A code-review flow that's multi-user by design — two roles, two instances, distinct publishing patterns on each side.
- A federation pair established with one composed command, including consent and thread allowlists.
- Records flowing in both directions consent-gated. Alice's request reaches bob; bob's verdict reaches alice; nothing else crosses unless you grant it.
- A pattern anyone can repeat to start the same flow with their own collaborators — and a flow you can wrap in an installed workspace to view both sides side by side.
Where to go next
- Pair two stewards in one command —
what
invitecomposes on top of. - Invite another steward (composed) —
full reference for the
invitearguments. - Consent management — what hash levels mean and how the consent filter chooses between competing grants.
- Async federation — how syncs work in the background once a pair is established.
Your first crystallized pattern
Do the same thing three times, watch the pattern detector form a hash chain, see the trust score evolve, and observe automatic routing kick in. Syncropel's cost curve bends here.
Parallel Dev — Monday Morning Walkthrough
A 20-minute hands-on walkthrough of the fleet workflow — start a 3-instance fleet, fan out a real task to two workers, handle a mid-run failure with the kill switch, watch the barrier join, tear down cleanly.