rex with your own app
rex is a Plex Media Server that owns no files. The official Plex apps see it as a normal Plex server. Another program, which this page calls your app, tells rex what is in your library and serves the files. When you press play, rex streams the file straight from your app. Nothing is downloaded first.
This page takes you from nothing to playing a movie, one step at a time. It does not assume any particular app. It says exactly what rex expects from one, so you can check that yours does it, or build one. Every step says what it does, why it matters and what you should see before you move on.
Four things to know before you start:
- Your app serves the files. rex fetches every byte from your app over plain HTTP, the way a browser downloads a file. rex keeps no files and needs no disk or mount for media.
- Your app tells rex what it has. It sends rex a list of files over rex's feed API, and every change after that. rex never lists a folder or scans anything.
- Your app decides what each file is. It says whether a file is a movie or an episode, which title it belongs to (by IMDb and TMDB id) and, for an episode, its season and number. rex never guesses from a file name.
- Plex describes it. Once rex is on a Plex account, it shows each title with the name, poster, summary and episode names Plex's own metadata has for those ids.
rex has been in closed beta for sponsors since October 15 and reaches the public on October 31. See rex.debridmediamanager.com for access.
Before you start
You do not need a TMDB key or your Plex token.
What your app must do
Give this list to whoever makes your app. rex's own API reference is docs/feed-api.md in every rex download.
Step 1. Install rex
What it does. Downloads rex into ~/rex and starts it as a service that comes back after a reboot, with everything the later steps need. Nothing after this step restarts rex.
Why it matters. rex is what your Plex apps talk to. The first start creates rex.db, which holds your libraries, everything your app sends and your watch history.
rex's download uses the GitHub CLI. Install it (sudo apt install -y gh) and sign in once with gh auth login. Then:
mkdir -p ~/rex && cd ~/rex
TAG=$(gh api 'repos/debridmediamanager/rex/releases?per_page=1' --jq '.[0].tag_name')
gh release download "$TAG" -R debridmediamanager/rex -p '*-linux-amd64.tar.gz'
tar xzf rex-*-linux-amd64.tar.gz --strip-components=1 && rm rex-*-linux-amd64.tar.gz
sudo loginctl enable-linger "$USER"
This takes rex's newest build, a nightly until the next release. On an ARM machine (Raspberry Pi, Ampere) use linux-arm64 in all three places. The last line keeps rex running when you log out.
Find rex's LAN IP: hostname -I prints several, so take the one on your home network (usually 192.168.x.x or 10.x.x.x). This page writes it as 192.168.1.20 everywhere: put yours in its place. Plex apps, your app and your own commands all reach rex there.
Now make it a service. Create ~/.config/systemd/user/rex.service:
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/rex.service
[Unit]
Description=rex
After=network-online.target
[Service]
WorkingDirectory=%h/rex
EnvironmentFile=-%h/rex/rex.env
ExecStart=%h/rex/rust/target/release/rex --catalog rex.db --announce-url http://192.168.1.20:32400
Restart=always
RestartSec=5
UMask=0077
[Install]
WantedBy=default.target
UMask=0077 makes the files rex writes readable by you only: rex.db will hold your app's login.
Plex Media Server runs on the same machine? End the ExecStart line with --port 32500 and use 32500 wherever this page says 32400: Plex already holds port 32400 and the ports apps use to find servers, and on any other port rex leaves those to Plex.
systemctl --user daemon-reload
systemctl --user enable --now rex
journalctl --user -u rex -f
What you should see.
INFO seeded rex.db: an empty library named yourhost at /mnt/zurg, machine=...
INFO no source yet: one registers at PUT /rex/sources/{name} (docs/feed-api.md)
INFO claim state: unclaimed
INFO rex on http://yourhost:32400 machine=... items=0 parts=proxy
rex names itself after the machine; your Plex apps show that name. no source yet is right: no app has registered yet. A WARNING that a CDN may strip Range is about the LAN IP and can be ignored. Leave the log open.
Step 2. See it work by hand
What it does. Plays the part of your app for one movie, with a throwaway file server and two curl commands.
Why it matters. It proves rex works on this machine before your app is involved. If this step works and your app does not, the problem is in your app. Skip it if your app is already set up.
You need ffprobe, jq and one video file. Put the file where a small web server can serve it:
sudo apt install -y ffmpeg jq
mkdir -p ~/rex-test/movies/Sintel.2010
cp /path/to/any-video.mkv ~/rex-test/movies/Sintel.2010/Sintel.2010.mkv
cd ~/rex-test
python3 -m http.server 8099 --bind 127.0.0.1 &
Register it with rex, under the name test:
curl -s -X PUT http://192.168.1.20:32400/rex/sources/test \
-H 'Content-Type: application/json' -d '{"url": "http://127.0.0.1:8099"}'
Send rex the file. It says the file is the movie Sintel (IMDb tt1727587, TMDB 45745) and carries ffprobe's output:
P=movies/Sintel.2010/Sintel.2010.mkv
jq -n --arg p "$P" --argjson size "$(stat -c%s "$P")" \
--argjson probe "$(ffprobe -v quiet -print_format json -show_format -show_streams "$P")" \
'{items: [{id: $p, path: $p, name: "Sintel.2010", size: $size,
kind: "movie", ids: {imdb: "tt1727587", tmdb: 45745}, media_info: $probe}]}' \
| curl -s -X POST http://192.168.1.20:32400/rex/feed/test/items \
-H 'Content-Type: application/json' --data-binary @-
What you should see. The answer is {"created":1,"updated":0,"held":0,"unchanged":0,"rejected":[]} and rex's log prints:
INFO FEED identified "Sintel.2010" -> movie tmdb:45745 (placed)
INFO FEED test upsert: 1 created, 0 updated, 0 held, 0 unchanged, 0 rejected
placed means rex took the title from what was sent, not from the file name. Sintel is now in Debrid Movies, playing from the little web server. No Plex app can open rex before
Clean up before you go on: removing the test source removes everything it sent. The first request answers a confirm path; send a DELETE to that path within five minutes.
curl -s -X DELETE http://192.168.1.20:32400/rex/sources/test
curl -s -X DELETE "http://192.168.1.20:32400/rex/sources/test/confirm/<code from the answer>"
kill %1
If not.
"held":1:media_infois missing or has no streams. Check thatffprobeprinted JSON for the file.- An answer with
rejected: the file is not a video by its extension, is a sample, or has noidorpath. rex's log names each and why. 404 no such source: thePUT /rex/sources/testdid not happen first.
Step 3. Add it to a Plex account
What it does. Registers rex on a Plex account, as you would a real Plex Media Server, and shares it with your own account.
Why it matters. rex gets every title's name, poster, summary and episode names from Plex's own metadata, and it may only ask Plex once it is on an account. Do this before you connect your app, so every title arrives with its name and poster. What your app sends before the claim keeps its file name until rex names it in the background, about 300 titles an hour, and Fix Match has nothing to offer until then. It also removes the "unclaimed" notice and is needed before
Use a fresh Plex account for this, not your own. rex is not made by Plex, and Plex could take action against an account that registers it. Keep your real account, with its watch history, friends and any Plex Pass, out of reach: make a new free account at plex.tv with an email address you do not use elsewhere, register rex on that one, and share rex with your own account. You then watch signed in as yourself, the way you would watch a friend's server. rex plays nothing until this step is done.
Register rex on the fresh account
- Sign in to plex.tv as the fresh account, open plex.tv/claim and copy the code. It expires in four minutes.
-
On the rex machine:
curl -X POST "http://192.168.1.20:32400/myplex/claim?token=claim-xxxxxxxxxxxx" curl -s http://192.168.1.20:32400/identity
What you should see. The second command shows claimed="1", and ~/rex/claim_state.json exists. Back that file up: without it rex has to be claimed again, and rex refuses a second claim while the file is there. From now on titles arrive with their names and posters, without a restart.
If not.
- The first command answers
409 Conflictand rex logsalready claimed: rex is on a Plex account already. - It answers
500 Internal Server Errorand rex logsclaim exchange failed: the code expired or was used. Get a new one from plex.tv/claim.
Share it with your own account
- Still signed in as the fresh account, open app.plex.tv, then Settings (the wrench), Manage Library Access and Grant Library Access.
- Enter your own account's username or email, pick the rex server and both libraries, Debrid Movies and Debrid TV, and send the invite.
- Sign out, sign in as yourself, and accept the invite. It arrives by email and in app.plex.tv.
rex tells plex.tv the address from step 1 the moment it is claimed, with no restart.
What you should see. Signed in as yourself, Plex apps list the rex server beside your own servers, with both libraries. Your watch history on rex is your own: it is kept apart from the fresh account's. Share it the same way with anyone else in your household.
Step 4. Connect your app
What it does. Points your app at rex. Your app registers, sends its whole library and then every change as it happens.
Why it matters. This is the only link between the two. rex has no setting for your app: your app finds rex, not the other way round.
Give your app rex's address, http://192.168.1.20:32400. How you enter it depends on your app.
What you should see. rex's log prints, under the name your app registered as:
INFO SOURCES PUT /rex/sources/myapp -> 200
INFO FEED myapp upsert: 0 created, 0 updated, 200 held, 0 unchanged, 0 rejected
INFO FEED identified "Some.Movie.2019.1080p" -> movie tmdb:12345 (placed)
INFO FEED myapp upsert: 180 created, 0 updated, 20 held, 0 unchanged, 0 rejected
held files are waiting for their ffprobe output and appear as soon as your app sends it. Check what rex has from your app at any time:
curl -s http://192.168.1.20:32400/rex/sources # the apps that registered, and their addresses
curl -s http://192.168.1.20:32400/rex/feed/myapp/items | head -c 600 # what is shown
curl -s http://192.168.1.20:32400/rex/feed/myapp/held | head -c 600 # what waits for ffprobe output
If not.
- Nothing at all in rex's log: your app never reached rex. Check the address you gave it, and that rex's port is open between the two machines.
- Files arrive but no
identifiedlines: your app sends nokindandids(held files and unchanged re-sends print none either). Those titles stay unmatched until it does; you can still fix one by hand with Fix Match in a Plex app signed in as the fresh account. Your own account, which rex is shared with, is refused. /rex/sourcesshows an address rex cannot reach, such as a container's own address: your app has to register an address that works from the rex machine. Test it withcurlfrom there.
Step 5. Watch something
What it does. Opens rex in a Plex app, the same way you would open any Plex server.
Why it matters. This is the point of the whole setup. It also proves the last link: rex reading a file from your app while you watch.
Open a Plex app on a phone, TV or computer on the same home network. rex shows up under the machine's name, with Debrid Movies and Debrid TV. Pick a movie and press play.
At home nothing else is needed: Plex apps find rex with Plex's own local discovery and connect to it over your network. That needs the device on the same network as rex (not a guest Wi-Fi or a separate VLAN) and rex on port 32400. If you moved rex to 32500, apps find it only through the Plex account it is
What you should see. Posters, titles and years as on any Plex server. Playback starts within a few seconds.
Two things you may meet:
- "yourhost server is unclaimed and not secure". A Plex app says this about any server that is not on a Plex account, and rex plays nothing until it is on one. Do
step 3 . - "This server is not powerful enough to convert video". The app cannot play the file as it is, often a web browser meeting AC3 or DTS sound. Plex apps on TVs and phones play most files as they are. To play everything everywhere, set up
Converting video .
Each file goes to Debrid Movies or Debrid TV by the kind your app sent. A file without one goes by its first folder: shows to Debrid TV, anything else to Debrid Movies.
Back up ~/rex/rex.db (it holds your library, watch history and your app's login, so keep it private) and claim_state.json:
sudo apt install -y sqlite3 # once
sqlite3 ~/rex/rex.db ".backup $HOME/rex/rex.db.bak"
In Docker the files sit in ~/rex/config, readable by root only. Back up from inside the container: docker exec rex sqlite3 /config/rex.db ".backup /config/rex.db.bak".
You are done. The sections below are optional.
Use it outside your home (optional)
What it does. Gives rex an HTTPS address that Plex apps away from home can reach, and tells plex.tv about it.
Why it matters. Away from home, app.plex.tv only connects to an HTTPS address, and the other Plex apps prefer one. A real Plex server can fall back to Plex's relay; rex cannot, so you choose how it is reached. rex must be /identity and its feed API (/rex/...), which is why a proxy that faces the internet must block /rex/.
Plex Pass. Away from home, Plex apps play only when the account you watch with has Plex Pass or a Remote Watch Pass, so you need one on your own account, not the fresh one. Plex checks this in the app, before rex sees any request. That is how Plex Web behaves; other Plex apps were not tested. At home, over the LAN IP, Plex's rules ask for no pass. app.plex.tv in a browser uses the HTTPS address even at home, so it may ask for a pass there too.
Whichever you pick, every byte you watch away from home leaves through your home connection's upload speed. As a rough guide, a 20 Mbit/s upload carries one 1080p WEB-DL, not a remux.
Not recommended:
- Cloudflare tunnel. Every byte goes through Cloudflare, which also drops the
Rangeof a file's first read, so every play start pulls the whole file from your app. - Tailscale Funnel, ngrok and other hosted tunnels. They cap bandwidth or monthly data, which a film runs through quickly. Tailscale Funnel also publishes all of rex, including the feed API: anyone on the internet could add to or delete from your library.
- Forwarding port 32400 without rex-direct or a certificate of your own. Plex apps would get a plain HTTP address, which app.plex.tv refuses.
rex-direct
rex-direct is DMM's answer to plex.direct, and it is on by default. On your router, forward port 32400 (TCP) to the rex machine. That is all, with nothing to restart.
Once claimed, rex gets its own name from DMM, <hash>.rex-direct.debridmediamanager.com, pointed at your home's public IP, and a certificate for that name alone. The first certificate takes a few minutes; rex's log says it is being issued and then shows the name in a rex-direct: line. rex then announces https://<hash>.rex-direct.debridmediamanager.com:32400 after the LAN IP. rex asks DMM again every six hours, so a new home IP is picked up by itself, and a renewed certificate too.
If your router forwards a different outside port to rex, add REX_DIRECT_PORT=<that port> to rex.env and restart rex. To use another provider, set REX_DIRECT_URL (rex's docs/rex-direct-api.md is the contract), or REX_DIRECT_URL=off to turn it off. The port forward does not open the feed API: see
Tailscale serve
Install Tailscale on the rex machine and on the devices you watch on, signed in to the same account. Then on the rex machine:
sudo tailscale serve --bg 32400
It prints the address, for example https://myhost.tailnet.ts.net. Put it first in the --announce-url list from :443 added, before the LAN IP:
ExecStart=%h/rex/rust/target/release/rex --catalog rex.db --announce-url https://myhost.tailnet.ts.net:443,http://192.168.1.20:32400
Keep the LAN IP in the list. Once rex announces, Plex apps take its addresses from plex.tv, and plex.tv lists only what rex announces. Without the LAN IP a TV at home that has no Tailscale cannot connect.
systemctl --user daemon-reload && systemctl --user restart rex
Everyone on your tailnet, including anyone you share this machine with, can use rex's feed API. Share the machine only with people you trust.
Caddy
You need port 443 on your router forwarded to the rex machine, and a name for your home's IP address:
- Your own domain: an
Arecord such asrex.example.compointing at your home IP. - No domain: use
1-2-3-4.sslip.iowith your public IP's numbers (curl -s ifconfig.meshows it). If your IP changes, update the name.
Install Caddy and make /etc/caddy/Caddyfile:
rex.example.com {
respond /rex/* 403
reverse_proxy 192.168.1.20:32400
}
respond /rex/* 403 keeps the feed API off the internet; your app still reaches rex directly. Then sudo systemctl reload caddy, make the ExecStart list --announce-url https://rex.example.com:443,http://192.168.1.20:32400, and restart rex as above.
A small VPS
For a connection with no public IP (CGNAT). rex has no relay and will not get one, so a server you rent stands in for your router: it has the public IP, and a tunnel from the rex machine carries everything to rex. Any VPS with a public IPv4 works, a few euros a month. Pick one whose monthly traffic covers what you watch away from home: every byte passes through it, and a plan with a small data cap will run out. Tested 2026-10-19 with a stand-in VPS: playback and seeking pass through unchanged.
You need the VPS's IP (1.2.3.4 below), ports 443, 80 and 7000 open on it, and a long random string for the tunnel (openssl rand -hex 32).
On the VPS, install frp (the frps program) and Caddy. Make frps.toml:
bindPort = 7000
auth.token = "your-long-random-string"
proxyBindAddr = "127.0.0.1"
proxyBindAddr keeps the tunnel's end on the VPS's loopback, so the internet reaches rex only through Caddy. Make /etc/caddy/Caddyfile, with the VPS's IP as 1-2-3-4.sslip.io (a free name that always points at that IP) or your own domain pointed at it:
1-2-3-4.sslip.io {
respond /rex/* 403
reverse_proxy 127.0.0.1:32400
}
respond /rex/* 403 keeps the feed API off the internet. Start frps -c frps.toml as a service and run sudo systemctl reload caddy.
On the rex machine, install frp too (the frpc program) and make frpc.toml:
serverAddr = "1.2.3.4"
serverPort = 7000
auth.token = "your-long-random-string"
[[proxies]]
name = "rex"
type = "tcp"
localIP = "192.168.1.20"
localPort = 32400
remotePort = 32400
Run it as a service, for example ~/.config/systemd/user/frpc.service with ExecStart=%h/frp/frpc -c %h/frp/frpc.toml and Restart=always. Then put the VPS's name first in the --announce-url list, add REX_DIRECT_URL=off to ~/rex/rex.env (rex-direct cannot work without a public IP at home, so plex.tv should not list it), and restart rex:
ExecStart=%h/rex/rust/target/release/rex --catalog rex.db --announce-url https://1-2-3-4.sslip.io:443,http://192.168.1.20:32400
What you should see
curl -s -o /dev/null -w '%{http_code}\n' https://myhost.tailnet.ts.net/identity
answers 200, and rex's log names the addresses it announces:
INFO heartbeat every 600s for machine ... -> https://myhost.tailnet.ts.net:443,http://192.168.1.20:32400
rex prints this at every start. Trouble shows later as heartbeat failed or announced but NOT visible. Otherwise the server lists those addresses on plex.tv, and a Plex app away from home picks the one it can reach. If rex logs waiting for a claim, do
Converting video (optional)
What it does. Lets rex convert what a device cannot play, the way Plex does.
Why it matters. Without it a device that cannot play a file as it is gets "This server is not powerful enough to convert video". Web browsers hit this most, for example with AC3 or DTS sound.
Install jellyfin-ffmpeg 8 as a package (Ubuntu 22.04's own FFmpeg 4.4 is too old; this one goes in /usr/lib/jellyfin-ffmpeg beside any other) and point rex at it. The first line picks the build for your system: there are builds for Ubuntu 22.04, 24.04 and 26.04 and Debian 12 and 13, on amd64 or arm64, and on any other curl stops with an error.
DEB=jellyfin-ffmpeg8_8.1.3-1-$(. /etc/os-release; echo "$VERSION_CODENAME")_$(dpkg --print-architecture).deb
curl -fLO "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v8.1.3-1/$DEB"
sudo apt install -y "./$DEB" && rm "$DEB"
printf 'REX_TRANSCODER=/usr/lib/jellyfin-ffmpeg/ffmpeg\n' >> ~/rex/rex.env && chmod 600 ~/rex/rex.env
systemctl --user restart rex
With an Intel or AMD graphics chip, also run sudo usermod -aG render "$USER" and reboot so the service gets the group; in an LXC, pass the GPU through. On Intel, also run echo LIBVA_DRIVER_NAME=iHD >> ~/rex/rex.env; AMD needs no such line. The chip makes conversion fast; without one rex uses the processor. With no Intel or AMD chip, rex also tries an NVIDIA card's encoder. On Intel, sudo apt install intel-opencl-icd also converts HDR and Dolby Vision on the chip.
What you should see. rex's log has a transcoder: line naming the FFmpeg and the hardware it found (hardware none means the processor). A file that failed before now plays; the log shows what was converted, for example video=copy h264 audio=ac3->aac.
Subtitles in a converted file: an image subtitle is burned into the picture and a subtitle you uploaded comes along. Plex for Android and iOS also keep a text subtitle; Plex Web and other players lose it for now.
Run rex in Docker
rex's image is private, like the download. Log in once with your GitHub username and a classic personal access token with the read:packages and repo scopes:
docker login ghcr.io -u YOUR_GITHUB_USERNAME
Make a folder, for example ~/rex, with this compose.yml:
services:
rex:
image: ghcr.io/debridmediamanager/rex:nightly
container_name: rex
restart: unless-stopped
network_mode: host
command: --announce-url http://192.168.1.20:32400
volumes:
- ./config:/config
command:isstep 1 's.:nightlyfollows the nightly builds;latestfollows releases only.network_mode: hostlets Plex apps on your network find rex by themselves, and lets rex reach your app at the address it registers, as from the machine itself../configgetsrex.dband, later,claim_state.json. They are readable by root only, becauserex.dbholds your app's login.- Plex Media Server on the same machine? Add
--port 32500tocommand:and use32500wherever this page says32400.
docker compose up -d
docker compose logs -f rex
You should see the same lines as
Update with docker compose pull && docker compose up -d. The image carries no FFmpeg, so --announce-url ... goes in command.
Keep the port private
rex takes no password on its feed API. It answers that API only from this machine, your home network (10.x, 172.16-31.x, 192.168.x) and Tailscale (100.64-127.x), and refuses everyone else with 403, so forwarding rex's port for /rex/* blocked in any proxy that faces the internet, as the
Updating
Run the TAG=, gh release download and tar lines of ~/rex (they replace the program and data/, and leave rex.db, rex.env and claim_state.json alone), then systemctl --user restart rex.