A calm, fast, self-hosted team chat app you run yourself — workspaces, channels, threads, reactions, search, reminders, and scheduled messages. Built with Laravel 13, Inertia + Vue 3, Laravel Reverb (WebSockets), and Meilisearch.
Website · Docs · Install · vs Slack · MIT License
The Desk ships as a single Docker Compose stack with a prebuilt image — one
docker compose up -d and it's live. Full operator docs (requirements,
install, reverse proxy & TLS, upgrades) live at
the-desk.emmanuelpaul.com/docs.
Local development uses Laravel Sail:
git clone git@github.com:emmpaul/the-desk.git
cd the-desk
cp .env.example .env
composer install
./vendor/bin/sail up -d
./vendor/bin/sail composer setupRun the quality gate before pushing:
./vendor/bin/sail composer test # Pint, PHPStan, Rector (dry-run), and tests at 100% coverage
./vendor/bin/sail npm run lint:check # ESLint / Prettier / vue-tsc / buildRector handles automated structural refactoring (the semantic counterpart to Pint's formatter). The gate runs it in dry-run mode; when it reports pending changes, apply them and re-run the gate:
./vendor/bin/sail composer refactor # apply Rector's suggested refactorstests/Browser holds Pest 4 browser tests that drive real Playwright browsers
against the app served in-process, with two clients exchanging messages over a
live Reverb server — the realtime send/receive, typing, edit, and delete paths
that headless feature tests can't reach. They live in a separate browser test
group excluded from the coverage gate, so composer test never runs them (and
they never affect the 100% coverage requirement). CI runs them in a dedicated
browser job.
Prerequisites (one-time, inside the Sail container):
./vendor/bin/sail npm ci # playwright npm package
./vendor/bin/sail npx playwright install chromium # the browser binaryThen, with Sail up (Reverb is part of sail up -d) and the frontend built:
./vendor/bin/sail npm run build # tests use the built assets
./vendor/bin/sail composer test:browser # or: sail bin pest tests/BrowserRebuild the frontend (npm run build) after changing any Vue component the
tests touch, since the in-process server serves the compiled Vite assets.
Two opt-in dev containers let you exercise the single sign-on flows against real
providers locally, without registering an app at an external IdP. They sit
behind the sso compose profile, so a plain sail up never starts them. Enable
them by setting COMPOSE_PROFILES=sso in your .env (or sail up --profile sso), then uncomment the matching dev values in .env.example:
-
OIDC — a mock provider (soluto/oidc-server-mock) seeded with
oidc1@the-desk.local…oidc4@the-desk.local(passwordpassword) and a pre-registeredthe-desk-devclient. It's reached asoidc:8081from both the browser and the app container so the derived issuer stays consistent, so add one line to your host's/etc/hostsonce:127.0.0.1 oidcThen set
SSO_OIDC_ISSUER=http://oidc:8081,SSO_OIDC_CLIENT_ID=the-desk-dev,SSO_OIDC_CLIENT_SECRET=the-desk-dev-secretand use "Sign in with SSO". -
LDAP — an OpenLDAP directory (osixia/openldap) seeded with
ldap1@the-desk.local…ldap4@the-desk.local(passwordpassword) underdc=the-desk,dc=local. Uncomment the dev block in.env.example:LDAP_HOST=ldap LDAP_PORT=389 LDAP_BASE_DN="dc=the-desk,dc=local" LDAP_USERNAME="cn=admin,dc=the-desk,dc=local" LDAP_PASSWORD=adminpassword LDAP_ATTR_GUID=entryuuid # OpenLDAP's stable id, not AD's objectGUIDThen sign in through the normal login form with a seeded email.
The production stack is orchestrated with docker-compose.prod.yml. The app is
served with FrankenPHP; Postgres, Meilisearch, Reverb,
a queue worker, and the scheduler all run as containers.
By default the stack pulls a prebuilt image from the GitHub Container Registry
(ghcr.io/emmpaul/the-desk), so up -d runs it with no build step. Every setting,
including the browser-facing Reverb values, is read at runtime, so one
published image works for any host. If you would rather build from source, a
one-line overlay restores a local build — see
Building from source.
- Docker Engine 24+ and the Docker Compose plugin.
- A domain and a TLS-terminating reverse proxy (nginx, Caddy, Traefik, …) in
front of the stack. TLS/HTTPS is your responsibility — the containers speak
plain HTTP. Your proxy must also forward WebSocket upgrade requests to the
reverbservice.
# 1. Clone and check out the latest release tag.
git clone git@github.com:emmpaul/the-desk.git
cd the-desk
git fetch --tags
git checkout v1.5.2 # x-release-please-version (the desired release tag)
# 2. Generate .env with all required secrets filled in.
# Creates .env from the template and fills APP_KEY, DB_PASSWORD,
# MEILISEARCH_KEY, and the REVERB_* keys with fresh random values.
# Safe to re-run — it never overwrites values you have already set.
./docker/gen-secrets.sh
# 3. Edit .env and set the non-secret settings the script can't guess:
# APP_URL, SMTP mail credentials, and — since your TLS proxy terminates
# wss/443 while the container speaks http/8080 — REVERB_PORT_PUBLIC=443 and
# REVERB_SCHEME_PUBLIC=https. These are read at runtime (a restart applies
# changes), so no rebuild is needed when they change.
# 4. Start the stack. This pulls the release-pinned image — no build step.
docker compose -f docker-compose.prod.yml up -dMigrations run automatically on start (the app container's entrypoint runs
php artisan migrate --force). The app and Reverb speak plain HTTP and publish to
loopback by default (APP_BIND=127.0.0.1) on APP_PORT (default 8000) and
REVERB_PORT (default 8080); point your reverse proxy at 127.0.0.1:8000 /
127.0.0.1:8080, or reach app:8080 / reverb:8080 directly from a proxy on the
compose network.
Required secrets.
APP_KEY,DB_PASSWORD, andMEILISEARCH_KEYhave no defaults — the stack refuses to start without them.gen-secrets.shgenerates all of these for you; prefer it over setting them by hand. If you would rather generateAPP_KEYyourself, anybase64:-encoded 32-byte value works, e.g.docker run --rm dunglas/frankenphp:1-php8.5-alpine php -r "echo 'base64:'.base64_encode(random_bytes(32)).PHP_EOL;".
Registration is open, so onboarding is self-service:
- Visit your
APP_URLand go to /register to create the first account. - Create your first workspace from Settings → Teams, then invite teammates.
Locking down registration. Public sign-ups are open by default. To run a private/invite-only instance, set
REGISTRATION_ENABLED=falsein.env(create your own account first). With it off,/registerreturns 404 and the "sign up" links are hidden — existing users and email invitations still work.
Requiring email verification. New accounts are not asked to confirm their email by default. To run a verified-only instance, set
EMAIL_VERIFICATION_ENABLED=truein.envand make sure your SMTP settings work so the verification email is delivered. With it on, a freshly registered user is emailed a confirmation link and is blocked from the workspace until they verify; any existing account with an unverified email is likewise gated on its next request.
Upgrades follow the same tag-based flow. Check out the newer release tag and
restart — up -d pulls the image that tag pins:
git fetch --tags
git checkout v1.5.2 # x-release-please-version (the desired release tag)
docker compose -f docker-compose.prod.yml down
docker compose -f docker-compose.prod.yml up -d
# pulls the newer pinned image; migrations run automatically via the entrypointYour data persists across down/up in named volumes (pgsql-data,
the-desk-meili-<version>, redis-data, storage-app).
Meilisearch upgrades reindex automatically. The search index lives in a version-scoped volume, so bumping
MEILISEARCH_VERSIONstarts the new Meilisearch on a fresh volume and the app rebuilds the index from Postgres on boot (php artisan search:sync) — no manual dump/migration. The old volume is left behind; prune it withdocker volume rm the-desk-meili-<old-version>.
MAJOR version upgrades may contain breaking changes. Before upgrading across a major version, read the CHANGELOG and the corresponding GitHub Release notes for required manual steps.
Each release publishes a prebuilt image to the GitHub Container Registry at
ghcr.io/emmpaul/the-desk (tags X.Y.Z, X.Y, and latest; edge tracks the
tip of master), and the First install steps above pull it. If
you would rather build the image yourself — to audit or patch the source, or to
run in an air-gapped environment — layer the build overlay
(docker-compose.build.yml) on top. It restores a local build for the app
services (they share one image):
# Check out the matching release tag first so the build matches the compose file,
# then run gen-secrets.sh and edit .env exactly as in First install.
docker compose -f docker-compose.prod.yml -f docker-compose.build.yml up -d --buildEverything else — secrets, .env, and the automatic migrations — is identical to
the pull flow. To go back to the published image, drop the overlay and up -d
again.
Reverb settings are runtime, so mind the browser vs. server split. The container speaks plain
httpon8080(REVERB_PORT/REVERB_SCHEME), but the browser reaches Reverb through your TLS proxy onwss/443. SetREVERB_PORT_PUBLIC=443andREVERB_SCHEME_PUBLIC=httpsin.env; the browser-facing host defaults to yourAPP_URLhost (override withREVERB_HOST_PUBLIConly for a dedicated WebSocket subdomain).
| Service | Role |
|---|---|
app |
FrankenPHP web server (HTTP + migrations on boot) |
reverb |
WebSocket server for real-time broadcasting |
queue |
Queue worker (queue:work) |
scheduler |
Scheduled tasks (schedule:work) |
pgsql |
PostgreSQL database (named volume) |
meilisearch |
Full-text search index (named volume) |
redis |
Cache, session, and queue backend (named volume) |
Cache, session, and queue all use the Redis driver
(CACHE_STORE/SESSION_DRIVER=redis, QUEUE_CONNECTION=redis, with
REDIS_HOST=redis), and broadcasting uses Reverb. The redis service is
always-on and persists to the redis-data volume with appendonly enabled, so
queued jobs survive a restart; every app process waits for it to be healthy
before starting.
Contributions are welcome — bug reports, docs fixes, and pull requests. Please read CONTRIBUTING.md for the development setup, the quality gates (100% test coverage, Pint/PHPStan/Rector, and the frontend checks), the Conventional Commits convention, and the PR workflow.
Found a vulnerability? Please report it privately through GitHub's private vulnerability reporting rather than opening a public issue. See SECURITY.md for the full policy, supported versions, and response timeline. The codebase is scanned continuously with CodeQL, dependency review, and Dependabot; findings surface in the Security tab.
The Desk is open source under the MIT License.
