Architecture
The main parts of Airwave — the self-hosted server, the admin web app, the viewer apps (tv-web, tv-native, tv-tauri desktop, tv-roku), and the shared packages — and how they fit together.
Airwave is a small server plus thin clients. The server does all the thinking — resolving channels, building schedules, running jobs, and holding the one connection to your Plex — and the clients just tune in and stream. Everything lives in a single pnpm + Turborepo monorepo built on the Better-T-Stack.
The parts
| Part | What it is | Stack |
|---|---|---|
Server (apps/server) | The backend: the API (REST + tRPC), the deterministic scheduling engine, background jobs, durable workflows, and all Plex integration. This is the only piece that talks to Plex. | Hono on Bun, @hono/trpc-server, workflow durable engine |
Admin web (apps/web) | The admin-only panel for building channels, packages, users, bumpers, jobs, and previewing lineups. Includes a built-in browser player for a quick check. | React, TanStack Router/Query, Vite, TailwindCSS, shadcn/ui, tRPC client |
tv-web (apps/tv-web) | The 10-foot TV app as a web player — runs in any browser and is packaged for LG webOS and Samsung Tizen (both sideloadable today). Native-first <video> with an hls.js fallback. | React, TanStack Router/Query, Vite, hls.js |
tv-native (apps/tv-native) | The same 10-foot app as a native binary for Apple TV, iPad, Android TV, and Fire TV, with an mpv engine for wide-codec direct play (4K HDR HEVC, TrueHD/DTS). | Expo / React Native (react-native-tvos), @airwave/mpv-player |
tv-tauri (apps/tv-tauri) | The same 10-foot app as a native desktop client for Windows, macOS, and Linux — reuses the tv-web React UI in a native shell with its own bundled libmpv for direct play, plus desktop affordances (mouse, PiP mini feed, true fullscreen). | Tauri (Rust shell), React/Vite, libmpv |
tv-roku (apps/tv-roku) | The 10-foot app as a native Roku channel — an independent codebase (zero shared code) held to strict visual + functional parity, playing through Roku's native Video node. | BrighterScript + SceneGraph |
Shared packages (packages/*) | The business logic and shared building blocks every app draws on (see below). | TypeScript workspace packages |
The web apps (web, tv-web), the native mobile/TV app (tv-native), and the native desktop app
(tv-tauri) are separate front-ends over the same server API; tv-roku is its own codebase held to
the same contract. A channel you build in the admin plays identically on every client.
The shared packages
| Package | Role |
|---|---|
@airwave/api | All the business logic — the services behind the routers: Plex resolution, the scheduling/strategy engine, bumpers, access control, jobs. Also carries the tRPC + REST router definitions. |
@airwave/auth | Better-Auth config — Plex OAuth + roles, plus the TV device-code login flow. |
@airwave/db | The Prisma schema, migrations, and generated client (PostgreSQL). |
@airwave/env | Typed environment loading. |
@airwave/ui | Shared shadcn/ui primitives and design tokens, used by web, tv-web, and tv-tauri. |
@airwave/config | Shared TypeScript / build config. |
@airwave/mpv-player | The native mpv playback module (video + headless audio) for tv-native. |
@airwave/key-input | The native remote / hardware-key input module for tv-native. |
Business logic stays in @airwave/api services; the routers in apps/server are kept thin.
How the pieces talk
- Viewers (
tv-web,tv-native,tv-tauri,tv-roku) use the REST API at/api/v1, authenticated with a bearer token obtained through the TV device-code login. A client asks "what's on channel N right now?" and gets back the item plus the exact offset (theeffectiveTime) to start playing at. - The admin (
apps/web) uses tRPC at/trpcwith a cookie session — the type-safe channel/package/ user/job management surface. (It can also hit REST for the browser player.) - Channel artwork is served through a public
/imgproxy. A CSS or<img>background can't send a bearer token, so the server proxies Plex cover art (guide thumbnails, blurred bumper backgrounds) and injects the source's token for it — clients never handle Plex credentials directly. - Persistence is PostgreSQL via Prisma. Channels, schedules, the media-metadata cache, users, sessions, and job state all live there.
Playback is brokered through one Plex owner token
The server holds a single Plex owner token for the connected source. It uses that token to sync metadata, resolve channel filters, and figure out what's on each channel now — but it is not a transcode proxy for the schedule itself. When a client tunes in, the server hands it the item, the offset, and the right Plex connection (resolved automatically local → remote → relay, so the same app works at home and on the road). The client then streams the media straight from Plex, direct-playing natively wherever its measured capabilities allow and only transcoding when it must. Because the owner token stays on the server, viewers never need their own Plex access to the underlying files.
Deployment shape
The whole backend ships as one Docker image whose behavior is chosen at runtime by the CG_ROLE
environment variable:
CG_ROLE | Runs |
|---|---|
server | The API (REST + tRPC), scheduling engine, jobs, and Plex integration. |
web | Builds and serves the admin web app. |
tvweb (optional) | Serves tv-web as an auth-gated browser player, for casting / kiosk setups. |
A PostgreSQL database and a docker-compose.yml wire it together; the server role applies Prisma
migrations (prisma migrate deploy) on start. The viewer apps are built and distributed separately —
tv-native and tv-roku through their platform stores, tv-tauri as signed desktop installers (or
sideloaded from source). See the project
README for the full deploy.
Where to go next
Media sources
How the server connects to Plex, enables libraries, and syncs metadata into its cache.
Channels
How a channel's definition becomes a resolved pool and a continuous, deterministic timeline.
Platforms
Every client platform — native vs. web, and current status.
Background jobs
The scheduled tasks that grow schedules, sync metadata, refresh connections, and reap sessions.
Source map
| Concern | Location |
|---|---|
Server entry (route mounting, CORS, /img, /api/v1, /trpc) | apps/server/src/index.ts |
REST API (viewer-facing /api/v1) | packages/api routers + apps/server/src |
TV device-code auth (/api/tv/auth) | apps/server/src/tv-auth.ts |
| Business logic / services | packages/api/src/services/* |
| Auth (Plex OAuth, roles, device-code) | packages/auth |
| Data model (Prisma schema + migrations) | packages/db/prisma |
| Admin web app | apps/web/src |
tv-web player | apps/tv-web/src |
tv-native app | apps/tv-native |
tv-tauri desktop app (React UI + Rust/libmpv shell) | apps/tv-tauri |
tv-roku channel (BrighterScript + SceneGraph) | apps/tv-roku |
| Native mpv / key-input modules | packages/mpv-player, packages/key-input |
See also: Platforms · Getting started · Channels · Users & access control
Downloads
Every Airwave build in one place — the client apps (TV, desktop, browser) and the self-hosted server, which OS each is for, and where to get it.
Self-hosting (Docker)
Run Airwave on your own hardware — one prebuilt image, a Postgres database, and docker compose. The deploy model, prerequisites, and where each piece lives.
