What is exposed, what is enforced rather than advised, and what to check before putting this anywhere the internet can reach.
Work down it. Each line says what goes wrong if you skip it.
Before anyone outside your house can reach the page
- Admin password set. Until one exists the panel is open to whoever can load the page — and every line is locked: no calls, texts or voicemail in any access mode until the password is set. → Access
- Call-in access chosen deliberately. A fresh install starts Admin only — opening the line to a guest code or to anyone is a decision you make, not a state you inherit. → Access
- TLS on the front door. Passwords travel with every request, and browsers refuse the microphone on plain http anyway.
- Open the panel over the TLS door (
:8443), not:8100. The plain-http port is published alongside Caddy and is a live admin surface — reach the panel overhttp://…:8100and the admin password and every API key you type cross the LAN in cleartext, sniffable by anything on the wire. Type them at the:8443door. If your own reverse proxy terminates TLS, don't publish:8100to the LAN at all — the station's webhook is the only thing that needs it, and that can go through the proxy too. - The embed allowlist (Players → Embed frame → Allowed origins) set to your real origin(s), or empty.
*lets any page on the internet mint call tokens against you; the server warns at startup if you choose it. Empty is same-origin only — the widget's own page needs no entry. - Fresh LiveKit secret. Never the example one.
-
CALLIN_ADMIN_KEYset as break-glass, so a lockout is recoverable without deleting files on the host. - Reach the panel by hostname only after a password exists. Before one is set, the panel accepts same-origin requests only from a literal address (a name can be pointed at your box by someone else);
CALLIN_PANEL_ORIGINSis the escape hatch during setup.
Money and airtime — every call spends your API keys
-
calls_per_hour,calls_per_day,caller_cooldown_secsnon-zero. 0 means unlimited; the hourly cap alone still permits 24× that in a day. -
max_actions_per_callset — caps requests, segments and on-air messages from one call.
Who gets what — permissions are a tier, not a switch
Each caller permission is granted to the least trusted caller who gets it: off, anyone, guest code, or admin. The tier is decided at the door, travels inside the signed room name, and is resolved before the DJ's tool list is built — a caller cannot raise their own.
Put the far-reaching ones on admin and they are yours alone, while the line stays open to everybody else.
| Permission | Default | What it reaches | Outlives the call |
|---|---|---|---|
allow_announcements |
guest | every listener | no |
allow_skip_track / allow_dj_segment |
admin | every listener | no |
allow_cancel_queue |
guest | another caller's request | no |
allow_sound_search |
anyone | your library's contents (reads only) | no |
allow_listener_countries |
off | where your listeners are, by country (a read) | no |
allow_takeover |
admin | every listener | yes |
allow_genre_lock |
admin | every listener | yes — up to 12 hours |
allow_never_play |
admin | every listener | yes — permanently |
Those are the shipped defaults, read from agent-worker/settings.py — not what an
upgraded store happens to hold. A fresh install ships allow_sound_search reachable by
anyone and allow_cancel_queue at guest, so those are the two rows to look at
before opening a line to strangers.
The three that need station admin credentials either way: allow_takeover, allow_genre_lock, allow_never_play.
-
allow_announcementshands the on-air DJ a line to read to everyone listening. At guest tier that is only callers you handed the code to. -
allow_cancel_queue— the queue is shared, so it can cancel a record a different caller asked for. That is exactly why the station exposes no listener-facing cancel of its own. It ships at guest, so on a code-gated line code-holders have it; worth turning off on an open line. -
allow_sound_searchis a pair of READS: a "sounds like" search, and the neighbours of the track on air. They queue nothing, change nothing, and are not counted against Actions per call. The risk is disclosure of your library's contents — the same as library search, not action. It ships reachable by anyone, matchingallow_library_search: if your library's contents are private, this is the row to change on an open line. -
allow_listener_countriesreads the station's audience rollup into the DJ's briefing, countries only. A country is named only when at least three of the last day's listeners share it, because on a small station one listener's country points at a person. It ships off; at anyone it tells every caller roughly where your audience is. -
allow_genre_lockis quieter than a takeover, which is the risk: a pinned show announces itself on air in a voice listeners recognise, a narrowed playlist does not. Needs a SUB/WAVE new enough to have the control at all — older stations answer that they can't, rather than failing. -
allow_never_play— the furthest-reaching switch here, and the only one with no expiry. It puts the record on air onto the station's never-play list: out of the queue, out of the fallback playlist, never selected again. Nothing goes out on air to say it happened, so an unwanted ban is found by noticing a record has stopped coming round.
The same
allow_never_playswitch lets a caller lift a ban, including one you set yourself. That is deliberate: a mistake made from the phone has a way back that doesn't depend on you spotting it.
Privacy — what you keep about people who call
-
record_callsis on by default (it is how a bad call gets diagnosed) and writes both sides of a stranger's conversation todata/calls/.record_keepcontrols retention; turn it off if you don't want it. - Transcripts and
data/secrets.jsonare plain files on disk, owner-readable only. Protect the volume; never commit.envordata/. - Tell callers. Nothing in the widget says a call is recorded — if you keep transcripts on a public line, that disclosure is yours to make.
- If you forwarded a port, delete the rule when you stop running this.
- Passwords are PBKDF2 hashes; the store refuses a guest code equal to the admin one; an unreadable password file counts as configured, locking rather than opening.
- A stored API key is only ever sent to the host it is saved for — testing a draft URL withholds it and says so.
- A caller's address comes from the socket, not a header they control, unless the connection came from a proxy named in
CALLIN_TRUSTED_PROXIES. - Destructive station tools are not on the call line at all, whatever the settings say.
- Join tokens last two minutes — the guest code and the usage limits are re-checked at every mint.
Both under Access, and the store refuses to let them match.
Admin protects the panel, API keys and test buttons — and opens the phone, so an operator carries one password. Guest is optional and protects only the phone; the code is the whole thing, no username.
Who can call is its own setting beside them:
| Admin only (default on a fresh install) | the phone answers only the admin password — a new line starts closed and is opened as a decision |
| Open | anyone who loads the page can call — the guest door is off and the code does not elevate; the admin password still opens everything |
| Guest code | the code you hand out, or the admin password |
| Automatic | open until you set a guest code, then required |
Open and Guest code are one choice apiece, not a cascade:
- Choosing a code-gated mode without having set that password refuses every call, and the panel says so rather than falling open.
- Every mode — Open included — waits for the admin password to exist first. An unconfigured deployment refuses all calls until it has an owner.
| Trigger | Result |
|---|---|
| 5 wrong tries from one address | 5-minute cooldown |
| a second round | banned until restart |
Guest failures are counted separately from admin ones. Locked out? CALLIN_ADMIN_KEY is always accepted, or restart.
The lockout keys on the immediate socket peer, which a client cannot choose. It believes a forwarded address only when CALLIN_TRUSTED_PROXIES names the proxy — so set that whenever a reverse proxy is in front.