Security.
What is built in and what is yours to do.
Playback addresses are signed and expire, purchases and ad rewards are verified on the server, the coin ledger is append-only, and console secrets are encrypted and write-only. Your part is the server, the keys and the backups.
01Built in
| Area | How it is protected |
|---|---|
| Video | Locked episodes are only served from signed addresses that expire (default 6 hours). Tampered, expired or reused-for-another-episode addresses get 403. Optional BunnyCDN token mode checks every request at the edge. |
| Coins | Coins are never added on the device. The wallet is an append-only ledger on the server. |
| Purchases | App Store transactions verified against Apple's root certificate (embedded in the server code, checked by fingerprint) and your bundle id; Google Play through the Android Publisher API; Stripe by signed webhook; PayPal by server capture with exact amount and currency. Each purchase is bound to the account that made it. |
| Ad rewards | Credited only from AdMob's signed server-side verification callback, each transaction once, with a daily cap. |
| Console | Sign-in with rate limits, optional two-factor (TOTP), roles, audit log of every sign-in and change, history with rollback. No console page answers without signing in. |
| Secrets | API keys and service accounts are stored encrypted with ADMIN_SECRET_KEY, write-only (replace or test, never read back), owner only. |
| Rate limits | Guest sign-up 30 per IP per hour, registration 10 per IP per hour, sign-in 20 per IP and 8 per account per 15 minutes, checkout 20 per viewer per hour, manual top-ups 5 per viewer per day. |
| Headers | The web player, studio and share pages send a Content Security Policy; the console also sends frame-ancestors 'none' and X-Frame-Options: DENY. |
02Your part
- Keep
.envout of version control and readable only by the server user. - Back up
ADMIN_SECRET_KEY: without it stored secrets cannot be decrypted. - Keep
DEMO_MODE=falseon a live service. - Set
TRUST_PROXY_HOPScorrectly so limits see real client addresses. - Only set
COUNTRY_HEADERwhen every request passes through the proxy that fills it. - Turn on two-factor for owners; give team members the smallest role they need.
- Keep the operating system, Node.js and ffmpeg updated, and run
npm auditafter updates.
03Rotating keys, and what to do if you lose one
The commands below are for Linux or macOS. On Windows run them in WSL or Git Bash.
Create every new key with openssl rand -hex 32, put it in server/.env and restart the server. What happens next depends on the key:
| Key | What changing it breaks | For how long | Steps |
|---|---|---|---|
MEDIA_SIGNING_KEY | Playback addresses already handed out stop working at once. | Until the player asks for a new address: viewers who press play again get a new one immediately. Without rotation an address lives at most the Playback address lifetime (Media & CDN, default 360 minutes). | Set the new value, restart. Nothing else to do. Do it at a quiet hour if you want to avoid interrupting playing viewers. |
ADMIN_SECRET_KEY | Every secret saved in the console (Stripe, PayPal, Google Play service account, Firebase service account, AI keys, BunnyCDN key, webhook secret) can no longer be decrypted. Accounts, settings that are not secrets, and all viewer data stay. | Until you enter the secrets again. Payments, push and AI that need a secret fail in the meantime. | Have every provider secret at hand, set the new value, restart, then enter each secret again in the console and use Test where offered. Check Console → Status. |
ORIGIN_PULL_SECRET | BunnyCDN pulls from /media/origin are refused while Bunny and the server send different values. | A few seconds to minutes, until both sides match. Video the CDN has already cached keeps playing. | Change the Bunny edge rule and .env one right after the other, then restart. |
ADMIN_SETUP_TOKEN | Nothing once an owner exists: it is never accepted again. | - | Remove it from .env after the first sign-in. |
Lost ADMIN_SECRET_KEY? The encrypted secrets cannot be recovered. Set a new key as above and enter the secrets again; nothing else is lost. Lost the owner password? Use reset-owner on the server. Provider key leaked? Revoke it in the provider's dashboard first, then save the new one in the console.
04Viewer data and account deletion
Viewers start as guests; an e-mail and password are optional. Viewers can delete their account and its data from the app (DELETE /api/me), which both stores require. The data lives in your database on your server.