> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aiola.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Connections

> Understand how a paired phone reaches the desktop server over LAN or Tailscale and receives revocable device-scoped access.

Connections makes the desktop's local Aiola server reachable from a paired phone or another Mac without exposing a reusable account token in the connection code. For setup steps, use [Connect Mobile](/guides/connect-mobile).

## Architecture

| Component            | Responsibility                                                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Desktop main process | Starts/restarts the local server, chooses the listening port, enumerates network endpoints, and orders them by preference          |
| Desktop renderer     | Requests a short-lived bootstrap, builds the version-3 QR payload, and shows device presence/revocation controls                   |
| Local Aiola server   | Exchanges the bootstrap for a device credential, issues WebSocket tickets, records device metadata, and closes revoked connections |
| Mobile client        | Scans the QR, exchanges the bootstrap, stores the device credential, and tries endpoint candidates in order                        |

## Desktop local server

The desktop server binds to loopback until **Central Settings → Connections → Allow LAN connections** is enabled. It tries the mode's preferred port and may select another available port, so clients must use the displayed/generated endpoint rather than hard-code a port.

| Desktop mode      | Preferred port |
| ----------------- | -------------- |
| Production        | `47821`        |
| Isolated instance | `47831`        |
| Preview           | `47921`        |

## LAN binding

Enabling **Allow LAN connections** restarts the local server with host `0.0.0.0`; disabling it uses `127.0.0.1`. Private IPv4 interfaces appear as Wi-Fi, Ethernet, or Local network endpoints with URLs shaped as follows:

```text theme={null}
http://<ipv4-address>:<selected-port>
```

LAN endpoints work on the same network and are marked **Not encrypted** in the app. The API is available only in the desktop app; an externally managed server must instead set `AIOLA_SERVER_HOST` and `AIOLA_ALLOW_LAN` in its service configuration and restart that service.

## Tailscale

When a Tailscale IPv4 address exists, it is preferred over LAN and labeled **Works from anywhere. Encrypted.** A MagicDNS endpoint can follow it when a tailnet address and DNS name are available.

| Priority | Endpoint kind         | Reachability                     |
| -------- | --------------------- | -------------------------------- |
| `0`      | Tailscale IP          | Tailnet; encrypted               |
| `1`      | Tailscale hostname    | Tailnet with MagicDNS; encrypted |
| `2`      | Private LAN           | Same local network; unencrypted  |
| `3`      | Other network address | Network-dependent; unencrypted   |

The QR includes one address per reachability class, so the Tailscale hostname is omitted when the Tailscale IP already represents the same path.

## Pairing bootstrap

Opening **Central Settings → Connections → Devices → Add** requests a bootstrap and renders a QR code. The implementation does not ask the user to type an address.

```json theme={null}
{
  "v": 3,
  "name": "<desktop-name>",
  "baseUrl": "<legacy-lan-or-first-url>",
  "baseUrls": ["<best-url>", "<fallback-url>"],
  "pairToken": "<bootstrap-secret>"
}
```

| Property    | Meaning                                                          |
| ----------- | ---------------------------------------------------------------- |
| `v`         | Pairing payload version `3`                                      |
| `name`      | Desktop display name; omitted only when unavailable              |
| `baseUrl`   | Legacy version-2 fallback, preferring an unencrypted LAN address |
| `baseUrls`  | Version-3 failover list, best endpoint first                     |
| `pairToken` | One-time bootstrap secret                                        |

The bootstrap expires after `120` seconds. While the dialog remains open, the desktop refreshes it 10 seconds before expiry, with a minimum refresh delay of 5 seconds. **Generate a new code** replaces it on demand.

## Device credentials

| Operation               | Method and path                         | Authentication or body                                | Result                                    |
| ----------------------- | --------------------------------------- | ----------------------------------------------------- | ----------------------------------------- |
| Create bootstrap        | `POST /api/pairing/bootstrap`           | Owner bearer session                                  | `{secret, expiresAt}`                     |
| Exchange bootstrap      | `POST /api/pairing/exchange`            | Owner bearer session; JSON `{secret, deviceId?}`      | Persistent device credential and metadata |
| List devices            | `GET /api/pairing/devices`              | Owner bearer session                                  | Non-secret device metadata                |
| Revoke device           | `DELETE /api/pairing/devices/:deviceId` | Owner bearer session                                  | `{ok: true}`                              |
| Create WebSocket ticket | `POST /api/pairing/ws-ticket`           | Owner bearer session plus `x-aiola-device-credential` | `{ticket, expiresAt}`                     |

`deviceId` is optional during exchange and is capped at 200 characters when supplied. Re-pairing the same device ID replaces its previous credential. Raw bootstrap secrets, device credentials, and WebSocket tickets are returned only to callers; the server persists SHA-256 hashes and compares presented secrets with timing-safe equality. The credential store is written with file mode `0600`.

Responses that contain credentials set `Cache-Control: no-store` and `Pragma: no-cache`.

## WebSocket tickets

A paired client presents its device credential in `x-aiola-device-credential` to mint a ticket. The ticket expires after `60` seconds and is single-use. The WebSocket upgrade supplies it as the `ticket` query parameter; consuming it binds the live connection to the paired device ID.

## Failover

Version-3 clients walk `baseUrls` in the desktop-supplied order. They therefore try Tailscale before LAN when both exist, without asking the user to choose an address. The legacy `baseUrl` deliberately prefers LAN so a version-2 phone without Tailscale can pair on the same Wi-Fi.

## Presence and status

The Devices list refreshes every `10` seconds and also refreshes when the window gains focus or becomes visible. Each paired row can show:

| Field            | Source                                                    |
| ---------------- | --------------------------------------------------------- |
| Device name      | Live client, then persisted paired-device metadata        |
| Platform         | `iOS`, `Android`, `Web`, `Desktop`, or `Unknown platform` |
| Remote address   | Live connection, then last persisted address              |
| Connection state | `Offline` when no live client is attached                 |
| Last seen        | Live or persisted timestamp                               |
| Paired           | Credential creation timestamp                             |

Presence updates are persisted at most once per `60` seconds during normal activity.

## Revocation

Choose **Revoke access** on a paired device and confirm **Revoke access**. Revocation invalidates its credential and already minted tickets, then immediately closes live connections for that device. The device can connect again only by pairing with a new QR code.

## Platform notes

The desktop UI implements both macOS and Windows. On Windows, allow Aiola on private networks when Windows Firewall prompts; otherwise phones on the same Wi-Fi cannot connect. The paired client platform metadata supports iOS, Android, Web, Desktop, and Unknown.

## Security considerations

| Property                 | Implementation                                                                             |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| Short bootstrap exposure | One-time secret with a 120-second lifetime                                                 |
| Scoped long-lived access | Per-device credential tied to the signed-in desktop owner                                  |
| WebSocket isolation      | 60-second, single-use ticket instead of placing the device credential in the WebSocket URL |
| Secret storage           | SHA-256 hashes only; timing-safe comparisons; credential file mode `0600`                  |
| Owner isolation          | Pairing routes require the desktop owner's bearer session                                  |
| Revocation               | Invalidates credentials/tickets and disconnects the live device                            |
| LAN confidentiality      | None at the HTTP transport layer; use Tailscale for the encrypted route                    |
| Credential caching       | Credential responses use `no-store` and `no-cache`                                         |
