# The relay

`present-relay` is a small Bun server that gives a live session **public reach** beyond the LAN. Its design centers on one rule: **the relay never executes a deck's code.**

## Run a relay

```bash
liebstoeckel relay --port 8080 --tokens <account-token>
# or set PRESENT_RELAY_TOKENS=tok1,tok2 ; serve behind TLS for public use
```

Then present through it from your machine:

```bash
liebstoeckel live my-talk --relay http://localhost:8080 --relay-token <account-token>
```

This uploads the built deck, gets back public presenter/viewer links + QR, and runs the deck's server plugins **locally** (as a privileged peer of the relay).

## How it splits responsibilities

1. **The relay** hosts each session's Yjs `Hub` (authoritative, in-memory, TTL-expired) and **serves the uploaded deck HTML**. Viewers/presenters connect to its WebSocket directly.

2. **Your local machine** rehydrates and runs any **server plugins**, connected to the relay as a privileged "runner" peer, applying their effects to the shared doc. The relay only moves bytes; it never runs deck code.

## Untrusted-HTML isolation

The relay serves the deck on its own origin, but with a response header:

```
Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline';
  connect-src <relay-ws> <relay-http>; frame-ancestors 'none';
  sandbox allow-scripts allow-popups;
```

Omitting `allow-same-origin` puts the deck in a **unique opaque origin**: it can run scripts and sync over WebSocket, but it **cannot reach the relay's cookies, API, or storage**, and each context gets a fresh opaque origin (so decks are isolated from one another too). `allow-popups` is kept only for the presenter pop-out; `default-src 'none'` blocks any external fetch, since a built deck inlines all its assets. No separate origin or wildcard certificate needed.
**Plugin implication:** In an opaque origin, `localStorage`/`IndexedDB` throw and cookies are denied. Plugin **client** parts must use WebSocket + shared state only; anything needing secrets or external HTTP belongs in the **server** part (which runs on your machine). WebSocket auth uses the **token in the URL**, so there's no CSWSH risk.

## Tokens & quotas

Three token layers keep roles separated:

- **account token**: your deck-runner authenticating to the relay.
- **session tokens**: presenter / viewer roles embedded in public URLs.
- **runner token**: the privileged server-plugin peer (WebSocket-only; can't load the page).

The relay enforces per-account quotas (max concurrent sessions, max deck bytes, session TTL, inbound frame caps) and keeps docs in memory only (ephemeral, for privacy).
**Production:** Local dev is plain `ws://`. For public use, terminate **TLS at a proxy** and pass `--public-url https://…` so links and the WebSocket use `wss://`.

[Server plugins](https://docs.liebstoeckel.app/plugins/server-plugins/)