Skip to content

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:

  1. Starts a MultiUserSession with a fresh collaborative document.
  2. Adds two simulated users (Alice and Bob).
  3. Lets each user apply a concurrent edit.
  4. Watches the CRDT backend merge the two edits.
  5. 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_py package. FCC ships a pure-Python in-memory CRDT backend that runs everywhere. When y_py is 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:

import fcc
print(fcc.__version__)
# -> 1.5.3 (or whatever your current version is)

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:

doc = session.crdt_doc
print(type(doc).__name__)
# -> CollaborativeDocument

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:

print(session.crdt_doc.get_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 MultiUserSession multiplexes many users on one document.
  • How apply_local_edit produces 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:

  1. Pool B's multi-user deep dive. Walk docs/tutorials/advanced-capabilities/crdt-multi-user-collaboration.md for the full protocol, the WebSocket bridge, and the React frontend wiring.
  2. Notebook 47. Run notebooks/47_crdt_multi_user_demo.ipynb to see the session running against the UX bus with presence updates.
  3. The collaboration engine. If you want turn-by-turn collaboration with scoring, open Hello FCC next and then notebook 06_collaboration_engine.ipynb.
  4. 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")


Next steps

  1. 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.
  2. Walk docs/tutorials/advanced-capabilities/crdt-multi-user-collaboration.md (Pool B) for the full protocol.
  3. Read ADR-012 — CRDT multi-user collaboration.
  4. Skim docs/tutorials/sample-prompts/pillar-crdt-prompts.md (Pool C) for ready-to-run prompts.
  5. When you are ready to learn about the Find → Create → Critique workflow cycle underneath this session layer, jump to the Quickstart: First Simulation page.