Skip to content

04 — Lobbies & matchmaking

With an access token (chapter 03) you can ask the lattice-director service to place you in a game session and hand back a server endpoint + a session token, which the runner then connects with.

The binding ships a Matchmaker helper that performs that whole exchange, so you no longer hand-roll HTTP against the director. The sequence it runs:

sequenceDiagram
  participant App as Your Unity client
  participant AU as auth
  participant DI as director
  participant GS as Game server (your sim)
  App->>AU: login → access_token
  App->>DI: POST /matchmake  (Bearer access_token) → session_handle + endpoint
  App->>DI: POST /resolve    (Bearer, {session_handle}) → { endpoint, session_token }
  App->>GS: runner.Connect(host, port, session_token)

Verified against: bindings/lattice-unity/com.lattice.netcode/Runtime/Matchmaking/Matchmaker.cs; control-plane/lattice-director/src/LatticeDirector/Program.cs:200-277.

Read the verified-status section before you build on this

The director endpoints are fully implemented server-side and the helper is covered by the binding's headless test suite — but the flow still needs a game-server instance registered with the fleet, and the session token is only enforced if your server verifies it. The exact status is spelled out at the end of this chapter.

The one-call join

Matchmaker.JoinAsync matchmakes, resolves, splits the endpoint, and connects the runner:

Find a session and connect to it
using Lattice;
using Lattice.Matchmaking;

// Keep ONE Matchmaker for the app's lifetime (it owns an HttpClient).
_matchmaker = new Matchmaker("https://director.example:3010", accessToken);

// ... when the player presses Play:
SessionTicket ticket = await _matchmaker.JoinAsync(_runner, region: "eu", mode: "ffa");
Debug.Log($"joined {ticket.SessionId} on {ticket.Host}:{ticket.Port}");

That is the whole flow. JoinAsync starts the runner as a Client if it is not running yet, then calls Connect with the session token as UTF-8 bytes.

Verified against: Runtime/Matchmaking/Matchmaker.Connect.cs:20-34 (JoinAsync), Matchmaker.Connect.cs:68-80 (ConnectRunner); exercised end to end against a live loopback server in tests/Lattice.Headless.Tests/MatchmakerConnectTests.cs.

Await it on the main thread

Unity installs a SynchronizationContext, so the continuation that starts the runner and calls Connect resumes on the main thread. Call JoinAsync from a MonoBehaviour async method (or an async void button handler) and do not ConfigureAwait(false) around it.

Constructing the Matchmaker

Constructors
new Matchmaker(string directorBaseUrl, string accessToken = null,
               HttpMessageHandler handler = null, string apiKey = null);
