Your First FCC Multi-User Session¶
v1.5.0 added Pillar D: CRDT multi-user collaboration — real-time editing with conflict-free merges. That sounds intimidating, but you can stand up a two-user session, observe a live merge, and read the resulting Lamport clocks in under 20 minutes without any network setup and without installing any extra packages. This page walks you through exactly that, at a hello-world level.
If you have completed Hello FCC and Quickstart: First Simulation, you are ready for this one.
What you will build¶
A single Python session that:
- Starts a
MultiUserSessionwith a fresh collaborative document. - Adds two simulated users (Alice and Bob).
- Lets each user apply a concurrent edit.
- Watches the CRDT backend merge the two edits.
- Prints the final document text and the Lamport-clock history.
No network, no WebSocket bridge, no React frontend. Just Python. The full v1.5.0 Pillar D stack supports all of those, but we are starting at the plain-old-Python layer.
Prerequisites¶
You need:
- Python 3.10 or newer.
- FCC v1.5.x installed in a virtual environment.
- A text editor that does not auto-correct code.
- About 20 minutes.
You do NOT need:
- The optional
y_pypackage. FCC ships a pure-Python in-memory CRDT backend that runs everywhere. Wheny_pyis installed, FCC automatically uses it; when it is not, the in-memory fallback takes over. For this tutorial, we let the fallback handle things.
If you haven't installed FCC yet:
git clone https://github.com/rollingthunderfourtytwo-afk/l2_fcc_agent_team_ext.git
cd l2_fcc_agent_team_ext
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
Verify the installation:
Step 1 — Create a session¶
Open a Python interpreter. We'll build the session piece by piece.
from fcc.collaboration.multi_user import MultiUserSession
session = MultiUserSession(session_id="first-session")
print(session.session_id)
# -> first-session
print(session.users)
# -> frozenset()
A session has a stable session_id, a set of connected users (empty
for now), and an internal CollaborativeDocument built on whichever
CRDT backend is available. Because we didn't pass a document
argument, FCC created one for us.
Inspect the CRDT backend:
FCC chose either the y_py backend (if installed) or the
InMemoryBackend (if not). Both implement the same protocol; your
code doesn't need to care which.
Step 2 — Add two simulated users¶
A real deployment would have a WebSocket bridge push users into the session when they connect. Here we skip the bridge and add users directly.
alice_presence = session.add_user("alice")
bob_presence = session.add_user("bob")
print(session.users)
# -> frozenset({'alice', 'bob'})
Each add_user call returns a PresenceInfo object — a snapshot of
where the user's cursor is and what they have selected. For a new
user, the cursor starts at position 0 and nothing is selected.
print(alice_presence.cursor_pos) # -> 0
print(alice_presence.selection) # -> None
print(alice_presence.last_seen) # -> 1712779280.123 (Unix time)
Step 3 — Apply concurrent edits¶
Now for the interesting part. Both users edit the document at the
same time. In a real CRDT system the edits would be independent;
here we simulate that by calling apply_local_edit twice in sequence
but treating them as if they arrived concurrently.
from fcc.collaboration.multi_user import Edit, EditKind
alice_edit = Edit(kind=EditKind.SET_TEXT, value="Alice was here. ")
bob_edit = Edit(kind=EditKind.SET_TEXT, value="Bob was here too. ")
alice_update = session.apply_local_edit("alice", alice_edit)
bob_update = session.apply_local_edit("bob", bob_edit)
Each call returns the CRDT update blob as raw bytes. In a networked deployment, these blobs are what the WebSocket bridge broadcasts to other connected clients. Here we just hold onto them.
Step 4 — Observe the merge¶
The CRDT backend has already merged the two edits. Look at the current document text:
With the in-memory backend, you will see a Last-Writer-Wins per position result. Alice's edit and Bob's edit target the same position (the start of the document), so one of them will have landed — but deterministically, based on the Lamport-clock tie-break.
If you installed y_py, you'd see a different merge because Yjs
preserves both edits at different positions. Both are correct
CRDT behaviour — they just use different merge semantics.
Step 5 — Read the Lamport clocks¶
A Lamport clock is a counter that increments on every local event. When two users edit concurrently, the clocks let you reason about which event "happened before" which.
for op in session.crdt_doc.backend.operations():
print(f"lamport={op.lamport} replica={op.replica_id[:8]} value={op.value!r}")
You will see two lines, something like:
lamport=1 replica=a3f2b1c9 value='Alice was here. '
lamport=2 replica=7d8e4f6a value='Bob was here too. '
Each operation carries:
lamport— the monotonic Lamport counter. Higher means later.replica_id— a stable UUID identifying the replica that generated the operation. Used to break ties when two operations share a Lamport tick.value— the actual edit payload.
If two operations share a Lamport tick (which happens in offline
scenarios), the replica_id tie-break chooses deterministically.
This guarantees that every replica converges to the same state
regardless of network ordering.
Step 6 — Emit a wire envelope¶
The CRDT update blob is what travels over the wire. FCC wraps it in a small envelope so that the receiving client knows which session and which user the update came from. Let's build one by hand:
from fcc.collaboration.multi_user import envelope_to_wire
wire_msg = envelope_to_wire(
user_id="alice",
session_id="first-session",
envelope_type="edit",
payload={"update": alice_update.hex()},
)
print(wire_msg)
You will see a dict with four keys: user_id, session_id, type,
and payload. The payload.update is the CRDT blob hex-encoded so
it survives JSON serialization. In a real deployment the WebSocket
bridge would send this dict as JSON.
Round-trip it:
from fcc.collaboration.multi_user import envelope_from_wire
user_id, session_id, env_type, payload = envelope_from_wire(wire_msg)
print(user_id, session_id, env_type)
# -> alice first-session edit
This is the protocol the React frontend uses — nothing hidden, nothing magical.
Step 7 — Remove a user¶
Users leave sessions. When they do, their presence is cleared but their edits remain in the document.
session.remove_user("bob")
print(session.users)
# -> frozenset({'alice'})
# Bob's edits are still in the document.
print(session.crdt_doc.get_text())
# Still shows Bob's contribution.
This is the correct CRDT behaviour. A user's presence (cursor, selection, liveness) is ephemeral; a user's edits are permanent.
Common mistakes¶
Mistake 1 — Expecting concurrency from one interpreter¶
A single Python interpreter is single-threaded. The two edits in Step 3 were technically sequential. The CRDT backend doesn't care — it treats them as concurrent because they were produced by different user IDs with independent Lamport clocks. For real concurrency, you'd run two Python processes each talking to the same session via the WebSocket bridge.
Mistake 2 — Confusing MultiUserSession with CollaborationEngine¶
FCC has two collaboration layers:
CollaborationEngine— turn-by-turn session with scoring, approval gates, and progress tracking. Use it for single-user or sequential-turn collaboration.MultiUserSession— CRDT-backed real-time editing with no turn structure. Use it when you need Figma-style live co-editing.
They can run side-by-side on the same session_id; see
src/fcc/collaboration/multi_user.py docstrings.
Mistake 3 — Passing the same user_id twice¶
add_user is re-entrant, so a second call with the same user_id
won't duplicate the user. But it will refresh the user's
last_seen timestamp. Don't rely on this to detect reconnection —
use the explicit join and leave envelope types.
Mistake 4 — Assuming ypy_available() matters for this tutorial¶
It does not. Whichever backend FCC picks, the Python API is the same.
ypy_available() is a read-only diagnostic.
What next?¶
You now have a working mental model of:
- How a
MultiUserSessionmultiplexes many users on one document. - How
apply_local_editproduces a CRDT update blob. - How the CRDT backend merges concurrent edits deterministically.
- How Lamport clocks and replica IDs break ties.
- How the wire envelope wraps updates for the WebSocket bridge.
From here, four good next directions:
- Pool B's multi-user deep dive. Walk
docs/tutorials/advanced-capabilities/crdt-multi-user-collaboration.mdfor the full protocol, the WebSocket bridge, and the React frontend wiring. - Notebook 47. Run
notebooks/47_crdt_multi_user_demo.ipynbto see the session running against the UX bus with presence updates. - The collaboration engine. If you want turn-by-turn
collaboration with scoring, open
Hello FCC next and then notebook
06_collaboration_engine.ipynb. - Read the ADR. If you want to know why CRDT was chosen over simpler approaches, read ADR-012 — CRDT multi-user collaboration.
Quick reference — the commands you ran¶
from fcc.collaboration.multi_user import (
MultiUserSession, Edit, EditKind,
envelope_to_wire, envelope_from_wire,
)
# 1. Create a session
session = MultiUserSession(session_id="first-session")
# 2. Add users
alice = session.add_user("alice")
bob = session.add_user("bob")
# 3. Apply edits
u1 = session.apply_local_edit("alice",
Edit(kind=EditKind.SET_TEXT, value="Alice was here. "))
u2 = session.apply_local_edit("bob",
Edit(kind=EditKind.SET_TEXT, value="Bob was here too. "))
# 4. Observe
print(session.crdt_doc.get_text())
# 5. Read Lamport clocks
for op in session.crdt_doc.backend.operations():
print(op.lamport, op.replica_id[:8], op.value)
# 6. Wire envelope
msg = envelope_to_wire("alice", "first-session", "edit",
{"update": u1.hex()})
# 7. Remove a user
session.remove_user("bob")
Related resources¶
- Hello FCC — the 5-minute first program.
- Quickstart: First Simulation — simulation-level first tutorial.
- Visual Tour — frontend and apps overview.
- Glossary of Essentials — vocabulary definitions including CRDT and Lamport clock.
- Codename Decoder — ecosystem codename references for AURORA (Sky Parlour, where presence updates ultimately render).
Next steps¶
- If you completed the steps above, move on to notebook
47_crdt_multi_user_demo.ipynb(Pool F) to see presence updates render in a live UI. - Walk
docs/tutorials/advanced-capabilities/crdt-multi-user-collaboration.md(Pool B) for the full protocol. - Read ADR-012 — CRDT multi-user collaboration.
- Skim
docs/tutorials/sample-prompts/pillar-crdt-prompts.md(Pool C) for ready-to-run prompts. - When you are ready to learn about the Find → Create → Critique workflow cycle underneath this session layer, jump to the Quickstart: First Simulation page.