# Netcode (Unity): Direct vs Relay connection modes

> How the Unity QUIC transport chooses between Direct and Relay, the fields each mode needs, and how join codes resolve to a relay endpoint.

The Unity transport that you drop into `NetworkManager` in place of
`UnityTransport` supports two connection modes. One field selects the
mode:

```csharp
t.ConnectionType = ConnectionType.Relay;   // or ConnectionType.Direct
```

Everything else about your game code stays the same. The mode decides
who peers dial, which fields the transport reads, and the datagram
payload ceiling it advertises.

## Choosing Direct or Relay

| | Direct | Relay |
| --- | --- | --- |
| Peers dial | The host's IP and port, over QUIC | The nearest relay endpoint |
| Allocation API | Not used | The host creates an allocation |
| Works behind NAT | No — the host must be reachable | Yes |
| Latency | Lowest | Adds the hop through the relay |
| Endpoint selection | You supply it (`Address`, `Port`) | Geo-DNS, resolved when the join code is redeemed |

Pick **Direct** when you control the host's network path: LAN play,
a port-forwarded host, or a dedicated server. Pick **Relay** when the
host is an ordinary player's machine behind NAT, or when you do not want
players to learn each other's addresses.

## Transport fields by mode

The high-altitude integration (L-HI, Netcode for GameObjects) configures
a `ClutchCallTransport` component. The low-altitude integration (L-LO,
raw `NetworkDriver` / Netcode for Entities) configures a
`ClutchCallNetworkInterface` struct.

| Field | Mode | Notes |
| --- | --- | --- |
| `ConnectionType` | both | `ConnectionType.Direct` or `ConnectionType.Relay`. |
| `SessionNamespace` | both | The session's namespace string. Identical on every peer. |
| `MaxPlayers` | Relay (L-HI) | The session's player ceiling. |
| `RelayJoinCode` | Relay (L-HI) | The code created in **Allocations**, redeemed on connect. |
| `Address` | Direct (L-HI) | The host's IP or DNS name. |
| `Port` | Direct (L-HI) | QUIC/UDP. `4443` in the quickstart. |
| `Role` | L-LO | `NetworkRole.Server` or `NetworkRole.Client`. |

In L-HI, after assigning the transport to
`nm.NetworkConfig.NetworkTransport`, you still call `nm.StartHost()` or
`nm.StartClient()` as usual. In L-LO, you pass the configured interface
to `NetworkDriver.Create(iface)`.

## Reachability requirements for Direct

Direct mode has no allocation step and no service in the middle. The
host binds a QUIC/UDP listener and peers dial it:

```csharp
t.ConnectionType = ConnectionType.Direct;
t.Address = "<host-ip-or-dns>";
t.Port    = 4443;        // QUIC/UDP
nm.StartHost();          // peers dial the host directly
```

For that to work, every client must be able to reach `Address:Port`
over **UDP** — QUIC is UDP, not TCP, so a TCP-only port forward or
load-balancer rule will not carry it. In practice that means one of:

- the host and clients are on the same LAN;
- the host's router forwards the UDP port to the host machine;
- the host is a dedicated server with a routable address and the UDP
  port open inbound.

If none of those hold, use Relay instead. There is no fallback inside
Direct mode — a client that cannot reach the host simply fails to
connect.

## Creating an allocation and sharing a join code

Relay mode starts with an **allocation**, created from the console's
Allocations screen. The allocation represents the session on the relay
fleet and yields a short **join code**.

The host shares that join code with its players through whatever channel
your game already uses — a lobby, an invite link, matchmaking
metadata. The transport does not distribute join codes for you.

You can also create an allocation from code. The host mints one, and each
player redeems the join code. Do this **before** you start the
`NetworkManager`, because the endpoint and the token must be in place
first:

```csharp
// Host — mint an allocation and a join code.
t.ConnectionType = ConnectionType.Relay;
t.MaxPlayers     = 16;
t.RelayBffUrl    = "https://portal.firstbyte.fast";
t.RelayApiKey    = apiKey;        // nck_live_… — a SERVER credential
var a = await t.AllocateAsync();
Share(a.JoinCode);

nm.StartHost();
```

