API Reference

The current contract for the DNS-Manager control plane (Cloudflare Worker dns-manager-api + D1 + KV HEALTH_KV + R2). Two surfaces, one database:

SurfaceHostAuthUsed by
Public device APIdnsm.cbkr.xyzWAF; provisioning/messages are HMAC-signedthe IPTV apps / TV boxes
Admin API + GUIm7k2.cbkr.xyzCloudflare Access + JWT (RBAC)operators only

open = unauthenticated (WAF-gated), HMAC = signed device request, JWT = operator bearer token, ingest = scoped capture token (bookmarklet / webhook β€” neither JWT nor HMAC). Endpoints marked (PR #NNN) are in an open pull request, not yet on main.

Retired: the legacy PHP panel (dns_list.php, dns_active.php) is gone β€” the Worker + D1 below is canonical.

Architecture

One Cloudflare Worker (dns-manager-api) backs both surfaces; a Cloudflare Pages site (public/) serves the admin GUI. Everything runs on the free tier (~10 ms CPU/request, no native bcrypt, WAF Block/Skip/Challenge only).

TV box / app ──HTTP──▢ dnsm.cbkr.xyz (WAF allow-list of app paths) β”‚ Worker: app-endpoints Β· provision Β· messages Β· connect β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ D1 dns-manager-prod (SQLite) β”‚ β”‚ KV HEALTH_KV (portal-health cache β€” only KV)β”‚ β”‚ Cache API replay-nonce Β· msg-poll Β· limiter β”‚ β”‚ R2 dnsm-message-assets (banner/poster art) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–² operator ──Access+JWT──▢ m7k2.cbkr.xyz (Pages GUI) ──▢ /api/admin/* (same Worker) β–² Cron (*/30) β†’ portal health + expiry/rate-limit/connect-code/abuse-jail/audit-DLQ sweeps

Components

LayerWhat
Worker dns-manager-apiall request handling β€” public app surface, HMAC device endpoints, JWT admin API, Cron (portal health + expired-message sweep).
D1 dns-manager-prodsystem of record: portals, devices, playlists, messages, audit. SQLite β€” schema applied via database/*.sql migrations (tracked in the schema_migrations ledger; see /api/admin/migration-status).
KVHEALTH_KV β€” cached portal-health probe (the only KV namespace; ~48 cron writes/day). Bundle F retired CONNECT_KV: Connect codes, abuse-jail, and the audit dead-letter queue moved to D1 (connect_codes/abuse_jail/audit_dlq); the replay-nonce, message poll-cache, and rate-limit counters moved to the Cache API.
R2 dnsm-message-assetsbanner/poster art, served publicly at /api/assets/<key>.
Cloudflare AccessZero-Trust email login fronting m7k2.cbkr.xyz (the boxes can't do Access, so the public surface is WAF-only).
WAFallow-lists the app paths on dnsm and edge-blocks /api/admin/* + /api/dns/* (returns 403 before the Worker runs).

Security model

Request flow β€” a box coming online

1. box POSTs /api/provision/start (HMAC) → gets a pairing code (shown on the TV). 2. operator finds the code on the Devices page → assigns a portal + Xtream creds + a curation profile. 3. box polls /api/provision/poll → status flips pending→assigned; it pulls the config (creds decrypted) + the CurationProfile, then activates (assigned→active). 4. box reports per-playlist status (/api/provision/status) and its running configHash on /api/messages/poll → the panel shows online/in-sync/stale. 5. operator can push messages, re-curate, reassign, rotate the code, or revoke at any time.

1 Β· Public app surface (IPTV-Smarters rebrand + Helix)

Pre-shipped apps re-pointed to https://dnsm.cbkr.xyz/api/. Do not rename these β€” the APKs append the path verbatim (shapes recovered in CONTRACTS.md). All open; no-store so GUI changes take effect on the next poll.

POST/api/dns.php

The Smarters resolver. The app posts a form body (m=gu, u=playlist, d, k, sc, pw, r, av, dt, do); the su field is load-bearing. Per-device routing is via the ?d= / ?device= query param (the worker routes by query, not the body field).

{ "status": "true", // MUST be the string "true" or the app aborts at splash "su": "http://portal:port,http://mirror:port", // portal(s); comma-separated = failover "ndd": "", "sc": "<32-hex>" // checksum (cosmetic β€” the app never enforces it) }
GET/load.json Β· GET/POST/api/home.php

Helix bootstrap (home.php?action=appinfo is Helix's real config URL; action=update|getvpn are re-pointed apkloc/vpn resources that return {}). Each portal becomes domain1…domainN with all 12 Helix keys (one missing key makes Helix discard the whole response). ?d=/?device=<deviceId> serves that device's playlists.

{ "domain1":"http://portal:port", "domain1name":"Portal", "domain1showtv":1, "domain1showvod":1, "domain1showseries":1, "domain1showcatchup":1, "domain1m3u8":0, ... }
POST/api/note.php Β· POST/api/vpn.php Β· POST/api/update.php
note.php β†’ { "status": true, "response": [ {title, msg, date} ] } // status is a JSON boolean vpn.php β†’ { "status": true, "response": [ /* OpenVPN profiles */ ] } update.php β†’ { "status": "true", "force": 0 } // force:1 = mandatory update
GET/api/intro.php

