Integrations β
Phlix supports several integration points with external services. Each integration is configured and managed independently.
Webhooks β
The Webhooks page (/admin/webhooks) in the admin console provides full CRUD management for webhook subscriptions, plus a per-webhook test trigger.
What it does β
Webhooks send signed HTTP POST requests to configured endpoints when events occur on the server. Each request carries a JSON payload and is verified with X-Phlix-Signature: sha256=<hmac> so receivers can authenticate the source.
Managing webhooks in the UI β
| Action | How |
|---|---|
| List | DataTable showing name, URL, event count badge, and row actions (Edit / Test / Delete) |
| Add | "Add Webhook" button opens a modal with name, URL, secret (with Show/Hide toggle), and event multi-select grouped by category |
| Edit | Row action opens the same modal pre-filled. Leave secret blank to keep the current value |
| Delete | Row action shows a confirm modal, then calls DELETE /api/v1/admin/webhooks/{id} |
| Test | Row action fires POST /api/v1/admin/webhooks/{id}/test and shows the delivery result (green checkmark or red X with message) |
The event multi-select lists the 7 subscribable events from the catalog, grouped into 5 categories:
| Category | Events |
|---|---|
| Playback | playback.started, playback.ended |
| Library | library.updated |
| Downloads | download.complete |
| Recordings | recording.started, recording.stopped |
| Alerts | alert |
webhook.test is not shown β it is used internally by the test button only.
API contract β
| Method | Path | Description |
|---|---|---|
GET | /api/v1/admin/webhooks | List all webhooks |
POST | /api/v1/admin/webhooks | Register a new webhook |
PUT | /api/v1/admin/webhooks/{id} | Update name, URL, or events (secret never returned; omit secret field to keep existing) |
DELETE | /api/v1/admin/webhooks/{id} | Remove a webhook |
POST | /api/v1/admin/webhooks/{id}/test | Fire a test dispatch |
All five are admin-only: 401 {code: auth.required} unauthenticated, 403 {code: auth.not_admin} for a non-admin. The check is taken by AdminMiddleware inside each handler β the routes are registered bare in Application::loadWebhookAdminRoutes(), with no route-level middleware group β and since S323 the middleware is a required, non-nullable constructor dependency of WebhookAdminController rather than an optional setter with a null guard. See Admin Gate Invariant.
The backend is documented in webhooks.md.
Note on other integrations β
Arr sync and authentication providers are covered above (step 1.4b). Trakt.tv OAuth connect/disconnect and Last.fm scrobbling connect/disconnect are documented in services.md (step 1.4c).
Arr sync β
The Arr sync section (/admin/integrations#arr-sync) connects to TRaSH-Guides-compatible indexers (Sonarr/Radarr/Bazarr/Prowlarr) to keep media metadata β season packs, quality profiles, and release profiles β in sync across yourarr stack.
What it does β
- Displays the last sync timestamp (or Never synced if no sync has run yet) and the current enabled/disabled state.
- A Sync now button manually triggers a POST to pull the latest release profiles from the configured TRaSH-Guides instance.
- An Enable / Disable toggle controls whether auto-sync runs on schedule.
Managing arr sync in the UI β
| Action | How |
|---|---|
| Check status | The card shows last_sync_at (ISO timestamp) or "Never synced" and an enabled/disabled badge |
| Sync now | Sync now button fires POST /api/v1/admin/sync/trash-guides; button label changes to "Syncingβ¦" with a spinner for the duration; toast on success or error updates the last-sync time |
| Toggle auto-sync | Enable/disable toggle fires PUT /api/v1/admin/sync/enable { enabled: bool }; success toast confirms |
API contract β
| Method | Path | Description |
|---|---|---|
GET | /api/v1/admin/sync/status | Returns { enabled, last_sync_at, last_sync_timestamp } |
POST | /api/v1/admin/sync/trash-guides | Triggers a manual TRaSH-Guides sync |
PUT | /api/v1/admin/sync/enable | Body { enabled: bool } β enables or disables auto-sync |
All three are admin-only: 401 {code: auth.required} unauthenticated, 403 {code: auth.not_admin} for a non-admin. As with the webhook routes above the check is taken by AdminMiddleware inside each handler (Arr\SyncController; the routes are registered bare in Application::loadArrSyncRoutes()), and since S323 the middleware is a required, non-nullable constructor dependency rather than an optional setter with a null guard. See Admin Gate Invariant.
Auth providers β
The Auth providers section (/admin/integrations#auth-providers) lets admins configure external authentication backends β OIDC (OpenID Connect) and LDAP β so users can log in with their corporate or identity-provider credentials instead of local Phlix accounts. A third provider, GitHub (plain OAuth 2.0), is available over the admin API but has no card in this UI yet.
Full SSO guide
This page covers the admin configuration UI. For the end-to-end login flow, the callback URL to register with your IdP / GitHub OAuth App (and the PHLIX_DOMAIN requirement behind it), the ldap:-prefix login convention, cookie-based sessions, GitHub setup over the API, and the security posture, see Single Sign-On (OIDC, LDAP & GitHub).
What it does β
- Lists all registered auth providers (OIDC, LDAP) with an enable/disable toggle per provider.
- Expanding a provider reveals its configuration form, pre-filled from the current server settings.
- LDAP additionally exposes a Test connection button that fires a dry-run
POSTwith the current form values and reports success or failure.
The enable/disable toggle persists a server setting (auth.oidc.enabled / auth.ldap.enabled / auth.github.enabled) and registers or unregisters the provider so the login flow goes live immediately β no server restart needed. Other workers pick a change up on their next sign-in request, by comparing a fingerprint of the persisted settings, so a settings save is also effective without a restart. A provider only becomes live when it is both enabled and configured: enabling one that has no saved configuration is rejected with 409 not_configured, so the "Enabled" badge always means "provider live", not merely "setting saved".
Provider configuration is stored in the database β one row per provider in the plugin_settings table (migration 093_plugin_settings.sql) β not in a per-plugin settings.json. An existing file is imported automatically, once, the first time the provider is read; there is no manual migration step.
A save preserves the fields it does not send (OIDC and GitHub)
The save endpoints replace the whole stored settings document. The OIDC and GitHub endpoints therefore deliberately re-emit any optional key that is absent from the request body, clearing only what is sent as empty. That is why saving from the OIDC form here β which has no redirect_uri field β does not erase a redirect_uri set through the API. Send "" to clear a field on purpose.
The LDAP endpoint is the exception: apart from bind_pw it rebuilds the document from the body alone, so a scripted partial save resets omitted keys to their defaults. This UI's LDAP form always sends the full set, so it is unaffected β but send the complete map if you call that endpoint yourself.
Managing auth providers in the UI β
| Action | How |
|---|---|
| List providers | Provider cards for OIDC and LDAP show name, whether they are currently enabled, and a Configure expand control |
| Enable/disable | Toggle switch per provider fires POST /api/v1/admin/auth-providers/{name}/enable|disable |
| Configure OIDC | Clicking Configure expands a form with provider_url, client_id, client_secret (optional, write-only), and scopes; Save fires POST /api/v1/admin/auth-providers/oidc/config |
| Configure LDAP | Clicking Configure expands a form with host, port, ssl (switch), base_dn, bind_dn, bind_pw (optional, write-only), user_filter, admin_group; Save fires POST /api/v1/admin/auth-providers/ldap/config; Test connection fires POST /api/v1/admin/auth-providers/ldap/test with the current form values and shows a result toast |
| Pre-fill | All forms call the GET settings endpoint before rendering so the current values are shown (secrets never returned β shown as empty with "(unchanged)" placeholder) |
API contract β
| Method | Path | Description |
|---|---|---|
GET | /api/v1/admin/auth-providers | Lists all providers { providers: [{ name, supports_authentication }] } |
POST | /api/v1/admin/auth-providers/{name}/enable | Enables a provider β 200 with {enabled, live}, 409 not_configured if it has no saved config, 404 unknown_provider for an unknown name |
POST | /api/v1/admin/auth-providers/{name}/disable | Disables a provider |
GET | /api/v1/admin/auth-providers/{name}/config-schema | Returns the provider's JSON schema for form rendering |
GET | /api/v1/admin/auth-providers/oidc/config | Returns { provider_url, client_id, scopes, redirect_uri, configured } (never the secret) |
POST | /api/v1/admin/auth-providers/oidc/config | Body { provider_url, client_id, client_secret?, scopes?, redirect_uri? } β omitted optional keys keep their stored value; redirect_uri must be an absolute http(s) URL or 400 invalid_redirect_uri |
GET | /api/v1/admin/auth-providers/oidc/schema | Returns the OIDC config schema |
GET | /api/v1/admin/auth-providers/github/config | Returns { client_id, scopes, redirect_uri, configured } (never the secret; configured = id and secret present) |
POST | /api/v1/admin/auth-providers/github/config | Body { client_id, client_secret?, scopes?, redirect_uri? } β same absent-key and absolute-URL rules as OIDC |
GET | /api/v1/admin/auth-providers/github/schema | Returns the GitHub config schema |
GET | /api/v1/admin/auth-providers/ldap/config | Returns { host, port, ssl, base_dn, bind_dn, user_filter, admin_group, configured } |
POST | /api/v1/admin/auth-providers/ldap/config | Body { host, port, ssl, base_dn, bind_dn, bind_pw?, user_filter, admin_group } |
POST | /api/v1/admin/auth-providers/ldap/test | Body same as LDAP save; returns { success, message } |
GET | /api/v1/admin/auth-providers/ldap/schema | Returns the LDAP config schema |