```csharp
// Player — redeem the code the host shared.
t.ConnectionType = ConnectionType.Relay;
t.RelayJoinCode  = joinCode;
var a = await t.JoinAsync();

nm.StartClient();
```

> **WARNING:**
> The API key carries your organisation's identity, so it is a server
> credential. Ship it in a host or dedicated-server build only. For a player
> build, redeem the code through your own matchmaker and hand the result to
> `ApplyAllocation(...)`, which needs no key:
> 
> ```csharp
> t.ApplyAllocation(allocationFromYourMatchmaker);
> nm.StartClient();
> ```

## Redeeming a join code: endpoint and token

A join code is not an address. `JoinAsync` redeems the code and gets back
three things:

1. **A nearest-relay endpoint.** Endpoint selection is geo-DNS based, so
   two players in different regions redeeming the same code can land on
   different relay addresses for the same session. You do not hardcode a
   relay hostname.
2. **A JWKS-verified token.** The relay checks this token on the
   connection. Your game code never constructs it; redemption is what
   produces it.
3. **The session namespace**, assigned by the platform. See
   [Session namespace](#session-namespace-and-max-players) below.

Redemption is a network call, so it is awaitable. Complete it before you
start the `NetworkManager`.

This is why Relay mode needs no `Address` or `Port` — supplying them has
no effect once `ConnectionType` is `Relay`. Conversely, a
`RelayJoinCode` is meaningless in Direct mode.

For background on how relay tokens are scoped and verified, see
[Authentication](/concepts/authentication).

## Session namespace and max players

`SessionNamespace` is the session's identity on the platform. It is
path-shaped:

```
arena/<your-org-id>/<your-session-name>
```

**In Relay mode the platform assigns it. Do not choose it.** An allocation
scopes the namespace to the organisation that owns the API key, so the
value you ask for is a suffix, not the whole string. Ask for `my-session`
and you get `arena/<your-org-id>/my-session` back.

This is what keeps two customers apart. Both can ask for the same session
name and still get two different namespaces, so neither can reach the
other's tracks.

`AllocateAsync`, `JoinAsync` and `ApplyAllocation` all write the assigned
value onto the transport for you. Read it back from the allocation if you
need it:

```csharp
var a = await t.AllocateAsync();
Debug.Log(a.SessionNamespace);   // arena/<org>/<session>
```

> **WARNING:**
> Do not overwrite `SessionNamespace` after an allocation. The relay confines
> a session to the namespace in its token, so a value you set by hand is
> refused.

In Direct mode there is no allocation and no token, so you set
`SessionNamespace` yourself. Use the same value on every peer. It applies
at both altitudes — the L-LO `ClutchCallNetworkInterface` takes the same
field alongside `Role`.

`MaxPlayers` is the session's player ceiling, set on the transport in the
L-HI integration. The quickstart's example value is `16`; use the number
your game design calls for.

## Path MTU per mode

The transport advertises `net_max_datagram` as the path MTU for the mode
in use:

| Mode | `net_max_datagram` |
| --- | --- |
| Direct | ~1350 B |
| Relay | ~1232 B |

Relay is lower because the relay path carries additional framing around
your payload. Size any single unreliable message to fit the **smaller**
of the two if you intend to support both modes from one build —
otherwise a snapshot that fits in a LAN Direct session will not fit once
the same code runs through the relay.

Deliveries that map onto QUIC reliable streams are not bounded by this
number in the same way; QUIC segments them for you. The ceiling matters
for the datagram-carried QoS levels. See the QoS / channel mapping table
on the Netcode quickstart screen for which `NetworkDelivery` value lands
on which QUIC carrier.

## Related

- [Modalities overview](/modalities/overview)
- [Authentication](/concepts/authentication) — relay tokens and namespace scope
- [Telemetry](/platform/telemetry) — QUIC handshake and transport metrics
