Security & privacy

Transport and transfer notes updated October 11, 2026 · current service release: 2.9.133

The short version

Your phone controls an open-source service on your own machine over your LAN, without a cloud relay. Pairing grants a per-box credential. Supported API and WebSocket connections use pinned TLS; legacy HTTP and native image/file-transfer paths remain, so not all traffic is encrypted. Updates, purchases and optional integrations use internet services. The source and the limitations below describe these boundaries.

Architecture

Two pieces: the phone app and couchsided.py, a single-file Python service (standard library only, no third-party imports) running on your box. The agent listens on two TCP ports:

PortWhat
8787Plaintext HTTP + WebSocket. Always on (backwards compatibility with older apps).
8788HTTPS + WSS with a self-signed certificate. On by default since agent 2.9.88; opt out with couchside tls off.

Local box control does not require a cloud relay or a Couchside account. Keep the service on your trusted local network; do not expose its ports to the public internet. Optional online services are described below.

Transport encryption

The agent mints its own RSA-2048 certificate on first start (via the box's openssl) and serves HTTPS beside plaintext. Because the certificate is self-signed, trust doesn't come from a CA. It comes from pinning at pairing time:

Initial trust still matters. Discovery and legacy TLS upgrade information can arrive over plaintext, so an active attacker on the LAN during initial pairing or upgrade remains a risk. Pair only with the box you can verify on screen and check that the app shows an encrypted connection. A saved TLS pin protects subsequent pinned connections; it does not make initial pairing immune to interference.

Native image loading and phone-to-box file uploads still use HTTP. On a pinned box, short-lived tickets are minted over TLS; image tickets authorize image reads and upload tickets authorize one upload. Tickets limit authority but do not encrypt image or file bytes. Treat those payloads as visible to someone able to observe that network traffic.

Pairing and auth

Control requests normally require Authorization: Bearer <token>: a 48-hex-character (24-byte) random token minted on the box at install, stored at /etc/couchside/token (mode 600), compared in constant time. Native uploads can instead use a scoped, single-use ticket; discovery and PIN pairing have separate checks. A phone gets its credential through QR or PIN pairing:

On the phone, the token is stored in the platform keychain (iOS Keychain / Android Keystore via expo-secure-store), per box.

Revocation

The agent has one token per box, not per device. Every paired phone holds the same secret. That makes revocation blunt by design:

couchside new-token

mints a fresh token and restarts the agent. Every previously paired phone stops working immediately, and each one re-pairs by QR or PIN. There is no way to cut off one phone and keep the others; if that matters to you (shared house, revoked roommate), rotate and re-pair. Treat the token like an SSH key: anyone holding it controls the box (next section).

If couchside new-token or couchside tls reports an unknown command, your box has an older copy of the management CLI (both were added 2026-08-15). Plain couchside update will not fetch it when your agent is already current (that command compares agent versions and exits early), so reinstall explicitly:

couchside update --force

or, equivalently, curl -fsSL https://couchside.tv/install.sh | bash. The installer is idempotent: it keeps your existing token, config, and pairings.

What the token grants

The agent implements predefined operations with validation of identifiers and arguments. Privileged tasks use specific sudoers permissions or argument-validating helpers. Some higher-risk capabilities have separate box-side settings: couchside allow-updates, couchside allow-system-updates, and couchside allow-launchers. These controls limit the direct API operations; they do not remove the authority granted by remote keyboard and mouse access. The source describes each operation and its checks.

But be clear about the baseline: the gamepad/remote path includes a virtual keyboard and mouse. A token holder can type into whatever session your box is showing; that's the product. The allowlist bounds what the HTTP API can execute directly; it is not a sandbox around someone who holds your token. This is why revocation and the pairing physical-presence requirements exist.

Discovery: what the box reveals to anyone

So the app can find boxes, the agent answers two unauthenticated probes from anything on your LAN:

Discovery does not return the control token, but it tells LAN scanners that a Couchside agent, version and hostname are present. Adding a box by IP remains possible when discovery is firewalled off. Control routes, scoped-ticket routes and pairing each have their own authorization policy. The agent’s access log redacts query strings.

What can leave your LAN

Local box control does not need a cloud relay. That does not mean the app or website never uses the internet: purchases, updates, game data and optional integrations contact online services. Website checkout and opt-in email news also have their own data flows; see the privacy policy.

Examples of outbound requests include:

FromToWhen / what
agentgithub.comUpdate check (compares versions, cached 6 h, only when the app asks) and downloading signed releases you install. The request's User-Agent carries the agent version.
appstore.steampowered.comGame names, artwork, review scores for your library view.
appitunes.apple.com / the app storesApp-update version check; purchases go through Apple/Google.

Installed-game artwork can be served from your box’s Steam cache. Other game-data and optional integration requests can reach third-party services. This table is an overview, not a complete network allowlist.

What this does NOT protect against

Verify it yourself

Every claim above is testable from any machine on your LAN:

curl http://YOUR-BOX.local:8787/api/ping

That is the discovery surface, exactly as described. Then confirm a credential-less client is refused:

curl -i http://YOUR-BOX.local:8787/api/status

That returns 401. On the box, couchside tls status prints the live HTTPS port and certificate fingerprints; compare against what the handshake presents:

openssl s_client -connect YOUR-BOX.local:8788 </dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -noout

And the source of everything on this page: the agent (auth gate, pairing, discovery, TLS), the installer (token mint, sudoers grants, the CLI), and the app (pinned transport, keychain storage). Releases are signed with an offline Ed25519 key, and the installer this site serves is build-verified against those signatures. Found something? support@couchside.tv.