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.
Live demo · Website · Docs · Install · vs Slack · MIT License
The Desk ships as a single Docker Compose stack with a prebuilt image. The
installer fetches the compose file, generates secrets, and pins the latest
release, then up -d runs it with no build step:
curl -fsSL https://raw.githubusercontent.com/deskhq/the-desk/master/docker/install.sh | sh
# edit .env (APP_URL, mail, REVERB_*_PUBLIC), then:
docker compose up -dFull operator docs (requirements, configuration, reverse proxy & TLS, and upgrades) live at docs.thedeskhq.app.
Try it live at demo.thedeskhq.app — the login page shows the shared credentials.
Set DEMO_MODE=true to run a public, single-shared-account demo off the seeded
"Northwind Labs" workspace (php artisan demo:seed). Every visitor signs in as
the same owner, so the mode adds guard rails: destructive owner actions are
blocked, all outbound email is swallowed, message/attachment writes are
rate-limited per IP, self-registration is forced off, and an hourly reset heals
the workspace. It defaults to false — leave it off on any real deployment. See
Running a public demo.
Local development uses Laravel Sail:
git clone https://github.com/deskhq/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
./vendor/bin/sail npm run format:check # Prettier
./vendor/bin/sail npm run types:check # vue-tsc
./vendor/bin/sail npm run test:js # Vitest unit suite (composables, lib helpers, eslint rules)
./vendor/bin/sail npm run build # Vite build./vendor/bin/sail composer ci:check runs both gates in one command.
Rector 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 # runs bin/browser-testscomposer test:browser runs the suite through bin/browser-tests, which shards
it across paratest workers (~4.4x faster than the single-process run it
replaced). The worker count is capped at half the available cores, never below
two: leaving --parallel to its own default runs one worker per core, and
browsers, PHP servers and Playwright then compete for the same cores, which
makes the suite both slower and flakier. Pin a different count with
BROWSER_TEST_PROCESSES=N to measure another setting.
Both suites stop at the first defect, so a run whose verdict is already decided
no longer costs the full wall clock. Under paratest that stops scheduling new
work rather than killing what is in flight, so expect a handful of results after
the first ⨯ rather than a hard stop. When you want the whole picture instead
(a broad refactor, a flaky sweep, a triage run), turn it off:
BROWSER_TEST_BAIL=0 ./vendor/bin/sail composer test:browser # browser suite
./vendor/bin/sail artisan test --parallel --coverage --min=100 # php suiteThe same runner reaps the processes a run leaves behind — the Playwright server every run used to leak, plus the paratest workers a killed run strands — both before it starts and once it ends, naming each one it kills. Left alone those leftovers eat the cores the cap is there to protect, so a machine that had run a few sweeps failed unrelated tests as though the application had regressed. Only processes orphaned onto init and running out of this checkout are touched, so a second suite (or the PHP gate, whose workers look identical) is safe alongside.
Rebuild the frontend (./vendor/bin/sail npm run build) after changing any Vue
component the tests touch, since the in-process server serves the compiled Vite
assets — and after pulling someone else's, for the same reason. You don't have
to remember: the suite compares public/build/manifest.json against everything
Vite bundles and stops the run with that command if the bundle is behind, rather
than letting a stale bundle fail the tests as though the app were broken.
The live Reverb server gets the same treatment. Running the PHP gate can leave
the reverb container stopped, and with no WebSocket server the realtime tests
fail one by one on the message each was waiting for, which reads as a
broadcasting regression rather than as the absent server it is. The suite
therefore probes the endpoint once before the first test and stops with
./vendor/bin/sail up -d reverb if nothing answers.
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
[email protected]…[email protected](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
[email protected]…[email protected](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.
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.