Splash video β€” returns video/mp4 bytes (or a 302 to INTRO_VIDEO_URL); never JSON, never 404 (the app loads it in a VideoView).

GET/api/assets/<key>

Public R2 passthrough for message banner/poster art (minted by the admin asset upload). Key is pinned to the minted shape (no traversal).

2 Β· Device provisioning (3DNX player β€” operator-assigned)

Every request is HMAC-signed. Headers: X-DNX-Sign (HMAC-SHA256 of the canonical string), X-DNX-Ts (unix s, Β±300 s), X-DNX-Nonce (single-use), X-DNX-KeyId, X-DNX-DeviceId. The signed deviceId is authoritative. Per-IP + per-device rate limits.

POST /api/provision/start {appVersion} β†’ {code:"ABCDEFGH", expiresIn:600} // displayed grouped (ABCD-EFGH) on the TV POST /api/provision/poll {code} β†’ {status:"pending"|"assigned"|"revoked"|"expired"|"error", config?} POST /api/provision/revoke {} β†’ {status:"revoked"} POST /api/provision/status {playlists:[{url, status:"active"|"error"}]} β†’ {ok, updated} POST /api/provision/register-code {code} β†’ {ok, code} // #180: bind the box's public 9-char support code 1:1 (idempotent; 409 if taken by another device) GET /api/profile/:deviceId β†’ ProvisionConfig | {status:"pending"|"revoked"|"error"} // deviceId is NOT read from the body β€” it is taken from the HMAC-verified X-DNX-DeviceId header. // config (assigned): { source_type:"xtream"|"m3u", portal_url|source_url, xtream_username, // xtream_password, display_name, profile?:CurationProfile, epg_url?(m3u XMLTV guide) } // + multi-playlist via playlists[]

3 Β· Device messages (operator β†’ box push)

HMAC-signed like provisioning. The box polls for messages (banner/poster/notification/ad) targeted at it (or broadcast) and acknowledges them. ad renders in the dashboard sidebar slot (1:9 art); it requires image_url, forces requires_ack off, and uses priority to pick the winning ad.

POST /api/messages/poll {configHash?} β†’ { messages: [ { id, type:"banner"|"poster"|"notification"|"ad", title, body, image_url, priority, requires_ack, created_at, expires_at?, action?, action_json? } ] } POST /api/messages/ack {messageId} β†’ { ok: true } // idempotent

A box reporting configHash on poll drives the config-drift badge in the panel.

4 Β· 3DNX Connect (phone self-service onboarding)

