GPUI can run the Elixir application session in one node or process and present its windows through a native display client elsewhere. The application remains renderer-independent; snapshots and normalized events cross the transport.

Topology

GPUI.Remote.Server is an OTP supervision tree. It owns a supervised acceptor, per-connection subtrees, and one isolated session subtree per mounted client. Each connection subtree owns its protocol process, connection owner, and task supervisor. Each session subtree owns bounded request workers, its coordinator, and its GPUI.Session; losing an essential child tears down that whole isolated subtree. Session work is delegated out of the central server and connection owner, so a slow mount or event in one session does not delay unrelated sessions—even when requests share one transport connection. GPUI.Remote.Client negotiates the protocol, mounts the application, forwards snapshots to its configured display, and sends display events back to the server.

The transport uses SafeRPC over framed TCP or SSL with request IDs, timeouts, capability negotiation, and safe ETF decoding. A protocol hello is required before other operations.

Start a server

{:ok, server} =
  GPUI.Remote.Server.start_link(
    app: MyApp.Desktop,
    port: 5050,
    max_in_flight_requests_per_connection: 64,
    max_in_flight_requests_per_session: 16
  )

GPUI.Remote.Server has a supervisor child specification, so production applications can place the same options directly in their root tree:

children = [
  {GPUI.Remote.Server, app: MyApp.Desktop, port: 5050, name: MyApp.RemoteServer}
]

Supervisor.start_link(children, strategy: :one_for_one)

Each client gets its own supervised session and root assigns. A disconnected client cannot mutate another client's application state. The request limits shown above are the defaults; both accept positive values up to 4,096.

Start a native display client

{:ok, client} =
  GPUI.Remote.Client.start_link(
    host: "127.0.0.1",
    port: 5050,
    display: GPUI.Display.Native,
    poll_interval: 16
  )

{:ok, snapshot} = GPUI.Remote.Client.mount(client)

GPUI.Remote.Client likewise supports {GPUI.Remote.Client, options} in a display-side supervision tree. The native event loop remains local to the display client. Only declarative snapshots, resources, events, and protocol operations cross the connection. Display-side file pickers read bounded bytes on the client and send those bytes with an operation ID; they never claim that a client-local path is readable by the application server. Clipboard buttons likewise write to the display client's clipboard. Application-side filesystem work has the opposite semantics: paths identify files on the application server. Applications must label that distinction explicitly instead of presenting a server-local directory as a remote-client directory. Source-backed virtual-list range requests also originate on the display and cross the same event channel, allowing the server to return only an overscanned slice.

Remote display updates use the same typed OTP message shape as local runtimes:

:ok = GPUI.Remote.Client.subscribe(client)

receive do
  {:gpui, ^client, %GPUI.Runtime.Update{events: events, snapshot: snapshot}} ->
    # The snapshot is already synchronized to the client's display.
end

After synchronizing a remote snapshot, callers can wait for its local native frame with GPUI.Remote.Client.await_frame/3. Native-only changes use GPUI.Remote.Client.frame_token/2 and await_frame_after/4. The remote client remains responsive while either display barrier is pending. Subscribers are monitored and removed when they exit.

SSL

A server can use a certificate and key:

GPUI_REMOTE_SSL=1 \
GPUI_REMOTE_SSL_CERTFILE=/path/to/server.pem \
GPUI_REMOTE_SSL_KEYFILE=/path/to/server.key \
GPUI_APP_PORT=5050 \
mix run examples/remote/app_server.exs

The client uses a CA certificate and optional SNI name:

GPUI_REMOTE_SSL=1 \
GPUI_REMOTE_SSL_CACERTFILE=/path/to/ca.pem \
GPUI_REMOTE_SSL_SERVER_NAME=localhost \
GPUI_APP_HOST=localhost \
GPUI_APP_PORT=5050 \
mix run examples/remote/display_client.exs

Failure behavior

Requests have explicit IDs and timeouts. Negotiation rejects mismatched current protocol versions or missing capabilities before mounting a session. The version identifies the one wire shape implemented by the current source tree; no older private-project wire revisions are retained. See Platform support and development status for the current package and protocol policy. Mounts and events carry stable operation IDs, so retrying after a lost reply does not mount twice or apply the same event twice. A disconnected session remains resumable until its configured session TTL expires; terminating a connection immediately removes only its connection owner.

Display events drained during an outage are retained in a bounded 1,024-event client queue and retried in order after reconnection. If that bound is exceeded, the newest events are retained. Poll timers use generation tokens, while each session coordinator owns and resets its expiry timer directly. Stale timer messages cannot create duplicate polling loops or expire a recently active session. Work above either configured in-flight limit is rejected with {:error, :overloaded} before it reaches the session. Automatically forwarded display events remain in the client's bounded queue and retry on a later poll. Client reconnect behavior remains isolated from the native window loop and does not create shared application state.

Remote transport and protocol tests live under test/integration/; local and remote native behavior is also exercised under Xvfb in test/e2e/.