diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2e96df2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,155 @@
+
+
+
+
+PiStation
+
+
+ Turn any TV or monitor into a screen the whole room can use.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+A Raspberry Pi drives a TV or monitor and shows a six digit code. Anyone on the network types
+that code into the website to take the screen. They can share a screen or a phone camera, draw
+over what is being shown, or open a shared whiteboard. When nobody is presenting, the display
+becomes a dashboard.
+
+No accounts. No cloud. Nothing leaves the network it runs on.
+
+## What it does
+
+- **Screen sharing** from any browser, no install and no cable. Anyone in the room can take a
+ turn, one at a time.
+- **Camera sharing** from phones and laptops. Point a phone at a whiteboard or a workbench and
+ it goes on the big screen, with front and rear cameras and a flip button. A screen share always
+ takes priority over a camera.
+- **Live annotation** over whatever is on screen, with an adjustable pen, arrows, shapes and a
+ highlighter. Everyone's cursor carries their name.
+- **Shared whiteboard** powered by Excalidraw, mirrored onto the display.
+- **Idle dashboard** with clock, weather, agenda and rotating wallpapers, plus a builder for
+ your own widgets.
+- **Night mode** that drops the screen to just the time and the join code outside working hours.
+- **Rotating codes**, changed every minute, so an old photo of the screen is worthless. Sessions
+ survive the rotation.
+- **Health reporting** from every kiosk: CPU, memory, Wi-Fi signal, temperature and uptime.
+
+## Self hosting
+
+Everything ships as a Docker Compose stack: a LiveKit media server, a Rust backend and the
+website.
+
+```
+git clone pistation
+cd pistation
+cp infra/.env.example infra/.env
+```
+
+Edit `infra/.env` and set at least `LIVEKIT_API_SECRET`, `SESSION_SECRET` and
+`BOOTSTRAP_ADMIN_PASSWORD`. Then:
+
+```
+docker compose -f infra/docker-compose.yml up -d
+```
+
+| Service | Port |
+| --- | --- |
+| Website | 3000 |
+| API | 8080 |
+| LiveKit | 7880 |
+
+Sign in at `/admin` with the bootstrap admin details from your `.env`.
+
+If you are serving anything other than localhost, set `PUBLIC_API_URL` and
+`PUBLIC_LIVEKIT_URL` in `infra/.env` to addresses your devices can actually reach, and change
+`turn.domain` in `infra/livekit.yaml` to the same host. LiveKit reads that file literally, so it
+does not pick up environment variables.
+
+The LiveKit container uses host networking, because WebRTC needs to advertise an address your
+devices can reach and a container on a bridge network has none. That is Linux only; on macOS or
+Windows, run LiveKit outside Docker.
+
+## Setting up a screen
+
+Works on the **Raspberry Pi Zero 2 W, Pi 4 and Pi 5**, and needs the **64 bit** Raspberry Pi OS.
+The media stack has no 32 bit ARM build, so the installer stops with an explanation rather than
+failing halfway if it finds a 32 bit system.
+
+Upload a kiosk build once on the admin home page, then create a kiosk. The admin panel hands you
+a single command to paste into an SSH session on a fresh Raspberry Pi OS Lite install. It **must
+be run with sudo**, since it installs packages, writes to the boot partition and adds a service:
+
+```
+curl -fsSL http://your-server:8080/install.sh | sudo bash -s -- --key
+```
+
+That installs the kiosk, writes its configuration, enlarges swap, tunes the boot config and
+reboots straight into the display. It is safe to re-run, which is also how you repoint a kiosk at
+a new server.
+
+The installer reads the board model and picks video settings and swap to match:
+
+| Board | Video | Swap |
+| --- | --- | --- |
+| Pi Zero 2 W | 960px at 12fps, quality 70 | 2 GB |
+| Pi 4 | 1600px at 30fps, quality 80 | 1 GB |
+| Pi 5 | 1920px at 60fps, quality 85 | 512 MB |
+
+Raspberry Pi OS ships with 100 MB of swap, which a 512 MB Zero 2 W exhausts as soon as the
+browser engine and the media stack are both running. Without more, the kernel starts killing
+processes and the kiosk appears to restart at random.
+
+Useful flags: `--server`, `--join-url`, `--package-url`, `--user`, `--profile`, `--swap`,
+`--skip-reboot`.
+
+## Configuration
+
+Set in `infra/.env`:
+
+| Variable | What it does |
+| --- | --- |
+| `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` | Credentials shared by the server and LiveKit |
+| `SESSION_SECRET` | Signs admin sessions, make it long and random |
+| `BOOTSTRAP_ADMIN_EMAIL` / `_PASSWORD` | Creates the first admin account on startup |
+| `PIN_ROTATION_SECONDS` | How often join codes change, 30 to 60 |
+| `PIN_GRACE_SECONDS` | How long an old code keeps working after it changes |
+| `PUBLIC_API_URL` / `PUBLIC_LIVEKIT_URL` | Addresses browsers and kiosks use to reach you |
+| `CORS_ORIGINS` | Browser origins allowed to call the API. Kiosks are exempt, since a desktop app has no fixed web origin |
+
+Video encoding on the kiosk is tunable per device, without rebuilding:
+
+| Variable | Default | Notes |
+| --- | --- | --- |
+| `PISTATION_VIDEO_MAX_WIDTH` | `1920` | Incoming video is scaled down past this |
+| `PISTATION_VIDEO_FPS` | `60` | Frames beyond this are dropped, never queued |
+| `PISTATION_VIDEO_QUALITY` | `85` | 1 to 100 |
+
+A Pi Zero 2W wants roughly `960`, `12` and `70`. Frames are dropped rather than queued at every
+stage, so an overloaded kiosk loses frames instead of falling further behind.
+
+## Images
+
+Published to GitHub Container Registry on every push to `main` and every `v*` tag:
+
+```
+ghcr.io/sirblobby/pistation-server
+ghcr.io/sirblobby/pistation-web
+```
+
+## Licence
+
+MIT. Source at [github.com/SirBlobby/pistation](https://github.com/SirBlobby/pistation).