exposedev/expose
Expose is a Laravel package that replaces dd() with a powerful debugging dashboard. Dump variables, queries, models, emails, and more to a beautiful web UI, inspect them without breaking your app, and share results with your team.
Expose is a CLI tunneling client (plus a self-hostable server), not a Laravel package with a service provider — so the first mental adjustment is that you install it globally or as a dev binary, then run it next to artisan serve / Valet / Herd / Sail.
Install it as a tool, not a dependency.
composer global require exposedev/expose
# or, pinned per project so the whole team uses the same version:
composer require --dev exposedev/expose
./vendor/bin/expose --version
Make sure ~/.composer/vendor/bin (or ~/.config/composer/vendor/bin) is on PATH if you go global.
Read expose --help and expose share --help before anything else. Flag names have drifted across releases (subdomain, auth, host, port options). The command help in your installed version is the only reliable reference; treat blog posts and older docs as hints.
Authenticate once. The first expose share run walks you through obtaining/storing a token in your home-directory config. Confirm where that file lands and whether it contains a secret — that determines whether it's safe to copy between machines.
First real use case: share a local Laravel app.
# terminal 1
php artisan serve --host=127.0.0.1 --port=8000
# terminal 2
expose share http://127.0.0.1:8000
With Valet/Herd it's simply expose share myapp.test. You get a public HTTPS URL that proxies to your local app — useful immediately for client demos, QA on a real phone, and webhook receivers.
Where to look first when something is wrong: the tunnel process output (run with the verbose flag), then the Laravel log — most failures are Laravel rejecting the request (host/CSRF/HTTPS), not the tunnel dropping it.
Webhook receiver loop (Stripe, GitHub, Slack, Shopify). This is the highest-value daily pattern. Keep a tunnel running in a dedicated terminal/tmux pane, point the provider's webhook URL at the tunnel host, and let it hit your local route. Because free/ephemeral subdomains rotate per session, either use a reserved subdomain (if your plan/self-hosted server supports one) or wrap the provider config update into a script so re-pointing is one command.
Two-process dev script. Codify it so nobody forgets the second terminal:
"scripts": {
"share": "expose share http://127.0.0.1:8000"
}
or a make share / Composer script that starts artisan serve in the background and Expose in the foreground. Prefer foreground for the tunnel so Ctrl-C cleans up the connection.
Project-level config for repeatable flags. If you're always sharing the same host with the same subdomain and basic-auth credentials, put those defaults in Expose's config file (created on first run / via its init command) rather than retyping flags. Keep credentials out of the committed repo; commit only the non-secret defaults.
Protect anything that isn't a throwaway demo. Layer Expose's own basic-auth option in front of the tunnel and keep Laravel's auth on sensitive routes. Before exposing, confirm APP_DEBUG=false if outsiders will hit it, and that /telescope, /horizon, /ignition, and .env are not reachable.
Sail / Docker. Run the Expose client on the host and point it at the published port (expose share http://localhost:80), not at a container hostname it can't resolve. If you must run it inside a container, target host.docker.internal and expect an extra layer of networking to debug.
Self-hosting for a team. The server component lets you run your own relay on a VPS: wildcard DNS for the tunnel domain, ports 80/443 reachable, TLS terminated by the server, then clients configured to talk to your host instead of the default. This is the right answer when an org won't allow a third-party tunnel. Pin client and server to compatible versions — mismatched protocol versions are the classic self-host failure.
Mobile/device QA handoff. Share the tunnel URL plus the basic-auth credentials in one message, with a note that it dies when your laptop sleeps. For longer-lived previews, run the tunnel on a always-on box or under a process supervisor.
OAuth callback testing. Workable only with a stable hostname, because the callback URL is registered at the provider. Use a reserved subdomain, register it once, and accept that you'll juggle providers if you test several.
CI/E2E webhook fixtures. Possible (spin up a tunnel, fire a fixture webhook at it, assert), but treat it as a flaky tier: network-dependent, rate-limited, and slow to fail. Gate it behind a label rather than running it on every push.
config/expose.php. There's no Laravel service provider magic to lean on; integration is process orchestration and HTTP-header correctness. If an Artisan wrapper exists in your version, it's a thin shell around the CLI — still debug at the CLI level.route(), asset(), redirects, and signed URLs come out as http:// and you get mixed-content or signature mismatches. Ensure TrustProxies covers the tunnel (or * in local dev) so X-Forwarded-Proto is honored.APP_URL versus request host. Laravel often derives URLs from the incoming Host, which is the tunnel domain — usually what you want. But anything using config('app.url') (queued mail, some notification links, ASSET_URL) will still point at myapp.test and be unreachable from the outside. Decide deliberately: leaving APP_URL alone is usually correct for tunnels; forcing it to the tunnel URL creates churn when the subdomain rotates.SESSION_DOMAIN of .myapp.test won't apply to the tunnel host, so sessions silently reset. Same class of problem for Sanctum stateful domains and CSRF cookie domain on SPA setups.npm run build, or configure server.hmr.host/clientPort to the tunnel host and expose the Vite port too. Otherwise the page loads but assets/hot updates fail in confusing ways.curl against the tunnel and against localhost to isolate.openssl, and typically pcntl/sockets depending on platform) and behaves best on Linux/macOS; Windows has historically been the roughest platform. If install succeeds but the binary exits immediately, check extensions and PHP version before anything else.expose.* config..env. A tunnel is a public ingress; the same hygiene you'd apply to a staging URL applies here, plus the hostname is guessable.How can I help you explore Laravel packages today?