A keyboard-less box shows a PIN+QR; the user types creds on a phone at /connect; the box pulls them. The claimSecret (box-only) is required to claim β€” a leaked on-screen code alone can't retrieve creds. Codes live in D1 (connect_codes; single-use, in-band expiry). All open (no creds yet) but per-IP rate-limited via the Cache+D1 limiter.

box POST /api/connect/new {deviceId} β†’ {code, claimSecret, activateUrl, expiresInSec} phone GET /connect#code=... (activation HTML page) phone POST /api/connect/submit {code, type, name, server|url, username, password, epgUrl?} box POST /api/connect/claim {deviceId, code, claimSecret} β†’ {status, config?}

4b Β· Device backup / restore (encrypted, device-scoped)

HMAC-signed like provisioning. The app builds a password-encrypted JSON envelope (AES-256-GCM) locally and uploads the opaque base64 ciphertext for cross-device restore β€” the Worker never decrypts or inspects it. One latest backup per device (re-upload overwrites). Body deviceId must equal the signed device; payload capped at ~900 KB. Stored in D1 device_backups.

POST /api/backup {deviceId, encryptedPayload(base64), formatVersion, createdAt, clientVersion?} β†’ {success:true, storedAt} // storedAt = server unix seconds GET /api/backup β†’ {deviceId, encryptedPayload, formatVersion, createdAt, clientVersion} | 404 DELETE /api/backup β†’ {success:true, removed:bool} // errors: 400 missing/invalid field or non-base64 Β· 403 deviceIdβ‰ authed device Β· 413 >900KB Β· 401 bad HMAC // curl (signing headers abbreviated β€” see Β§2 for the full HMAC header set): curl -X POST https://dnsm.cbkr.xyz/api/backup \ -H 'X-DNX-Sign: …' -H 'X-DNX-Ts: …' -H 'X-DNX-Nonce: …' -H 'X-DNX-KeyId: k1' -H 'X-DNX-DeviceId: DEV' \ -H 'Content-Type: application/json' \ -d '{"deviceId":"DEV","encryptedPayload":"","formatVersion":"1","createdAt":"2026-06-17T00:00:00Z","clientVersion":"1.8.0"}' curl https://dnsm.cbkr.xyz/api/backup -H 'X-DNX-Sign: …' -H 'X-DNX-DeviceId: DEV' … # GET latest curl -X DELETE https://dnsm.cbkr.xyz/api/backup -H 'X-DNX-Sign: …' -H 'X-DNX-DeviceId: DEV' …

5 Β· Admin API (operator panel)

All JWT (Bearer) + Cloudflare Access. Token from /api/auth/login (HS256, 1 h TTL). RBAC: provisioning/messages require admin or operator.

Auth

POST /api/auth/login {username, password} β†’ {success, token, must_change_password, user} POST /api/auth/change-password {current_password, new_password} β†’ {success, message} // bumps token_version

DNS entries / portals (all admin-authed)

MethodPathPurpose
GET/api/dns/listlist portals (admin-only β€” returns inactive rows + descriptions)
POST/api/dns/createadd a portal
PUT/api/dns/update/:idedit
POST/api/dns/toggle/:idactivate / deactivate
DELETE/api/dns/delete/:iddelete
GET/POST/api/dns/healthcached / fresh portal health probe
GET/api/logspublic-surface API access logs

Devices (provisioning)

