Security & privacy

Describes agent 2.9.90 · app 2.9.50 · last verified against the source August 2026

The short version

Your phone talks directly to an open-source service on your own machine, over your own LAN. Auth is a per-box bearer token you get by scanning the box's screen. Traffic is encrypted with a certificate pinned at pairing. There is no cloud, no account, and no analytics. Everything on this page is checkable against the source, and the last section says plainly what this design does not protect against.

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.

There is no third machine. No relay, no broker, no push service, no account server. If the phone and the box aren't on the same network, nothing works. That is the design, not a limitation we plan to "fix" with a cloud tier.

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:

One caveat: a box that was paired before TLS existed gets upgraded in place. The app sees the box advertise TLS on its status poll and pins the advertised fingerprint. That advert arrives over plaintext, so this upgrade path is trust-on-first-use: it defeats a passive eavesdropper, but an active man-in-the-middle present at the exact moment of upgrade could pin itself. Re-pairing by QR replaces the TOFU pin with the physical-presence one. New pairings always use the QR path.

Two flows still touch plaintext on a TLS box, deliberately and narrowly: Steam cover-art images and phone→box file uploads (the platform image loader and streamed uploader can't pin a self-signed certificate). Both authenticate with short-lived single-purpose tickets minted over the pinned channel; the bearer token itself never rides cleartext for a pinned box. Cover art is public imagery; the residual exposure is the bytes of files you upload.

Pairing and auth

Every state-changing route requires 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. A phone gets it one of two ways, both requiring you to see the box's screen:

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

Commands the agent runs are chosen from a frozen allowlist in the agent source: a client-supplied id is looked up, never interpolated, and subprocess is always an argv list, never a shell string. Root-side actions (reboot, session restart, journal reads) go through exact-path sudoers grants on fixed commands or argument-validating root-owned wrappers, never through wildcards. Riskier capabilities are off until you enable them on the box: couchside allow-updates, couchside allow-system-updates, couchside allow-launchers. The phone can never grant itself any of these.

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:

No token, no control, no user data, but it does tell any LAN scanner "a Couchside agent, version X, named Y, lives here." If that bothers you, it is the cost of one-tap discovery; adding a box by IP works with discovery firewalled off. The only other unauthenticated writes are the two PIN-pairing endpoints described above. Everything else returns 401 without the token, and the agent's own request log redacts query strings so tokens never land in the journal.

No telemetry, and exactly what does leave your LAN

No analytics, crash reporting, or advertising SDKs in the app; no third-party imports in the agent; no first-party collection endpoint exists at all; couchside.tv is static pages. We cannot see your usage because there is nothing to send it to.

"No telemetry" is not "never touches the internet." The complete list of outbound connections, all to well-known third parties, none carrying usage data:

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.

Cover art for installed games is served by your box from Steam's local cache; the phone doesn't fetch it from a CDN.

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.