Your media. Your server. Your rules.
Curatio Server is the self-hosted backend that powers the Curatio app. Point it at your own video files and every device running Curatio — Apple TV, iPhone, iPad, Mac — gets a fully featured streaming experience without any cloud subscription, external service, or third-party account.
Your library never leaves your home.
Curatio Server runs on any machine you control — a home server, a NAS, a VPS, a spare Mac mini. It scans your local video files, pulls rich metadata and artwork from TMDB, and serves everything directly to the Curatio app over your network or the internet. The app pairs with the server in seconds using a simple on-screen code, then your full library appears just like any streaming service — hero images, cast info, episode guides, Continue Watching, everything.
Your files on disk
↓
Curatio Server (this project)
↓ metadata from TMDB
↓ streams directly
Curatio app on Apple TV / iPhone / iPad / Mac
- Native Curatio app support — pairs instantly with the Curatio iOS and tvOS app using the device-code pairing flow; no manual URL entry required on the TV
- Automatic library scanning — point at your Movies and TV folders; the server matches files to TMDB metadata and artwork automatically
- Direct streaming with seek support — files are served with HTTP range requests so the Curatio player can jump to any point instantly, no buffering from the start
- Continue Watching — the server tracks exactly where you stopped across every device; pick up on your iPhone where you left off on Apple TV
- Invite-gated accounts — you create one-time invite codes; friends and family register with them; no open sign-ups, no strangers in your library
- Two-factor authentication — optional TOTP (authenticator app) 2FA for every account
- Web interface — a full browser UI is built in, so you can browse and play directly from any browser as well as the Curatio app
- Admin dashboard — manage users, create invites, trigger library scans, and monitor your server from
/admin - TMDB-powered search — search your library and discover titles not yet in it, with clear labelling of what you own and what you don't
| Requirement | Notes |
|---|---|
| Docker + Docker Compose | The recommended way to run Curatio Server |
| TMDB API key | Free — themoviedb.org/settings/api |
| Your video files | Movies and TV shows organised in standard folders (see below) |
git clone https://github.com/your-org/curatio-server.git
cd curatio-server
cp .env.example .envOpen .env and fill in these values:
TMDB_API_KEY=your_key_here
MASTER_PASSWORD=choose_a_strong_password
SECRET_KEY=generate_this_below
BASE_URL=https://yourserver.com
MEDIA_ROOT=/path/to/your/mediaGenerate a secure SECRET_KEY:
python3 -c "import secrets; print(secrets.token_urlsafe(48))"In docker-compose.yml the MEDIA_ROOT path from your .env is automatically mounted into the container. Just start it:
docker-compose up -dThe server will be available at http://localhost:8000. The library scan runs on startup — check the logs to watch progress:
docker-compose logs -f app | grep -E "scan|movie|episode"- Open
http://localhost:8000/adminand sign in with yourMASTER_PASSWORD - Go to the Invites tab and click Create Invite
- Copy the invite code — you'll use it to register your first user account
- Open
http://localhost:8000in any browser and register using your invite code - Open the Curatio app on your Apple TV or iPhone
- The app will show a 6-character code on screen and ask you to visit your server's pairing page
- Go to
https://yourserver.com/pairon your phone, sign in, and enter the code - The Curatio app connects immediately — your full library appears on screen
That's it. Every device you pair this way gets its own account with independent watch progress.
Curatio Server uses the same folder structure as Plex, so if you're already organised that way you're ready to go.
/media/
├── Movies/
│ ├── The Dark Knight (2008)/
│ │ └── The Dark Knight (2008).mkv
│ ├── Inception (2010).mp4 ← flat layout works too
│ └── Parasite.2019.1080p.mkv ← dot separators are fine
└── TV/
├── Breaking Bad/
│ ├── Season 01/
│ │ ├── Breaking Bad - S01E01 - Pilot.mkv
│ │ └── Breaking Bad - S01E02.mp4
│ └── Season 02/
│ └── S02E01.mkv ← season folders are optional
└── Severance/
└── Severance S01E01.mkv ← SxxEyy anywhere in the filename works
Movies: the server looks for Title (Year) in the filename and searches TMDB with that. Separators like dots and underscores are normalised to spaces automatically.
TV shows: the folder name inside TV/ is used as the show name. Episodes are matched by finding SxxEyy (e.g. S01E04) anywhere in the filename. Files without a recognisable episode code are skipped with a log message.
When you add new files to your media folders, trigger a rescan from the admin dashboard under Library → Scan Now, or from the command line:
curl -X POST https://yourserver.com/admin/api/scan \
-H "Cookie: admin_session=YOUR_ADMIN_TOKEN"The scan only processes new and changed files — it won't re-fetch metadata for things already in the database.
Curatio Server streams files as-is without transcoding, so what plays depends on the device. The Curatio app uses AVKit on Apple platforms, which has native support for:
| Format | Notes |
|---|---|
| MP4 (H.264 / AAC) | Best compatibility — works everywhere |
| MP4 (H.265 / HEVC) | Supported on Apple Silicon and A9+ devices |
| MKV | Supported on newer Apple devices; may not play on older hardware |
| WebM | Supported in the web browser UI |
| HLS (.m3u8) | Natively supported by AVKit |
If you have a large collection of MKV files with HEVC or DTS audio that won't play on older devices, the recommended solution is to add an FFmpeg transcoder as a sidecar — Curatio Server is designed to sit alongside one if needed.
For access outside your home network you'll need to expose the server publicly. A few options:
Behind a reverse proxy (recommended)
Use the included nginx.conf as a starting point. Put your domain behind Cloudflare or use Let's Encrypt for TLS.
Tailscale or WireGuard
Run the server on your home network and access it over a private VPN — no ports exposed publicly, and the Curatio app works fine over Tailscale.
Direct port forwarding
Forward port 443 from your router to the machine running Curatio Server and set BASE_URL to your public domain.
Whatever method you use, always run with HTTPS in production — the FORCE_SECURE_COOKIES=true setting in .env ensures session cookies are always secure.
The admin panel lives at /admin. Log in with your MASTER_PASSWORD (not your user account password — these are separate). From here you can:
- Overview — see total movies, TV shows, episodes, and active users at a glance
- Users — enable or disable accounts, promote users to admin, or remove them entirely
- Invites — create single-use invite codes with optional expiry dates for new user registration
- Library — trigger a full rescan and see detailed file counts
The admin panel is separate from user accounts by design, so you can manage the server without exposing your personal account.
The Curatio app communicates with the server over a clean REST API. You can also call it directly from scripts or other tools.
| Method | Route | Description |
|---|---|---|
| POST | /auth/register |
Create an account using an invite code |
| POST | /auth/login |
Sign in with email and password (supports TOTP) |
| POST | /auth/logout |
End the current session |
| GET | /auth/me |
Return the current user's profile |
| POST | /auth/setup-2fa |
Begin two-factor authentication setup |
| POST | /auth/confirm-2fa |
Activate 2FA with a TOTP code |
| Method | Route | Description |
|---|---|---|
| POST | /auth/device/code |
Generate a pairing code for the Curatio app |
| GET | /auth/device/{token}/status |
Poll for pairing completion |
| POST | /auth/device/claim |
Claim a code after signing in (used by /pair page) |
| GET | /pair |
The pairing web page the app directs users to |
All routes below use {user_key} — a permanent token the app stores after pairing.
| Method | Route | Description |
|---|---|---|
| GET | /{user_key}/manifest.json |
App manifest — catalogs and capabilities |
| GET | /{user_key}/catalog/{type}/{id}.json |
Catalog row (supports search and skip) |
| GET | /{user_key}/meta/{type}/{id}.json |
Full metadata — cast, episodes, similar |
| GET | /{user_key}/stream/{type}/{id}.json |
Stream URL for the given item |
| GET | /{user_key}/file/{file_key} |
Range-aware video delivery |
| POST | /{user_key}/scrobble |
Report playback progress |
| Method | Route | Description |
|---|---|---|
| GET | /api/library/movies |
Paginated movie list |
| GET | /api/library/tv |
TV shows (one entry per show) |
| GET | /api/library/tv/{tmdb_id}/episodes |
All episodes for a specific show |
| GET | /api/continue-watching |
In-progress items for the current user |
| GET | /api/recently-added |
Most recently added files |
| GET | /api/search?q=… |
Search local library and TMDB |
| GET | /api/detail?tmdb_id=…&media_type=… |
Full detail page data |
| GET | /stream/{file_key} |
Browser video stream (cookie-authenticated) |
| Method | Route | Description |
|---|---|---|
| POST | /admin/api/login |
Sign in with master password |
| GET | /admin/api/stats |
Library and user statistics |
| POST | /admin/api/scan |
Trigger a library scan |
| GET | /admin/api/users |
List all users |
| POST | /admin/api/users/{id}/toggle |
Enable or disable a user |
| POST | /admin/api/users/{id}/admin |
Toggle admin privileges |
| DELETE | /admin/api/users/{id} |
Remove a user |
| GET | /admin/api/invites |
List invite codes |
| POST | /admin/api/invites |
Create an invite code |
| DELETE | /admin/api/invites/{code} |
Delete an invite |
- File paths are never exposed. The streaming endpoints use opaque random tokens (
file_key) that the server resolves to disk paths internally. It is structurally impossible for a client to request an arbitrary path. - All streaming is authenticated. Requests without a valid session or
user_keyreceive a 404 — nothing is served anonymously. - Media is mounted read-only. The Docker Compose configuration mounts your media folder with
:ro, so the server process cannot modify your files even if something goes wrong. - TOTP secrets are encrypted in the database. Secrets are never stored in plain text.
- Admin and user sessions are separate. Compromising a user account doesn't affect admin access, and vice versa.
curatio-server/
├── app/
│ ├── main.py — FastAPI app, all web routes, embedded browser UI
│ ├── client_protocol.py — Curatio app protocol (manifest, catalog, meta, stream, pairing)
│ ├── models.py — Database models
│ ├── config.py — Environment variable configuration
│ ├── database.py — Database engine and session management
│ ├── auth.py — Password hashing, TOTP, session tokens
│ ├── tmdb.py — TMDB metadata client and cache
│ ├── scanner.py — Library scanner
│ └── streaming.py — Range-aware file delivery
├── Dockerfile
├── docker-compose.yml
├── nginx.conf — Production reverse proxy config
├── requirements.txt
└── .env.example
MIT — free to use, modify, and self-host.
Curatio Server can also be shipped as a desktop app for people who do not want to install Docker. The desktop build bundles the FastAPI server, a small native setup/launcher UI, and a local SQLite database.
- Shows a first-run setup UI for the TMDB API key, admin password, media folder, Movies/TV folder names, and local port.
- Saves settings in the current user's app data folder instead of the install directory.
- Runs the server on
http://127.0.0.1:8000by default and opens the web UI in the user's browser. - Stores the desktop SQLite database and log file next to the user's launcher settings.
Build on Windows so PyInstaller can produce a Windows executable:
.\scripts\build_windows_installer.ps1Requirements:
- Python 3.11+
- Inno Setup with
ISCC.exeonPATH
Output:
dist\installer\CuratioServerSetup.exe
Build on macOS so PyInstaller can produce a signed/notarizable .app bundle for macOS:
./scripts/build_macos_dmg.shRequirements:
- Python 3.11+
- Xcode command line tools
hdiutil(included with macOS)
Output:
dist/CuratioServer.dmg
Note: The included scripts create unsigned local builds. For public distribution, sign and notarize the
.app/.dmgon macOS and code-sign the Windows installer/executable with your publisher certificate.