MethodPathPurpose
GET/api/admin/devices?status=&q=&tag=&expiring=&provider=&limit=&offset=paginated list β†’ devices[], total, limit/offset, per-status counts. Filters: status, free-text q, tag, expiring=<days>|expired, provider=<id>
GET/api/admin/devices/by-code/:codefind a pending device by its pairing code
GET/api/admin/device-codes/:codesupport lookup (#180) β€” resolve a box by the human-dictatable 9-char code a user reads out (Crockford, public, identifier-only) β†’ {device_id, status, …}. The box binds it via the public POST /api/provision/register-code. The device list + detail also include device_code per row; the Devices Info modal shows it as a Support code (#184)
GET/api/admin/devices/:idone device
GET/api/admin/devices/:id/eventsper-device telemetry timeline
PATCH/api/admin/devices/:idedit identity (device_name / device_number / owner_name / notes / tags / provider_account_id) and clear/set the geo_flagged review flag (0/1)
POST/api/admin/devices/:id/assignassign source (xtream/m3u) + creds + curation profile + helix flags + optional epg_url (m3u XMLTV guide)
POST/api/admin/devices/:id/clone-from{source_device_id} β†’ restore/clone: copy the source device's active playlists onto this one, re-encrypting Xtream creds under this device's key. Append-only. The Restore button for a box that reappeared under a new device_id (reinstall / debug↔signed)
POST/api/admin/devices/:id/rotate-codeissue a fresh pairing code
POST/api/admin/devices/:id/revokerevoke
POST/api/admin/devices/:id/expiryset / extend / clear this device's subscription expiry (renewal reactivates an expired box)
DELETE/api/admin/devices/:idhard delete
POST/api/admin/devices/bulk-tagsadd/remove tags across many devices
POST/api/admin/devices/bulk-expiryset/extend expiry across many devices
GET/api/admin/devices/export.csvCSV export of the fleet (formula-injection neutralized)
POST/api/admin/devices/importCSV import of device identity/metadata
POST/api/admin/sources/import (M3)bulk-import m3u OR xtream sources from CSV. device_id + EITHER source_url (m3u) OR server/portal_url + username + password (xtream); optional source_type (else inferred), display_name, epg_url, priority, tags. Each row appends a playlist (a device_id may repeat); xtream creds are AES-GCM encrypted per row (needs PROVISION_KEK; xtream rows skip if unset). β†’ {imported, skipped[]}
POST/api/admin/devices/categories/preview (PR #120)fetch + classify provider categories for the content filter
POST/api/admin/source/preflight (M2)validate + auto-detect a source URL: {url} β†’ {detected_type, is_hls, reachable, status} (xtream/m3u, .m3u8β†’HLS; fail-soft reachability probe, advisory only β€” never assigns)

Multi-playlist (per device)

GET/POST/api/admin/devices/:id/playlistslist / add a playlist
PUT/DELETE/api/admin/devices/:id/playlists/:pidedit / remove
PATCH/api/admin/devices/:id/playlists/:pid/priorityreorder (priority only)

Rollouts (canary / staged rollout Β· #160)

Ring a config change (helix render-flags or an m3u source) to a pilot tag cohort, watch adoption via the existing config-drift (configHash) signal, then promote to the full cohort or roll back to a per-device snapshot. The ring only controls which devices get a change and when β€” never the device-facing response shape.

MethodPathPurpose
GET/POST/api/admin/rolloutslist / create a draft β€” {description?, change_spec:{op:"set_flags"|"set_source_m3u", …}, target:{tag}, pilot:{tag}} β†’ previews {id, ring_state:"draft", counts:{target,pilot}} (applies nothing)
GET/api/admin/rollouts/:idstatus + per-ring adoption metrics (synced / drifted / awaiting)
POST/api/admin/rollouts/:id/apply-pilotsnapshot + apply the change to the pilot ring only (draft→pilot)
POST/api/admin/rollouts/:id/promotesnapshot + apply to the rest of the target cohort (pilot→promoted)
POST/api/admin/rollouts/:id/rollbackrestore every applied device's pre-change snapshot (β†’rolled_back)

State transitions are compare-and-swap guarded (a double-submit returns 409). Cohorts are capped at 500 and applied in chunked D1 batches. Encrypted creds travel verbatim in the snapshot, so rollback never needs the KEK.

Health-aware auto-failover (#161)

POST/api/admin/failover/runrun the auto-failover sweep now β†’ {reverted, failed_over} (the same sweep a */30 Cron runs). When a device reports its primary source failed (device_playlists.status='error', the box's own report β€” never the datacenter probe), it's failed over to its next device-reported-healthy priority source, recorded as an auto-failover rollout (so it's auditable + rollback-able on the rail above), and auto-reverted once the original recovers. Anti-flap cooldown FAILOVER_COOLDOWN_S (default 3600s).

Resellers (XUI.one auto-provision)

Register a reseller panel, then auto-provision a device: the Worker calls the panel's admin API to create a line and assigns the resulting Xtream creds to the device. The panel API key is AES-GCM encrypted at rest (never returned/logged). XUI.one plain-api_key mode.

MethodPathPurpose
GET/POST/api/admin/resellerslist / create β€” {label, type:"xui", admin_base (…:9000), access_code, api_key, stream_base}; key encrypted on save, returned only as has_key
PATCH/DELETE/api/admin/resellers/:idedit / delete (changing access_code requires re-supplying api_key β€” it re-anchors the key's AAD)
POST/api/admin/resellers/:id/testconnectivity check β€” decrypts the key + calls get_packages β†’ {ok, packages} (fail-soft, never 500s)
POST/api/admin/devices/:id/provision-from-reseller{reseller_id, package?, duration?, max_connections?, bouquets?} β†’ calls create_line β†’ assigns the new Xtream line to the device (creds encrypted). Returns {server, username} β€” never the password. Needs the panel's :9000 cert valid (Workers reject self-signed) + a stream_base for building the client URL.

No-API ingest (capture lines from panels with no API β€” Inbox)

For reseller panels with no API. A scoped ingest token lets an operator bookmarklet (or a reseller webhook) post a captured line into a staging Inbox; the Worker auto-detects m3u/Xtream, encrypts creds, and dedups. The operator then assigns each inbox item to a device. Generate the token + bookmarklet/webhook URL on the Inbox page.

MethodPathAuthPurpose
POST/api/ingest/sourceingestpublic + CORS β€” the bookmarklet posts {raw|type,server,username,password|url, label} (Bearer ingest token, scope ingest). Auto-detect + encrypt + dedup β†’ Inbox
POST/api/ingest/webhook/:secretingestpublic β€” a reseller "line created" callback; secret in an X-Webhook-Secret header (preferred β€” kept out of logs) or the path (scope webhook). JSON or form body
GET/api/admin/inboxJWTlist pending captures (creds shown only as has_creds)
POST/api/admin/inbox/:id/assignJWT{device_id} β†’ append the captured source to that device (creds re-encrypted under its key)
POST/api/admin/inbox/:id/discardJWTdrop a captured line
GET/POST/api/admin/ingest-tokensJWTlist / mint a scoped token {label, scope:"ingest"|"webhook"} β€” the plaintext is shown once
DELETE/api/admin/ingest-tokens/:idJWTrevoke a token (its bookmarklet/webhook stops working)

Messages

MethodPathPurpose
POST/api/admin/messagescreate β€” {target_device_id?, type, title, body, image_url, priority, requires_ack, action, expires_at} (no target = broadcast; image must be https; type:"ad" requires image_url, forces requires_ack off, priority Β±1000)
GET/api/admin/messages?device=&all=1&limit=list with ack counts
POST/api/admin/messages/assetupload banner/poster/ad art to R2 β†’ public /api/assets/<key> URL (also surfaced as a click-to-copy chip in the composer)
GET/api/admin/messages/assetslist uploaded R2 art β€” {assets:[{key,url,size,uploaded}], truncated}, newest-first (the Messages page β€œAsset library”)
DELETE/api/admin/messages/:idsoft-delete (active=0; preserves ack history)

Providers (wholesale lines)

MethodPathPurpose
GET/api/admin/providerslist provider accounts + live device counts (total / currently-serving, for oversell)
POST/api/admin/providerscreate a provider account
PATCH/api/admin/providers/:idedit a provider account
DELETE/api/admin/providers/:iddelete a provider account

Ledger (per-device cost)

GET/api/admin/ledgerlist ledger entries
POST/api/admin/ledgerrecord a ledger entry
DELETE/api/admin/ledger/:iddelete a ledger entry

Fleet ops Β· triage Β· audit

MethodPathPurpose
GET/api/admin/dashboardfleet summary counts for the Dashboard
GET/api/admin/triagetriage cards β€” expiring-soon / expired / offline-active / oversold / geo-flagged
GET/api/admin/jailcurrently abuse-jailed IPs (D1)
DELETE/api/admin/jail/:iprelease a jailed IP (clears jail + fail counter)
GET/api/admin/connect/activelive Connect codes (masked)
POST/api/admin/connect/:code/revokerevoke a Connect code
GET/api/admin/provision/logs?device=&event=&limit=provisioning audit log
GET/api/admin/migration-statusapplied-migrations ledger + schema-drift checks
GET/api/admin/audit-dlq?limit=failed-audit dead-letter queue (D1)
GET/api/admin/backups?device_id=&limit=device backups β€” metadata only (size, versions, timestamps); never the encrypted payload

Admin operations (operator workflows)

The panel pages map to these flows. Sign in via Cloudflare Access at m7k2.cbkr.xyz (first login forces a password change; the panel calls /api/auth/login for the JWT).

Provision a new device

Restore a device after a reinstall / build switch

Why it's needed: a box's device_id is generated and stored by the app, so a reinstall β€” or switching between a debug and a signed build β€” makes it reappear as a brand-new device with no config. Restore copies the old device's playlists onto the new one so you don't re-enter anything.

Capture lines from a panel with no API (Inbox)

On the Inbox page, generate an ingest token (shown once) and copy the bookmarklet into your browser. On a reseller panel's "line created" page, select the m3u URL / Xtream creds and click the bookmarklet β€” the line lands in the Inbox (creds encrypted, m3u/Xtream auto-detected, deduped). Or generate a Webhook URL and paste it into a panel that can POST callbacks. Then Assign each inbox item to a device (creds re-encrypted for it), or Discard it.

Curate content (the filter)

In the assign form, choose Default β€” English regions and Preview categories: the panel fetches the provider's live categories and classifies each keep/hide (US/CA/Caribbean/UK/AU/NZ + 24/7-English + Canada-French kept). Flip any row; tick Lock to make hides PIN-protected. The result is pushed as the device's CurationProfile β€” no app change.

Message the fleet

Messages β†’ compose a banner/poster/notification/ad (Sidebar), broadcast or target one device, optionally require acknowledgement, set an expiry, and upload art to R2. The live TV preview shows how it renders. Sent messages show ack counts; deactivate to stop delivery (ack history kept).

Manage portals & health

Dashboard β†’ CRUD the DNS entries (portals), set active/priority. Check Health probes each portal from the edge (best-effort badge; a Cron re-probes active portals every 30 min). Portals double as the assign-form presets.

Roll out a change safely (canary)

For a risky config change (a new provider URL, a render-flag flip) across a segment: tag a small pilot cohort, create a rollout targeting the segment tag with that pilot tag, apply-pilot, then watch the rollout's adoption metrics (synced / drifted / awaiting) as the pilot boxes re-poll. If it's healthy, promote to the rest; if not, roll back β€” every applied device is restored from its pre-change snapshot. Lowers the blast radius of a bad edit from the whole segment to the pilot. Drive it from the Rollouts page.

Self-healing failover

A box reports per-portal outcome after it tries each source. When it reports its primary source failed and has a healthy lower-priority source, a */30 Cron (or POST /api/admin/failover/run) automatically promotes the healthy source β€” recorded as an auto-failover rollout you can see and roll back β€” and auto-reverts when the original recovers. The trigger is the box's own report, not the edge health probe (which has datacenter-IP false-negatives and stays an operator badge signal only). An anti-flap cooldown stops a flapping portal from bouncing a device.

Monitor & triage

Roles

admin and operator both reach the provisioning/messages admin API; the distinction is reserved for future least-privilege splits. All admin mutations write a provision_audit row (actor + IP/geo).

Data model (D1 tables)

TableHolds
usersoperator accounts β€” PBKDF2 hash, role, token_version, must_change_password.
dns_entriesportals/servers β€” title, dns_url (host), port, protocol, active, priority, description.
devicesprovisioned boxes β€” pair_code, status, AES-GCM creds, source_type/url/portal, profile_json (curation), display_name + device_name/number/owner/notes, tags_json, provider_account_id, geo_flagged/geo_flagged_at (country-change review flag), last_seen + edge telemetry (ip/country/ISP), config_hash/reported_config_hash (drift), expires_at.
device_playlistsmulti-playlist per device β€” source/creds/profile/helix-flags, priority, is_active, load/activation counts, device-reported status (active/error β†’ drives auto-failover).
rollouts · rollout_targetscanary / staged-rollout engine (#160) — a staged change + its cohorts and lifecycle state (draft→pilot→promoted→rolled_back), and one row per applied device carrying the pre-change snapshot (prev_config_json) for rollback. Auto-failover (#161) records its actions here as auto-failover rollouts (reverted_at tracks the anti-flap cooldown).
reseller_panelsregistered reseller panels for auto-provision (XUI.one) β€” label, admin API base + access code, AES-GCM-encrypted API key, client stream base. Never returns the key (only has_key).
inbound_sources · ingest_tokensno-API ingest — captured reseller lines staged in the Inbox (type/portal/source, AES-GCM-encrypted xtream creds, via, dedup_hash, status pending→assigned/discarded); and the scoped capture tokens (SHA-256-hashed, scope ingest|webhook). A partial unique index dedups only among pending captures.
messages Β· message_ackspush messages (type/title/body/image/priority/requires_ack/target/expiry/active) + per-device acks.
provider_accountswholesale lines a device draws from β€” label + metadata; LEFT-JOIN device counts surface oversell.
ledger_entriesper-device cost ledger entries (renewal/subscription bookkeeping).
rl_jaileddurable rate-limit jail rows (Bundle E) β€” written only on a limit breach; cron-swept on expiry.
connect_codes3DNX Connect activation codes (Bundle F3, was KV) β€” single-use, in-band expiry, atomic claim; cron-swept.
abuse_jailabuse-jailed IPs (Bundle F4, was KV) β€” write-once-on-jail, in-band expiry, fail-open; cron-swept.
audit_dlqfailed-audit dead-letter queue (Bundle F5, was KV) β€” durable retry sink for lost provision_audit rows; cron-swept.
provision_auditnon-secret event log β€” actor, device, event, masked detail, IP/country/ISP/colo.
api_logspublic-surface access log β€” device id, IP, endpoint, method, response code, UA.
sessionsadmin session rows (legacy/auxiliary to the signed JWT).
schema_migrationsapplied-migration ledger (feeds /api/admin/migration-status).
device_backupsdevice-scoped encrypted backup store β€” opaque base64 payload + metadata, one latest per device (never decrypted server-side).

CurationProfile

Pushed in a device's provision/playlist config (stored as profile_json); the box applies it client-side. The panel's "Default β€” English regions" preset (PR #120) computes hide_categories from the provider's real category list so US/CA/Caribbean/UK/AU/NZ + 24/7-English + Canada-French survive.

{ "hide_categories": ["France","Arabic", ...], // computed hide-list (the default preset) "forced_hide": [...], // hidden AND not user-overridable (PIN-locked) "allow_countries": [...], // keep-only by country (NOT used by the default β€” it drops 24/7 groups) "deny_langs": [...], "order": [...], "rename": { } }