new Matchmaker(HttpClient http, string accessToken = null, string apiKey = null);
  • directorBaseUrl must be an absolute http/https URL. A path prefix works (https://api.example/director) — the helper appends the route under it.
  • accessToken is the lattice-auth access token from chapter 03, sent as Authorization: Bearer. It is a settable property: assign matchmaker.AccessToken when you refresh, and the next call picks it up.
  • apiKey is your game's public api key, sent as X-Lattice-Api-Key. It identifies the game; the access token is what authorises, so this is safe to ship in a client build. Set it — it is required to join by room code, and it is what tags a new session as yours. Also a settable property (matchmaker.ApiKey).
  • handler lets you supply your own transport (certificate pinning, a proxy, or a mock in tests). An injected handler is not disposed with the Matchmaker.
  • The second overload takes an HttpClient you configured yourself; its BaseAddress must be the director's base URL, and it is not disposed with the Matchmaker either.

Verified against: Matchmaker.cs:42-75 (AccessToken, ApiKey, both constructors), Matchmaker.cs:273-282 (the two headers, sent per request).

Send the key on /matchmake too, not only on /resolve

Sessions are scoped by game. A client that sends the key when joining by code but not when matchmaking creates its room in the unscoped pool — and then cannot find that room by its own code. The helper sends it on every route for exactly this reason, so setting ApiKey once is all you do. A key the director cannot resolve is refused with 403 rather than quietly ignored: being silently mis-scoped would make your room invisible to a friend whose own key resolved fine, with nothing in either client able to explain why.
Verified against: Matchmaker.cs:273-282 (ApiKeyHeader); control-plane/lattice-director/src/LatticeDirector/Program.cs:134-141 (GameScopeAsync, 403 on an unresolvable key); RoomCodeEndpointTests.cs:220-232 (Two_games_sharing_a_mode_name_do_not_share_sessions).

Parties and session size

Pass a MatchRequest for anything beyond a solo join:

Party of three, 16-player session
var request = new MatchRequest("eu", "ffa")
{
    Party = new[] { "sub-a", "sub-b", "sub-c" },   // account ids; omit ⇒ just you
    MaxPlayers = 16,                               // omit ⇒ the director's default of 8
};
SessionTicket ticket = await _matchmaker.JoinAsync(_runner, request);

Omitted fields are left out of the request body, not sent as null — the director treats an absent party as "the caller's own sub from the bearer token".

Verified against: DirectorContracts.cs:19-42 (MatchRequest), control-plane/lattice-director/src/LatticeDirector/Program.cs:213-215 (solo ⇒ own sub, max_players ?? 8).

Joining a session someone else found

A party member does not matchmake — the leader already did. They resolve the shared handle, which mints their own session token (tokens are per player):

Join by handle
// e.g. from the social service's "lattice.sessionHandle" join metadata
SessionTicket ticket = await _matchmaker.JoinByHandleAsync(_runner, sessionHandle);

Verified against: Matchmaker.Connect.cs:36-48 (JoinByHandleAsync); per-player sub binding at SessionTokenService.cs via control-plane/lattice-director/src/LatticeDirector/Program.cs:271-276.

Playing with a friend: the room code

The handle is built to be unguessable, which also makes it unreadable. Every /matchmake also mints a six-character room code the host can say out loud, and the friend types it into their own copy of the game. Two calls, one on each side:

Host: show the code
SessionTicket ticket = await _matchmaker.JoinAsync(_runner, region: "eu", mode: "ffa");
CodeLabel.text = ticket.RoomCode;      // e.g. "0NHBZ4" — read this out
Friend: type it in
// Whatever they typed is fine: case, spaces, dashes and look-alike glyphs are fixed up first.
SessionTicket ticket = await _matchmaker.JoinByCodeAsync(_runner, CodeField.text);
CodeLabel.text = ticket.RoomCode;      // the CANONICAL form — echo this back, not what was typed

JoinByCodeAsync needs ApiKey set, because a code is unique within your game rather than globally — the key is what tells the director whose codes to search. Nothing else changes: it resolves this player's own session token and connects the runner exactly like the handle path.

Verified against: Matchmaker.Connect.cs:50-66 (JoinByCodeAsync); Matchmaker.cs:201-219 (FindSessionAsync carries the code onto the ticket), Matchmaker.cs:235-246 (ResolveTicketByCodeAsync); DirectorContracts.cs:129-141 (SessionTicket, RoomCode); driven against a real listening runner in MatchmakerConnectTests.cs:111-140 (JoinByCodeAsync_ResolvesTheTypedCodeAndConnects).

The code is on the matchmake result, not the resolve result

/resolve does not echo it back, so SessionDescription.RoomCode (and the ticket JoinAsync/FindSessionAsync hands you) is the only place a client ever learns it. A ticket from JoinByHandleAsync has RoomCode == null — that path never sees one. GetSessionAsync does not carry it either, deliberately: /sessions/{id} has no membership check, so publishing the code there would turn a session id into a join credential.
Verified against: DirectorContracts.cs:47-66 (SessionDescription, RoomCode); control-plane/lattice-director/src/LatticeDirector/Api/Contracts.cs:51 (ResolveResponse has no code); RoomCodeEndpointTests.cs:317-330 (The_room_code_is_not_exposed_by_the_session_status_endpoint).

If you want the code without joining — to validate a text field as the player types, or to echo the canonical form back — RoomCodeFormat is the same normaliser on its own:

string code = RoomCodeFormat.Normalize(CodeField.text);   // null ⇒ it cannot be a code
JoinButton.interactable = code != null;

Codes are six characters from a 27-glyph alphabet with no vowels (so a code can never spell a word) and no look-alike pairs: O folds to 0, I and 1 to L, 2 to Z, 5 to S, 8 to B. That is why normalising is the right move rather than rejecting — someone who types O for 0 meant the room you are in. A code that is one character too long is refused, never truncated: silently dropping a character would join a different room than the player asked for.

Verified against: RoomCodeFormat.cs:16-70 (RoomCodeFormat, Normalize, IsCanonical); alphabet and mapping copied from RoomCode.cs:21,57-85 (Normalize), with a drift guard that reads that file at MatchmakerRoomCodeTests.cs:357-370 (TheAlphabetIsTheDirectorsAlphabet).

Normalising locally is not belt-and-braces — it saves the player's retry budget

/resolve is rate-limited per IP (a burst of 10, refilling ~5/minute), and a request that was never going to succeed still spends one. Catching "that is not a code" before sending is what keeps three typos from throttling a player who had the right code all along.
Verified against: control-plane/lattice-director/src/LatticeDirector/Program.cs:87-104 (the token-bucket policy); refused-before-sending at MatchmakerRoomCodeTests.cs:199-215 (AnImpossibleCodeCostsNoRequest).

Private rooms

A room a stranger can never be paired into. Quick match offers it to nobody, so its room code and handle are the only ways in:

A room for invited friends only
var request = new MatchRequest("eu", "ffa") { MaxPlayers = 4, Private = true };
SessionTicket ticket = await _matchmaker.JoinAsync(_runner, request);
CodeLabel.text = ticket.RoomCode;      // the only way anyone else gets in

The friend joins exactly as before — JoinByCodeAsync with the six characters. They never send the flag, and they do not need to: /resolve reports what the room is, so ticket.Private is true on their side too and a join screen can badge it.

if (ticket.Private) Badge.text = "Private room";

A private request also never reuses an open session — it creates a fresh room or it fails. That is the flag working, and it gives 409 a second meaning on /matchmake: with the fleet saturated there is nowhere to make a new room, while the identical public request might still have been seated into somebody else's. So it arrives as JoinFailure.PrivateRoomUnavailable, distinct from NoCapacity, and the useful thing to offer is quick match rather than "try again":

catch (DirectorException ex) when (ex.Failure == JoinFailure.PrivateRoomUnavailable)
{
    OfferQuickMatchInstead();
}

Verified against: DirectorContracts.cs:26-50 (MatchRequest, Private, ToJson), DirectorContracts.cs:140-146 (SessionTicket.Private), DirectorContracts.cs:204-209 (PrivateRoomUnavailable); Matchmaker.cs:109-120 (MatchmakeAsync, private → its own call kind); director policy at control-plane/lattice-director/src/LatticeDirector/Services/Matchmaker.cs:52-87 (MatchParty, privacy leaves the pairing pool in both directions); MatchmakerPrivateRoomTests.cs:150-172 (APrivateMatchmakeReadsItsOwn409).

GetSessionAsync does not report privacy — and says so

/sessions/{id} omits the field, so SessionDescription.Private is bool? and reads null there. Reading a missing field as false would badge a private room as public; null is "not asked", which is a different answer from "no". After MatchmakeAsync it is never null, and SessionTicket.Private — the one to show a player — is a plain bool from /resolve, which always answers.
Verified against: DirectorContracts.cs:72-78 (SessionDescription.Private); control-plane/lattice-director/src/LatticeDirector/Api/Contracts.cs:63-70 (SessionStatusResponse has no flag); MatchmakerPrivateRoomTests.cs:101-117 (SessionStatusDoesNotClaimARoomIsPublic).

Handling failures

Every director refusal surfaces as a DirectorException. Branch on ex.Failure, not on the status: the same code means different things on different routes, and ex.PlayerMessage is a sentence you can put straight in front of a person.

try
{
    await _matchmaker.JoinByCodeAsync(_runner, CodeField.text);
}
catch (DirectorException ex)
{
    if (ex.Failure == JoinFailure.NotSignedIn) { await RefreshAccessTokenAndRetry(); return; }

    ShowMessage(ex.PlayerMessage);   // "That room is already full.", "No room with that code.", …
    Debug.LogWarning(ex.Message);    // the route, the status and the service's own error text
}
JoinFailure When What the player is told
UnknownCode 404 resolving a room code No room with that code. Check it and try again.
UnknownSession 404 resolving a handle or session id That session is no longer available.
RoomFull 409 on /resolve That room is already full.
NoCapacity 409 on /matchmake No servers are free right now. Try again in a moment.
PrivateRoomUnavailable 409 on a private /matchmake Could not open a private room right now. Try again, or use quick match.
RateLimited 429 — the per-IP /resolve budget Too many join attempts from your network. Wait a minute…
UnknownApiKey 403 — the director does not know your key The platform did not recognise this build's game key.
NotSignedIn 401 — no/expired access token You are not signed in any more. Sign in and try again.
MalformedCode local, before any request That is not a valid room code. Codes are 6 characters.
ApiKeyMissing local — ApiKey was never set This build is not set up to join by room code.
None anything else: 400, 5xx, an unreadable body Could not reach the game service…

StatusCode and Error (the service's own {"error": …} text) are still there for logs. Note the rows that share a status: 409 is "this room is full" from /resolve but "no server has capacity" from /matchmake — and a third thing on a private matchmake, where the status and the error text are identical and only the request tells them apart. 404 is "no such code" or "no such session" depending on which identifier was sent. That is the whole reason Failure exists — a 429 shown as "check your code" is how a throttled player spends the rest of their budget retyping a code that was always right.

Transport failures (DNS, TLS, timeouts) surface as the usual HttpRequestException / TaskCanceledException, untouched. The default request timeout is 15 s; pass your own HttpClient to change it.

Verified against: DirectorContracts.cs:158-185 (JoinFailure), DirectorContracts.cs:203-208 (Failure, PlayerMessage), DirectorContracts.cs:232-283 (DirectorFailures, the status→reason and reason→sentence maps); Matchmaker.cs:267-299 (SendAsync, status → DirectorException); director statuses at control-plane/lattice-director/src/LatticeDirector/Program.cs:200-277; every row driven at MatchmakerRoomCodeTests.cs:248-266 (EachRefusalIsToldApart), MatchmakerRoomCodeTests.cs:268-292 (TheSameStatusMeansDifferentThingsOnDifferentRoutes).

The individual routes

JoinAsync is FindSessionAsync + connect, and FindSessionAsync is MatchmakeAsync + ResolveTicketAsync. Use the steps directly when a lobby UI needs what is in between.

MatchmakeAsync → POST /matchmake

SessionDescription session = await _matchmaker.MatchmakeAsync(new MatchRequest("eu", "ffa"));
// session.SessionHandle / SessionId / Endpoint / Region / Mode / PlayerCount / MaxPlayers

Wire body and response:

POST /matchmake (Authorization: Bearer <access_token>)
{
  "region": "eu",
  "mode": "ffa",
  "party": ["sub-a", "sub-b"],   // optional; omitted ⇒ solo
  "max_players": 8,              // optional
  "private": true                // optional; omitted ⇒ false (a normal, pairable room)
}
200
{
  "session_handle": "3pQ…_",     // opaque, URL-safe; this is what you resolve
  "room_code": "0NHBZ4",         // 6 chars, for a player to read out — see below
  "session_id": "b1f2…",
  "endpoint": "1.2.3.4:9000",    // the game server's host:port
  "region": "eu",
  "mode": "ffa",
  "player_count": 1,
  "max_players": 8,
  "private": false               // what the room IS
}

Verified against: control-plane/lattice-director/src/LatticeDirector/Program.cs:200-227; control-plane/lattice-director/src/LatticeDirector/Api/Contracts.cs:25-39 (MatchmakeRequest, MatchmakeResponse); binding mapping at DirectorContracts.cs:47-79 (SessionDescription).

room_code is the one to show a player

The handle is built to be unguessable, which also makes it unreadable. room_code is six characters from an alphabet with no vowels and no look-alike glyphs, so it survives being read out loud. It is unique within your game rather than globally, so resolving one needs your game's X-Lattice-Api-Key alongside the player's bearer — send that header on /matchmake too, since it is what tags the session with your game. Codes expire after six hours; the handle does not.

ResolveAsync → POST /resolve

Exchanges the opaque handle for connect details and mints the session token. Resolve even straight after matchmaking — the token exists only here.

ResolveResult resolved = await _matchmaker.ResolveAsync(sessionHandle);
// resolved.Endpoint / SessionToken / SessionId

The body takes either identifier — {"session_handle": "…"} as shown, or {"room_code": "0NHBZ4"} with the X-Lattice-Api-Key header. Resolving also takes a seat in the session, so a room that fills up stops being offered to other matchmaking players; it is idempotent, so a host resolving its own handle does not consume a second slot.

200
{
  "endpoint": "1.2.3.4:9000",
  "session_token": "eyJ…",       // director-signed Ed25519 JWT
  "session_id": "b1f2…",
  "private": false               // what the JOINED room is, on both request shapes
}

Prefer ResolveTicketAsync, which returns a SessionTicket with the endpoint already split.

Verified against: control-plane/lattice-director/src/LatticeDirector/Program.cs:227-277; control-plane/lattice-director/src/LatticeDirector/Api/Contracts.cs:43-51 (ResolveRequest, ResolveResponse); Matchmaker.cs:128-144 (ResolveAsync), Matchmaker.cs:222-233 (ResolveTicketAsync).

GetSessionAsync → GET /sessions/{id}

Live occupancy for a session you know the id of — for a lobby screen polling player counts. It mints no token.

SessionDescription status = await _matchmaker.GetSessionAsync(sessionId);
Label.text = $"{status.PlayerCount}/{status.MaxPlayers}";

Verified against: control-plane/lattice-director/src/LatticeDirector/Program.cs:279-290; Matchmaker.cs:180-195 (GetSessionAsync).

What the session token contains

The director signs an Ed25519 (EdDSA) JWT binding the session to a specific endpoint and player, so the game server can verify — offline, against the director's JWKS — exactly who may join which session at the netcode handshake:

session token claims
sid    = session id            hdl = session handle
ep     = endpoint (host:port)   sub = player account id (from your access token)
region = region                 iat/exp/jti = issued/expiry/unique id

Verified against: lattice-director/src/LatticeDirector/Tokens/SessionTokenService.cs:14-24,58-93.

Connecting with a ticket you already have

SessionTicket is the connect-ready shape: Host, Port, SessionToken, SessionTokenBytes(), plus SessionId / SessionHandle / the raw Endpoint. Build one from details you obtained elsewhere (a cached resolve, or a payload passed between systems) and connect without another round trip:

SessionTicket ticket = SessionTicket.ForEndpoint("1.2.3.4:9000", sessionToken, sessionId);
Matchmaker.ConnectRunner(_runner, ticket);   // starts the runner as Client if needed, then Connects

DirectorEndpoint.TryParse is the same host:port split on its own, if you only need that. Both accept bracketed IPv6 ([::1]:9000); a bare IPv6 literal is rejected as ambiguous, because there is no way to tell a port from an address group.

Verified against: DirectorContracts.cs:108-152 (SessionTicket, ForEndpoint), DirectorContracts.cs:285-337 (DirectorEndpoint, TryParse); parsing cases in tests/Lattice.Headless.Tests/DirectorEndpointTests.cs.

Where game servers come from

The director does not spawn servers. A session's endpoint is the endpoint a running game-server instance advertised by registering itself with the fleet:

A game server registers itself (server-side, X-Fleet-Token required)
curl -s -X POST http://director:3010/fleet/register -H "X-Fleet-Token: $FLEET" \
  -H 'content-type: application/json' \
  -d '{"instance_id":"i-1","endpoint":"1.2.3.4:9000","region":"eu","modes":["ffa"],"capacity":64}'

Verified against: control-plane/lattice-director/src/LatticeDirector/Program.cs:160-179 (RegisterRequest, gated by X-Fleet-Token); docs-site/docs/getting-started/self-hosted.md:69-73.

The matchmaker then either reuses an open session in that region+mode or creates a new one on the live instance with the most free capacity. With no live registered instance matching region+mode, /matchmake returns 409 and JoinAsync throws a DirectorException with StatusCode == 409.

Verified against: control-plane/lattice-director/src/LatticeDirector/Services/Matchmaker.cs:52-88 (MatchParty).

Social parties (optional)

The social service models parties and carries game-defined join metadata. The blessed convention for suite games is a well-known bag key that holds a director-resolvable handle:

JoinMetadata convention
// "lattice.sessionHandle" — a director-resolvable handle a suite game reads → resolve → connect.
public const string LatticeSessionHandleKey = "lattice.sessionHandle";

A party leader calls JoinAsync (or FindSessionAsync), publishes ticket.SessionHandle into the party's join metadata, and members call JoinByHandleAsync with it.

Verified against: control-plane/lattice-social-client/src/Models.cs:79-96 (JoinMetadata, LatticeSessionHandleKey), Models.cs:118-126 (Party with SessionHandle).

Party matchmaking is server-modeled, not exercised by the Unity sample

The party plumbing exists on the social service and its client models, but the tic-tac-toe sample does not use it (it's a two-player listen-server). Treat this section as a pointer to the capability, not a verified end-to-end Unity flow.


Appendix: calling the director yourself

The helper is optional — the routes are plain HTTP and nothing stops you from calling them with UnityWebRequest, your own HttpClient, or a backend of your own. Use this path if you already have an HTTP layer with your own retry/telemetry policy, or if your session data arrives from somewhere other than the director.

From /resolve to Connect (the glue the helper does for you)
// resolve returned: string endpoint (e.g. "1.2.3.4:9000"), string sessionToken (a JWT)
var parts = endpoint.Split(':');
string host = parts[0];
ushort port = ushort.Parse(parts[1]);
byte[] token = System.Text.Encoding.UTF8.GetBytes(sessionToken);

_runner.StartGame(LatticeGameMode.Client);
_runner.Connect(host, port, token);

Two things that split is missing and the helper handles: bracketed IPv6 endpoints, and a clear error when a fleet instance registered something that is not host:port. If you keep your own HTTP layer but want the rest, hand your values to SessionTicket.ForEndpoint and Matchmaker.ConnectRunner.

Verified against: NetworkRunner.cs:230-236 (Connect(string address, ushort port, byte[] token)).


Verified wiring status (honest)

What is real and works:

  • /matchmake and /resolve are fully implemented and return exactly the shapes above. /matchmake returns a session_handle + endpoint; /resolve returns endpoint + a real Ed25519-signed session_token + session_id. Both require a valid auth Bearer token.
    Verified against: control-plane/lattice-director/src/LatticeDirector/Program.cs:200-277, SessionTokenService.cs:58-93, and the director's own suite (MatchmakerTests.cs, EndpointTests.cs, SessionTokenTests.cs).
  • The binding now ships a director client. Matchmaker covers matchmake / resolve / session-status, the endpoint split, the error mapping and the connect. Its request shapes, response parsing, error paths and base-URL handling are pinned by headless tests against a mocked HttpMessageHandler, and JoinAsync is additionally driven against a real listening runner — a mocked director hands back a live loopback endpoint and the client reaches Connected.
    Verified against: tests/Lattice.Headless.Tests/MatchmakerTests.cs, MatchmakerConnectTests.cs, DirectorEndpointTests.cs.
  • The endpoint is a real advertised host:port — but only because a game-server instance registered it via /fleet/register.

The caveats you must design around:

  1. You must run and register a game server. The director never launches servers. With no live fleet instance matching the requested region+mode, /matchmake returns 409. In the self-hosted path you start your own server (e.g. LatticeGameMode.Server) and POST /fleet/register its endpoint.
    Verified against: control-plane/lattice-director/src/LatticeDirector/Services/Matchmaker.cs:52-88 (MatchParty); control-plane/lattice-director/src/LatticeDirector/Program.cs:160-179.
  2. Session-token enforcement at the game server is yours to wire. The /resolve token is designed to be verified offline by the game server at the netcode handshake (SessionTokenService.Validate), but the tic-tac-toe sample connects with an arbitrary token ("ttt") to a loopback server, so nothing here proves a native server rejecting a bad token. The helper delivers the token; verifying it is a server-side responsibility.
    Verified against: TicTacToeGame.cs:61; SessionTokenService.cs:95-111.
  3. The Fusion-compat lobby API is still stubs. SessionInfo / LobbyInfo / GetAvailableRegions / JoinSessionLobby on NetworkRunner are compile-stubs that never connect — they exist for ported Photon Fusion code to compile. Use Matchmaker, not those.
    Verified against: Runtime/FusionCompat/NetworkRunner.Simulation.cs:150-204.

Bottom line: the matchmake → resolve → connect contract is real, and the glue is now in the binding. What remains yours is running and registering a game server, and enforcing the session token on it. For learning and for local/P2P play, skip the director entirely and use the listen-server flow from chapter 02.


Where the session token actually gets checked

Passing the token to Connect only delivers it. The game server verifies it inside the handshake, through the token-validation seam of the secure datagram channel — which is also what turns that channel from unauthenticated into authenticated.


Next: 05 — Networked objects, the heart of replication.