brainstate

deskhq/the-desk · source

Snapshot 0 · 2026-11-04T12:47:28Z · PHP · MIT

Content fingerprint665756247792432620f8a3441b4bcc318e2b89aa4c94033f9553a62a84f157b3

GitHub description at capture

Calm, fast, self-hosted team chat you run yourself — channels, threads, reminders, scheduled messages. One Docker compose file, MIT-licensed. Laravel + Vue + Reverb.

README at capture

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset=".github/readme-banner-dark.png">
    <img src=".github/readme-banner-light.png" alt="The Desk — open source, self-hosted team chat" width="800">
  </picture>
</p>

<p align="center">
  A calm, fast, <strong>self-hosted team chat</strong> 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.
</p>

<p align="center">
  <a href="LICENSE"><img src="https://img.shields.io/github/license/deskhq/the-desk?color=c9a35c" alt="License: MIT"></a>
  <a href="https://www.php.net"><img src="https://img.shields.io/badge/PHP-8.5-777bb4" alt="PHP 8.5"></a>
  <a href="https://laravel.com"><img src="https://img.shields.io/badge/Laravel-13-ff2d20" alt="Laravel 13"></a>
  <a href="https://vuejs.org"><img src="https://img.shields.io/badge/Vue-3-42b883" alt="Vue 3"></a>
</p>

<p align="center">
  <a href="https://github.com/deskhq/the-desk/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/deskhq/the-desk/tests.yml?branch=master&label=tests" alt="Tests"></a>
  <a href="https://github.com/deskhq/the-desk/releases/latest"><img src="https://img.shields.io/github/v/release/deskhq/the-desk?color=c9a35c" alt="Latest release"></a>
  <a href="https://github.com/deskhq/the-desk/releases"><img src="https://img.shields.io/github/release-date/deskhq/the-desk?color=c9a35c" alt="Latest release date"></a>
  <a href="https://github.com/deskhq/the-desk/commits/master"><img src="https://img.shields.io/github/last-commit/deskhq/the-desk/master?color=c9a35c" alt="Last commit"></a>
  <a href="https://github.com/deskhq/the-desk"><img src="https://img.shields.io/github/stars/deskhq/the-desk?style=flat&color=c9a35c" alt="GitHub stars"></a>
</p>

<p align="center">
  <a href="https://demo.thedeskhq.app/"><strong>Live&nbsp;demo</strong></a> ·
  <a href="https://thedeskhq.app">Website</a> ·
  <a href="https://docs.thedeskhq.app/">Docs</a> ·
  <a href="https://docs.thedeskhq.app/self-hosting/installation/">Install</a> ·
  <a href="https://docs.thedeskhq.app/comparison/">vs&nbsp;Slack</a> ·
  <a href="LICENSE">MIT&nbsp;License</a>
</p>

<!-- Generated by `bin/capture-shell` from the seeded demo workspace, not
captured by hand, and re-checked in CI so it cannot drift from the app. See
CONTRIBUTING.md -> "The product shots". -->
<p align="center">
  <img src=".github/screenshot-app.png" alt="The Desk: a channel timeline with threads, reactions, and direct messages" width="900">
</p>

## Self-hosting

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:

```bash
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 -d
```

Full operator docs (requirements, configuration, reverse proxy & TLS, and
upgrades) live at
**[docs.thedeskhq.app](https://docs.thedeskhq.app/)**.

### Public demo

Try it live at **[demo.thedeskhq.app](https://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](https://docs.thedeskhq.app/self-hosting/demo/)**.

## Development

Local development uses [Laravel Sail](https://laravel.com/docs/sail):

```bash
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 setup
```

Run the quality gate before pushing:

```bash
./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](https://github.com/rectorphp/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:

```bash
./vendor/bin/sail composer refactor    # apply Rector's suggested refactors
```

### Browser (E2E) realtime tests

`tests/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):

```bash
./vendor/bin/sail npm ci                              # playwright npm package
./vendor/bin/sail npx playwright install chromium     # the browser binary
```

Then, with Sail up (Reverb is part of `sail up -d`) and the frontend built:

```bash
./vendor/bin/sail npm run build                       # tests use the built assets
./vendor/bin/sail composer test:browser               # runs bin/browser-tests
```

`composer 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:

```bash
BROWSER_TEST_BAIL=0 ./vendor/bin/sail composer test:browser        # browser suite
./vendor/bin/sail artisan test --parallel --coverage --min=100     # php suite
```

The 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.

### Local SSO providers (OIDC & LDAP)

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](https://github.com/Soluto/oidc-server-mock))
  seeded with `oidc1@the-desk.local` … `oidc4@the-desk.local` (password
  `password`) and a pre-registered `the-desk-dev` client. It's reached as
  `oidc:8081` from both the browser and the app container so the derived issuer
  stays consistent, so add one line to your host's `/etc/hosts` once:

    ```
    127.0.0.1 oidc
    ```

    Then set `SSO_OIDC_ISSUER=http://oidc:8081`, `SSO_OIDC_CLIENT_ID=the-desk-dev`,
    `SSO_OIDC_CLIENT_SECRET=the-desk-dev-secret` and use "Sign in with SSO".

- **LDAP** — an OpenLDAP directory ([osixia/openldap](https://github.com/osixia/docker-openldap))
  seeded with `ldap1@the-desk.local` … `ldap4@the-desk.local` (password
  `password`) under `dc=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 objectGUID
    ```

    Then sign in through the normal login form with a seeded email.

## Contributing

Contributions are welcome — bug reports, docs fixes, and pull requests. Please
read **[CONTRIBUTING.md](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.

## Security

Found a vulnerability? Please report it privately through GitHub's
[private vulnerability reporting](https://github.com/deskhq/the-desk/security/advisories/new)
rather than opening a public issue. See **[SECURITY.md](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](https://github.com/deskhq/the-desk/security).

## License

The Desk is open source under the [MIT License](LICENSE).