PHPackages                             detain/phlex-hub - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. detain/phlex-hub

ActiveProject[HTTP &amp; Networking](/categories/http)

detain/phlex-hub
================

Central cloud directory + reverse-tunnel relay for Phlix media servers. Sign in once, reach any of your servers from anywhere. Self-hostable.

v1.0.0(3w ago)10MITPHPPHP ^8.3CI passing

Since May 20Pushed yesterdayCompare

[ Source](https://github.com/detain/phlix-hub)[ Packagist](https://packagist.org/packages/detain/phlex-hub)[ RSS](/packages/detain-phlex-hub/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (14)Versions (17)Used By (0)

Phlix Hub
=========

[](#phlix-hub)

**Central cloud directory + reverse-tunnel relay for [Phlix](https://github.com/detain/phlix) media servers.**Sign in once, reach any of your servers from anywhere — no port forwarding, no static IP, no VPN. Fully self-hostable.

[![CI](https://github.com/detain/phlix-hub/actions/workflows/ci.yml/badge.svg)](https://github.com/detain/phlix-hub/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/cb97e3ed6da0d152fe152c223e9b8219cc2b1dd5049550cac7d5a95056a7d7ed/68747470733a2f2f636f6465636f762e696f2f67682f64657461696e2f70686c69782d6875622f67726170682f62616467652e737667)](https://codecov.io/gh/detain/phlix-hub)[![PHP](https://camo.githubusercontent.com/ce1068cd9f78ad2a081449d7e46efb2a2da1ff161c1c5aad703f27366a985ecc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e332532422d3737376262343f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://www.php.net/)[![PHPStan](https://camo.githubusercontent.com/b72adb1f27170ecf486459c4b07e920bb3db2b464444bce8277e018270665646/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d627269676874677265656e)](https://phpstan.org/)[![Psalm](https://camo.githubusercontent.com/bda1417d07f86745831148171d1a209a7101d6b6de5283b398fcddb73cb21cd4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5073616c6d2d6c6576656c253230312d627269676874677265656e)](https://psalm.dev/)[![Code style](https://camo.githubusercontent.com/95a767fbd3a1cb06f66fbf4a1298de6d009ce02706a5aed73960e9fc8432f258/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532307374796c652d5053522d2d31322d626c756576696f6c6574)](https://www.php-fig.org/psr/psr-12/)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)

---

Table of contents
-----------------

[](#table-of-contents)

- [What is the Hub?](#what-is-the-hub)
- [Features](#features)
- [Architecture](#architecture)
- [Requirements](#requirements)
- [One-line install](#one-line-install)
- [Updating an existing install](#updating-an-existing-install)
- [Uninstalling](#uninstalling)
- [Running alongside phlix-server](#running-alongside-phlix-server)
- [Quick start (development)](#quick-start-development)
- [Production install on Ubuntu](#production-install-on-ubuntu)
    - [1. System packages](#1-system-packages)
    - [2. MySQL: database, user, and grants](#2-mysql-database-user-and-grants)
    - [3. Application code](#3-application-code)
    - [4. Environment configuration](#4-environment-configuration)
    - [5. Run migrations](#5-run-migrations)
    - [6. Run as a systemd service](#6-run-as-a-systemd-service)
    - [7. Reverse proxy &amp; TLS](#7-reverse-proxy--tls)
- [Docker](#docker)
- [Configuration reference](#configuration-reference)
- [Database schema](#database-schema)
- [HTTP API](#http-api)
- [Connecting a media server](#connecting-a-media-server)
- [Testing &amp; quality](#testing--quality)
- [Project structure](#project-structure)
- [Related repositories](#related-repositories)
- [License](#license)

---

What is the Hub?
----------------

[](#what-is-the-hub)

A Phlix media server normally lives on your home network behind NAT. The **Hub** is the small, self-hostable cloud service that makes those servers reachable from anywhere:

- Each server **claims** itself to the Hub once (a short pairing code), then holds an outbound **WebSocket reverse tunnel** open to the Hub.
- Remote clients (apps, browsers) connect to the Hub, which **relays** their traffic down the tunnel to the right server — so the server never needs an inbound port open.
- The Hub is also a **directory**: it tracks which servers you own, their liveness (heartbeats), shared libraries, invite links, and media requests.

You can use the public Hub or run your own — the same codebase powers both.

Features
--------

[](#features)

- **Accounts &amp; auth** — signup/login, Argon2id password hashing, HMAC-SHA256 JWT access &amp; refresh tokens, and a published JWKS endpoint. The first account created is auto-promoted to admin.
- **Server claiming** — short-lived claim codes, enrollment JWTs, and a server registry with per-server public keys (JWK).
- **Reverse-tunnel relay** — servers hold an outbound WebSocket to the Hub; remote clients are multiplexed down that tunnel over a compact binary frame protocol. Idle tunnels are reaped and liveness is tracked with heartbeats.
- **Subdomain allocation** — each enrolled server can be assigned `&lt;subdomain&gt;.&lt;public-domain&gt;`for clean, per-server URLs.
- **Library sharing** — share a specific library on one of your servers with another Hub user, with read-only or read/write permission levels.
- **Invite links** — single-use, signed invite links that grant library access to a recipient.
- **Hub-to-hub federation** — federate with peer hubs to share libraries across hubs, with per-peer sessions and admin delegation.
- **Media requests** — a Jellyseerr-class request queue. Users request movies/series; admins approve, and the Hub talks to Sonarr/Radarr to fulfil them.
- **Web UI &amp; admin console** — the hub's front door is a Vue SPA (the shared `@phlix/ui` design system) served at `/app` (`/` redirects to `/app/servers`): My Servers, Federation, and Shares for every signed-in user, plus a `requiresAdmin` **admin console** at `/app/admin/*` (Hub Dashboard, Users, Logs, Settings, Audit Logs). The original Smarty pages still resolve as a legacy fallback. Everything is backed by a full JSON API under `/api/v1` (incl. `/api/v1/admin/*`).
- **Operations-ready** — structured JSON logging (Monolog) across dedicated channels (app, error, hub, relay, audit), a `/health` endpoint, and idempotent SQL migrations.

Architecture
------------

[](#architecture)

The Hub runs as a set of long-lived [Workerman](https://www.workerman.net/) workers in a single process group:

WorkerDefault portPurposeHTTP`8800`REST API + the Vue SPA (`/app`) + legacy SSR pages + `/health`Relay (server-facing)`8802`Servers connect here to open their outbound tunnelRelay (client-facing)`8803`Remote clients connect (`GET /client/{server_id}`) and are routed down a tunnelSupporting pieces:

- **PSR-11 container** (PHP-DI 7) wires services; routes are registered in [`src/Application.php`](src/Application.php).
- **MySQL** is accessed through an async connection pool (`workerman/mysql`). The pool is initialised lazily so `/health` stays up even if the database is briefly unreachable.
- **JWT** auth is symmetric (HS256); Ed25519 keys are used for signing enrollment/relay material, and the public key set is served at `/.well-known/jwks.json`.

Requirements
------------

[](#requirements)

- **PHP 8.3+** with the `pcntl`, `posix`, `json`, `mbstring`, `curl`, and `sodium` extensions
- **MySQL 8.0+** (or MariaDB 10.6+)
- **Composer 2**
- A POSIX host (Linux recommended; Workerman uses `pcntl`/`posix` for process management)

One-line install
----------------

[](#one-line-install)

On a fresh Ubuntu/Debian host, [`scripts/install.sh`](scripts/install.sh) does everything in [Production install](#production-install-on-ubuntu) for you — system packages, MySQL database + user, code, env file, JWT secret, migrations, a systemd service, and an HAProxy reverse proxy with an auto-renewing Let's Encrypt certificate:

> The installer also compiles the **Swoole + php-uv** extensions from source (the coroutine runtime Workerman uses), idempotently skipping the build when they already load, and runs a `disable_functions` preflight — see [Swoole &amp; php-uv on Linux](https://detain.github.io/phlix-docs/install/linux#swoole-php-uv-coroutine-runtime).

```
curl -fsSL https://raw.githubusercontent.com/detain/phlix-hub/master/scripts/install.sh | sudo bash
```

To set up HTTPS at the same time, pass your domain and an email for Let's Encrypt:

```
curl -fsSL https://raw.githubusercontent.com/detain/phlix-hub/master/scripts/install.sh \
  | sudo bash -s -- --domain hub.example.com --admin-email you@example.com
```

The script prompts for the install path, database user/password, and hostname when run in a terminal (with sensible defaults), and runs **fully unattended** when piped or given `-y`. See `sudo bash scripts/install.sh --help` for every flag. Prefer to do it by hand? Follow the [step-by-step guide](#production-install-on-ubuntu) below.

### Install flags

[](#install-flags)

`sudo bash scripts/install.sh --help` lists every option. The most useful:

FlagEffect`--domain HOST`Public hostname for the hub (enables TLS when paired with `--admin-email`)`--admin-email EMAIL`Email registered with Let's Encrypt`--db-name`, `--db-user`, `--db-pass`, `--db-host`, `--db-port`MySQL identity (random password if `--db-pass` omitted)`--jwt-secret SECRET`HMAC secret used to sign JWTs (random 32-byte hex if omitted)`--service-user USER`System user to run as (default `phlix-hub` — dedicated system account, created if missing)`--workers N`HTTP worker processes (default 4)`--branch NAME`Git branch or tag to install (default `master`)`--repo URL`Git repository URL (default `detain/phlix-hub`)`--tls` / `--no-tls`Force or skip Let's Encrypt + HAProxy TLS`--no-proxy`Skip the managed HAProxy entirely (use your own reverse proxy)`--update`Pull new code + run migrations on an existing install (preserves env + secrets)`--uninstall`Remove the install — interactive prompts before each destructive step`--purge`With `--uninstall`, also drop the DB, delete the Let's Encrypt cert, and remove the dedicated system user`-y`, `--non-interactive`Never prompt; use defaults/flags`--interactive`Force prompts even when piped> Default service user changed from `www-data` to `phlix-hub` so the hub runs under its own dedicated system account, isolated from the apache/nginx-owned `www-data`. Existing installs that were created on `www-data` keep running on `www-data` — `--update` reads `User=` from the systemd unit rather than rewriting it.

Updating an existing install
----------------------------

[](#updating-an-existing-install)

The same `scripts/install.sh` updates an in-place install **without rotating any secrets**. It reads the existing `/etc/phlix-hub.env` (so the JWT secret and DB password are preserved), pulls the latest code, refreshes Composer dependencies, runs pending migrations, and restarts the service:

```
sudo bash /opt/phlix-hub/scripts/install.sh --update -y
```

Or via the one-liner:

```
curl -fsSL https://raw.githubusercontent.com/detain/phlix-hub/master/scripts/install.sh \
  | sudo bash -s -- --update -y
```

Pin to a specific tag or branch with `--branch`:

```
sudo bash /opt/phlix-hub/scripts/install.sh --update --branch v0.2.0 -y
```

What `--update` does, in order:

1. Discovers the install path from the systemd unit's `WorkingDirectory` (so non-default `--install-path` setups are detected automatically).
2. Reads `/etc/phlix-hub.env` and reuses every value — `HUB_JWT_SECRET`, `HUB_DB_PASSWORD`, `HUB_PUBLIC_DOMAIN`, etc. are never regenerated.
3. `git fetch --depth 1 origin $BRANCH` then `git reset --hard origin/$BRANCH` in the install directory. Uncommitted local edits are **discarded** — the script warns first.
4. `composer install --no-dev --optimize-autoloader` (follows `composer.lock`).
5. Clears `var/smarty/{compile,cache}` to avoid stale compiled templates.
6. Runs `scripts/run-migrations.php` — idempotent, only pending migrations apply.
7. `systemctl daemon-reload` then `systemctl restart phlix-hub`.
8. `curl http://localhost:$HUB_PORT/health` as a final sanity check.

What it explicitly does **not** touch: the env file, MySQL grants, HAProxy config, or the Let's Encrypt certificate. If a release adds new `HUB_*` env vars, append them to `/etc/phlix-hub.env` yourself — anything the code expects but doesn't find falls back to its documented default.

Uninstalling
------------

[](#uninstalling)

`scripts/install.sh --uninstall` removes an existing install. By default it is **interactive**and prompts separately before each destructive step. The MySQL database and the Let's Encrypt certificate are **kept** unless you opt in explicitly:

```
sudo bash /opt/phlix-hub/scripts/install.sh --uninstall
```

Add `--purge` to also drop the database (and user) and delete the Let's Encrypt certificate via `certbot delete`. Combine with `-y` for a fully unattended teardown:

```
sudo bash /opt/phlix-hub/scripts/install.sh --uninstall --purge -y
```

Piped, non-interactive runs require an explicit `-y` to proceed.

What it removes, only if it finds them:

1. The `phlix-hub` systemd service — `stop`, `disable`, remove the unit, `daemon-reload`.
2. HAProxy fragment at `/etc/haproxy/phlix-managed/phlix-hub.cfg.fragment`, and `/etc/haproxy/haproxy.cfg` is rebuilt. If phlix-server is still installed, its frontend and backend stay. If phlix-hub was the last Phlix project, the pre-Phlix snapshot at `/etc/haproxy/haproxy.cfg.pre-phlix.bak` is restored (or `haproxy.cfg` is removed and haproxy is stopped + disabled if no snapshot exists).
3. The combined PEM at `/etc/haproxy/certs/.pem`.
4. `/etc/cron.d/phlix-hub-certbot` and `/etc/letsencrypt/renewal-hooks/deploy/phlix-haproxy.sh`.
5. The Let's Encrypt cert via `certbot delete --cert-name ` — only with `--purge` or interactive confirmation.
6. The MySQL database and dedicated user — only with `--purge` or interactive confirmation.
7. The install directory (`rm -rf`, with a denylist of system paths like `/`, `/etc`, `/opt`).
8. `/etc/phlix-hub.env` (env file).
9. The dedicated system user (`phlix-hub` or whatever `User=` the systemd unit was using) via `userdel` — only with `--purge` or interactive confirmation. Refuses to touch shared OS accounts (`www-data`, `root`, `daemon`, etc.). Cross-detects phlix-server's systemd unit and refuses to remove a user that's still being used by the sibling service.

System packages (`php-*`, `mysql-server`, `haproxy`, `certbot`) and `ufw` rules are left in place — uninstall them yourself with `apt remove` / `ufw delete` if you no longer need them.

Running alongside phlix-server
------------------------------

[](#running-alongside-phlix-server)

Both installers can share a single HAProxy instance — they auto-merge into one `/etc/haproxy/haproxy.cfg`. Just run both installers normally; whichever runs second detects the first's fragment and rebuilds a combined config that routes by `Host:` header.

```
# 1. Install phlix-hub first (with TLS).
curl -fsSL https://raw.githubusercontent.com/detain/phlix-hub/master/scripts/install.sh \
  | sudo bash -s -- --domain hub.example.com --admin-email you@example.com -y

# 2. Install phlix-server, also with TLS, on a different hostname.
curl -fsSL https://raw.githubusercontent.com/detain/phlix-server/master/scripts/install.sh \
  | sudo bash -s -- --domain phlix.example.com --admin-email you@example.com -y
```

After both finish, `/etc/haproxy/haproxy.cfg` is a Phlix-managed config carrying both projects' frontends and backends, with HAProxy picking the right cert per SNI hostname from `/etc/haproxy/certs/`.

**How the merge works.** Each install drops a fragment at `/etc/haproxy/phlix-managed/.cfg.fragment` with `fe_http`, `fe_https`, and `backends`sections. A shared rebuilder then assembles the final `haproxy.cfg` from every fragment it finds. HAProxy's `crt /etc/haproxy/certs/` directive auto-loads every `.pem` in that directory and picks the right one per SNI hostname.

The first install snapshots any pre-Phlix `haproxy.cfg` to `/etc/haproxy/haproxy.cfg.pre-phlix.bak`.

**Uninstall behaviour**: `--uninstall` removes only that project's fragment and rebuilds. If other Phlix projects remain, their frontend stays untouched. When the **last** Phlix project is uninstalled, the rebuilder restores the pre-Phlix snapshot (or removes `haproxy.cfg`outright if there was no pre-Phlix config) and stops/disables `haproxy`.

The **hub server-tunnel port** (`:8802`) is a separate listener — servers connect to that port directly. Open it on the firewall but don't put it behind the HAProxy 80/443 frontend.

If you'd rather use your own reverse proxy (nginx, Caddy, Traefik, etc.) instead of the managed HAProxy, pass `--no-proxy` to either install script. Each service then listens on its own port (8800 / 8802 / 8803 for phlix-hub, 8096 for phlix-server) and you point your proxy at those.

Everything else is already namespaced (env files, systemd units, install dirs, service users, MySQL DBs, backend ports, certbot artefacts) so there are no other co-install conflicts.

Quick start (development)
-------------------------

[](#quick-start-development)

```
git clone https://github.com/detain/phlix-hub.git
cd phlix-hub
composer install

# Point the Hub at a MySQL instance (see Configuration reference below).
export HUB_DB_HOST=127.0.0.1 HUB_DB_USER=phlix_hub HUB_DB_PASSWORD=phlix_hub HUB_DB_NAME=phlix_hub
export HUB_JWT_SECRET="$(openssl rand -hex 32)"

php scripts/run-migrations.php     # create the schema (idempotent)
php public/index.php start         # start the Hub (Ctrl-C to stop)

curl http://localhost:8800/health  # => {"status":"ok",...}
```

Then open  to create the first account (auto-promoted to admin). After signing in, `/my-servers` lists your servers and `/claim-server` walks through pairing a new one.

> Run `php public/index.php start -d` to daemonize; `stop`, `restart`, `reload`, and `status`are also available.

### CLI (`bin/phlix`)

[](#cli-binphlix)

A small [`webman/console`](https://www.workerman.net/doc/webman/components/command.html) CLI ships at `bin/phlix`:

```
php bin/phlix list         # list available commands (works with no database)
php bin/phlix migrate      # apply migrations/*.sql (idempotent; tracking table)
php bin/phlix smoke:jwt    # smoke-test the JWT create/validate round-trip
```

`migrate` is the CLI equivalent of `php scripts/run-migrations.php`.

Production install on Ubuntu
----------------------------

[](#production-install-on-ubuntu)

These steps target **Ubuntu 22.04 / 24.04**. Run as a sudo-capable user.

### 1. System packages

[](#1-system-packages)

```
# PHP: use the version-agnostic php-* package names so apt installs the
# distro's current PHP. Ubuntu 24.04 ships PHP 8.3 by default, which meets
# the Hub's requirement.
sudo apt update
sudo apt install -y \
  php-cli php-mysql php-mbstring php-curl \
  php-xml php-bcmath php-gd php-zip \
  git unzip mysql-server

php -v   # confirm PHP 8.3 or newer

# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
```

> `pcntl`, `posix`, and `sodium` ship with the `php-cli` package on Ubuntu — verify with `php -m | grep -E 'pcntl|posix|sodium'`. If your distro's default PHP is older than 8.3, upgrade to Ubuntu 24.04 (or newer) rather than pulling in a third-party PHP build.

### 2. MySQL: database, user, and grants

[](#2-mysql-database-user-and-grants)

Secure the server first (sets the root password, removes anonymous users, etc.):

```
sudo mysql_secure_installation
```

Then create the database and a dedicated, least-privilege user. Open a root shell with `sudo mysql` and run:

```
-- Database (utf8mb4 throughout)
CREATE DATABASE phlix_hub
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

-- Dedicated user. The Hub connects over TCP to 127.0.0.1 by default, so the
-- user host must match. Use a strong, unique password.
CREATE USER 'phlix_hub'@'127.0.0.1' IDENTIFIED BY 'CHANGE-ME-strong-password';

-- Least privilege: only the rights the app needs on its own schema.
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, REFERENCES
  ON phlix_hub.* TO 'phlix_hub'@'127.0.0.1';

FLUSH PRIVILEGES;
```

Notes:

- The migration runner issues `CREATE TABLE` / `ALTER TABLE`, so `CREATE`, `ALTER`, and `INDEX`are required in addition to the CRUD grants.
- If the Hub runs on a **different host** than MySQL, create the user for that host (or `'%'`) and set `HUB_DB_HOST` accordingly. Make sure MySQL's `bind-address` allows remote connections.
- MySQL distinguishes `'localhost'` (unix socket) from `'127.0.0.1'` (TCP). The Hub uses TCP, so grant to `'127.0.0.1'`. To also allow socket logins for manual `mysql` use, create a second `'phlix_hub'@'localhost'` user.

Verify the credentials work:

```
mysql -h 127.0.0.1 -u phlix_hub -p phlix_hub -e 'SELECT 1;'
```

### 3. Application code

[](#3-application-code)

```
sudo git clone https://github.com/detain/phlix-hub.git /opt/phlix-hub
cd /opt/phlix-hub
sudo composer install --no-dev --optimize-autoloader
sudo mkdir -p .logs
sudo chown -R www-data:www-data /opt/phlix-hub
```

### 4. Environment configuration

[](#4-environment-configuration)

The Hub is configured entirely through environment variables (see the [reference](#configuration-reference)). Create an env file the service will load:

```
sudo tee /etc/phlix-hub.env >/dev/null =32-byte secret. Generate once and keep stable.
HUB_JWT_SECRET=CHANGE-ME-run-openssl-rand-hex-32
EOF
sudo chmod 600 /etc/phlix-hub.env
```

Generate a secret with `openssl rand -hex 32`. If `HUB_JWT_SECRET` is unset the Hub falls back to a random per-process secret — fine for dev, but it invalidates every token on restart, so it must be set in production.

### 5. Run migrations

[](#5-run-migrations)

```
sudo -u www-data --preserve-env \
  env $(grep -v '^#' /etc/phlix-hub.env | xargs) \
  php /opt/phlix-hub/scripts/run-migrations.php
```

The runner records applied migrations in a `migrations` table and is **idempotent** — re-running it after a successful apply is a no-op.

### 6. Run as a systemd service

[](#6-run-as-a-systemd-service)

```
sudo tee /etc/systemd/system/phlix-hub.service >/dev/null  **TLS for server subdomains:** automated ACME (Let's Encrypt) provisioning is **not** built in. Provision certificates out-of-band (e.g. a wildcard cert via certbot DNS-01) and point the Hub at them.

Docker
------

[](#docker)

A `Dockerfile` is provided (PHP 8.3 + Swoole/UV, nginx, supervisor).

```
docker build -t phlix-hub .

docker run -d --name phlix-hub \
  -p 8800:8800 -p 8802:8802 -p 8803:8803 \
  -e HUB_DB_HOST=host.docker.internal \
  -e HUB_DB_USER=phlix_hub \
  -e HUB_DB_PASSWORD=CHANGE-ME \
  -e HUB_DB_NAME=phlix_hub \
  -e HUB_JWT_SECRET="$(openssl rand -hex 32)" \
  phlix-hub

# Apply migrations against the configured database
docker exec phlix-hub php /var/www/html/scripts/run-migrations.php
```

Point `HUB_DB_HOST` at a reachable MySQL instance (a linked container, `host.docker.internal`, or an external host).

Configuration reference
-----------------------

[](#configuration-reference)

All settings are environment variables, read in [`config/`](config). Defaults shown are the development fallbacks.

### Server ([`config/server.php`](config/server.php))

[](#server-configserverphp)

VariableDefaultDescription`HUB_HOST``0.0.0.0`HTTP bind address`HUB_PORT``8800`HTTP listen port`HUB_WORKERS``2`Number of HTTP worker processes`HUB_WORKERMAN_LOG``.logs/workerman.log`Workerman's own log file`HUB_PUBLIC_DOMAIN``phlix.media`Base domain for per-server subdomains### Database ([`config/database.php`](config/database.php))

[](#database-configdatabasephp)

VariableDefaultDescription`HUB_DB_HOST``127.0.0.1`MySQL host`HUB_DB_PORT``3306`MySQL port`HUB_DB_USER``phlix_hub`MySQL user`HUB_DB_PASSWORD``phlix_hub`MySQL password`HUB_DB_NAME``phlix_hub`MySQL database name### Auth ([`config/auth.php`](config/auth.php))

[](#auth-configauthphp)

VariableDefaultDescription`HUB_JWT_SECRET`*(random per-process)***Required in production.** ≥32-byte HMAC secret`HUB_JWT_ACCESS_TTL``3600`Access-token lifetime (seconds)`HUB_JWT_REFRESH_TTL``604800`Refresh-token lifetime (seconds)### Sonarr / Radarr (optional, for media requests — [`config/server.php`](config/server.php))

[](#sonarr--radarr-optional-for-media-requests--configserverphp)

VariableDefaultDescription`HUB_SONARR_URL``http://localhost:8989`Sonarr base URL`HUB_SONARR_API_KEY`*(empty)*Sonarr API key`HUB_SONARR_ENABLED``0`Enable Sonarr fulfilment`HUB_RADARR_URL``http://localhost:7878`Radarr base URL`HUB_RADARR_API_KEY`*(empty)*Radarr API key`HUB_RADARR_ENABLED``0`Enable Radarr fulfilmentDatabase schema
---------------

[](#database-schema)

Migrations live in [`migrations/`](migrations) and are applied in filename order. The schema:

TablePurpose`users`Hub accounts (Argon2id passwords; unique email + username)`servers`Claimed media servers and their operational state`server_claims`Pending/paired claim codes minted during pairing`server_heartbeats`Recent heartbeats for liveness and clock-skew detection`relay_sessions`One row per open WebSocket relay session`shared_libraries`Library grants from a server owner to another user`library_shares`Per-library shares with read-only / read-write levels`invite_links`Single-use signed invite links`webhooks`User-defined HTTP callbacks for `phlix.*` event aliases`media_requests`Jellyseerr-class request queue`dns_challenges`DNS-01 challenge records for subdomain TLS`hub_settings`Hub-wide configuration key/value settings`federation_hubs`Peer hubs for hub-to-hub federation`federation_library_shares`Libraries shared across federated hubs`audit_logs`Audit trail of administrative actionsHTTP API
--------

[](#http-api)

Selected endpoints (full surface in [`src/Application.php`](src/Application.php)). Protected routes require a `Bearer` access token (or session cookie for SSR pages).

### Health &amp; discovery

[](#health--discovery)

MethodPathNotes`GET``/health`Service + version JSON`GET``/.well-known/jwks.json`Public JWKS### Auth

[](#auth)

MethodPathNotes`POST``/api/v1/auth/register`Create account (canonical; `/api/v1/auth/signup` is an alias)`POST``/api/v1/auth/login`Obtain access + refresh tokens`POST``/api/v1/auth/refresh`Exchange a refresh token`POST``/api/v1/auth/logout`Invalidate session`GET``/api/v1/me`, `/api/v1/auth/me`Current user, incl. `is_admin` (protected)### Servers

[](#servers)

MethodPathNotes`POST``/api/v1/server-claims/new`Server mints a claim code`POST``/api/v1/server-claims/claim`User redeems a claim code (protected)`GET``/api/v1/me/servers`List your servers (protected)`DELETE``/api/v1/me/servers/{id}`Remove a server (protected)`GET``/api/v1/me/servers/{id}/access-info`Connection info (protected)`POST``/api/v1/servers/{id}/heartbeat`Server liveness (enrollment JWT)`GET``/api/v1/servers/{id}/info`Server metadata (enrollment JWT)`POST`/`DELETE``/servers/{id}/subdomain`Allocate / revoke subdomain### Sharing, invites &amp; requests

[](#sharing-invites--requests)

MethodPathNotes`POST`/`GET``/api/v1/me/shares`Create / list library shares`PATCH`/`DELETE``/api/v1/me/shares/{id}`Update / delete a share`POST`/`GET``/api/v1/me/invite-links`Create / list invite links`GET``/invite/{token}`Accept an invite (public page)`POST`/`GET``/api/v1/me/requests`Create / list media requests`GET``/api/v1/admin/requests`Admin request queue`POST``/api/v1/admin/requests/{id}/approve`Approve a request`POST``/api/v1/admin/requests/{id}/deny`Deny a request### Admin (web console)

[](#admin-web-console)

The Vue admin console at `/app/admin/*` is backed by these admin-gated endpoints (`[AuthMiddleware, AdminMiddleware]` — 401 unauthenticated / 403 non-admin):

MethodPathNotes`GET``/api/v1/admin/dashboard/summary`Server fleet (total/online/offline), active relay sessions, pending requests, user count`GET``/api/v1/admin/dashboard/activity`Recent audit events (`?limit=`)`GET`/`POST``/api/v1/admin/users`List / create accounts`GET`/`PUT`/`DELETE``/api/v1/admin/users/{id}`Fetch / update / delete an account`POST``/api/v1/admin/users/{id}/set-admin`Grant / revoke admin`POST``/api/v1/admin/users/{id}/reset-password`Set a new password`GET``/api/v1/admin/logs`, `/logs/tail`, `/logs/tail-all`Browse / tail the hub log files`GET`/`PUT``/api/v1/admin/settings`Read / persist hub settingsThe same logic is also reachable under `/api/v1/me/*` for back-compat (`/me/audit-logs`, `/me/logs*`, `/me/hub-settings`, `/me/federation/*`).

### Relay (WebSocket)

[](#relay-websocket)

EndpointPortNotesServer tunnel`8802`Server opens its outbound tunnel here`GET /client/{server_id}``8803`Client connects and is routed to its serverConnecting a media server
-------------------------

[](#connecting-a-media-server)

1. On the Hub, sign in and open **My Servers** (`/app/servers`) to start a claim — the legacy `/claim-server` page still works too — or the server requests a code via `POST /api/v1/server-claims/new`.
2. Enter the claim code on the server; the server is issued an **enrollment JWT** and registers its public key.
3. The server opens its **outbound relay tunnel** to the Hub and begins sending heartbeats.
4. The server now appears under **My Servers** (`/app/servers`), and remote clients can reach it through the Hub — no inbound ports required.

Testing &amp; quality
---------------------

[](#testing--quality)

```
composer test     # PHPUnit (Unit + Integration suites)
composer cs       # PHP_CodeSniffer (PSR-12)
composer stan     # PHPStan (level 9)
composer psalm    # Psalm (errorLevel 1)
```

CI runs on every push and pull request via [GitHub Actions](.github/workflows/ci.yml):

- Composer validation
- PHP\_CodeSniffer (PSR-12)
- PHPStan (level 9)
- Psalm (errorLevel 1)
- PHPUnit (with coverage uploaded to Codecov)
- Composer security audit

Project structure
-----------------

[](#project-structure)

```
phlix-hub/
├── config/          # Environment-driven config (server, database, auth, logger)
├── migrations/      # Idempotent SQL migrations
├── public/
│   └── index.php    # Workerman HTTP entry point
├── scripts/
│   └── run-migrations.php
├── src/
│   ├── Application.php   # Worker bootstrap + route registration
│   ├── Auth/            # JWT, users, auth manager
│   ├── Common/          # Container, database pool, logging, web portal
│   ├── Http/            # Router, request/response, controllers, middleware
│   ├── Hub/             # Claims, heartbeats, sharing, DNS, TLS, relay sessions
│   ├── Federation/      # Hub-to-hub federation peers, shares, sessions
│   ├── Relay/           # Reverse-tunnel relay workers, frame codec, tunnels
│   └── Requests/        # Media request manager
├── tests/           # PHPUnit Unit + Integration suites
├── web-ui/          # Vite + TypeScript SPA (@phlix/hub-web-ui), built to public/assets/app/
└── Dockerfile

```

Related repositories
--------------------

[](#related-repositories)

- [`detain/phlix`](https://github.com/detain/phlix) (a.k.a. `phlix-server`) — the local media server.
- [`detain/phlix-shared`](https://github.com/detain/phlix-shared) — shared interfaces, DTOs, and protocol types.

License
-------

[](#license)

MIT — see [`LICENSE`](LICENSE).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance98

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 91% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

21d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b1036e0717211b8030b83cbe729e8ba6ba442fdbd5285fb97a39d7dcfe339342?d=identicon)[detain](/maintainers/detain)

---

Top Contributors

[![detain](https://avatars.githubusercontent.com/u/1364504?v=4)](https://github.com/detain "detain (171 commits)")[![jvisedop](https://avatars.githubusercontent.com/u/55050494?v=4)](https://github.com/jvisedop "jvisedop (17 commits)")

---

Tags

dashboardembyjellyfinjwtldapmedia-hubmedia-serveroidcphpphp8plexrelayremote-accessreverse-tunnelself-hostedssowebhookswebsocketworkerman

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/detain-phlex-hub/health.svg)

```
[![Health](https://phpackages.com/badges/detain-phlex-hub/health.svg)](https://phpackages.com/packages/detain-phlex-hub)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k86.9M2.2k](/packages/symfony-symfony)[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.6k38.2k](/packages/matomo-matomo)[laravel/framework

The Laravel Framework.

34.7k532.1M19.2k](/packages/laravel-framework)[tempest/framework

The PHP framework that gets out of your way.

2.2k31.1k11](/packages/tempest-framework)[api-platform/state

API Platform state interfaces

244.3M111](/packages/api-platform-state)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M506](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
