SSyncropel Docs

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 on 9201 in a separate terminal. Both can live on the same machine; that's what this tutorial uses.
  • Identity initialized on each — spl identity show should report a DID; if not, spl identity generate.
  • spl version reports 0.27.2 or newer (when code-review-pair template + spl federation invite both 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-localhost

In terminal B (bob):

SYNCROPEL_HOME="$HOME/.syncro-bob" \
spl serve --port 9201 --insecure-localhost

Verify each is healthy:

curl -s http://127.0.0.1:9100/health
curl -s http://127.0.0.1:9201/health

Each 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 status

Both 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_done

The 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/syncropel

The 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_inbox

5. 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_done

6. 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

On this page