exposedev/server
exposedev/server is the server-side component of Expose, the open-source PHP tunneling service. Run your own tunnel server to securely expose local Laravel or PHP applications to the web, with WebSocket-based connections and a self-hosted alternative to ngrok.
exposedev/server is the self-hosted relay half of Expose — the public-facing service that terminates TLS, accepts tunnel connections from clients, and forwards HTTP/WebSocket traffic back to a developer's machine. A Laravel developer touches it in two ways: as a consumer (pointing the client at the hosted or company server) and as an operator (running the server so the team doesn't depend on a third-party tunnel).
Minimal path:
# operator side
composer global require exposedev/server
expose serve # first run writes config + SQLite state, prints dashboard URL
# developer side (Laravel app)
composer require --dev exposedev/laravel-expose # the client/artisan bridge
php artisan expose
Look first at, in order:
host, port, dashboard subdomain, certificate authority paths, database path, and the TLS/ACME block. Everything else is downstream of these.server_host, server_port, and the auth token on the client side must match the server; most "it doesn't connect" reports are a mismatch here.First use case: php artisan expose against php artisan serve / Herd / Valet, paste the resulting https://<subdomain>.<your-server> into Stripe, GitHub, or Slack, and iterate on the webhook handler on your local machine with a real TLS endpoint.
Per-developer subdomains, server-issued tokens. Create a dashboard user per developer rather than sharing one token. Stable reserved subdomains (--subdomain=alice) mean webhook URLs in provider dashboards don't churn, and revocation is per-person.
Team server as infrastructure. Run expose serve under systemd/supervisor, persist the SQLite database and the certificate storage directory on a volume, and back them up. Treat the process as stateful — losing the cert store means re-issuing every certificate.
Two TLS topologies, pick one deliberately. Either the Expose server terminates TLS itself (needs 443 plus the tunnel port reachable, and its own cert story), or you front it with nginx/Caddy and disable the built-in TLS. Don't run both — double termination produces confusing redirect and header behaviour.
On-demand certificates (3.2.0). The server can now request certificates on demand for its own infrastructure hostnames (dashboard, API host), not just for client-submitted custom domains. Practical pattern: DNS for expose.example.com (dashboard) and *.tunnel.example.com (client subdomains) points at the server, on-demand issuance is enabled, and the allowlist/ask-style gate covers exactly those names. This removes the manual step of provisioning the dashboard cert before the dashboard is usable over HTTPS.
Config via environment in deployment. Keep the checked-in config file as defaults and override host/port/ACME contact/CA paths through env vars in the deploy manifest, so staging and production differ only in env.
Webhook fidelity. The tunnel is a byte-level proxy — raw bodies and headers arrive intact, so Stripe-Signature / X-Hub-Signature-256 verification keeps working. Do not enable any request-rewriting middleware on the tunnel path.
Custom domains per client. When a developer needs hooks.acme-client.test to hit their app, register it as a custom domain, point DNS at the server, and let on-demand issuance cover it. Keep this capability gated to trusted users only.
Host headers. Without a strict allowlist (or an ACME ask endpoint that validates the requested name against your known hosts), anyone who points DNS at your IP can obtain a certificate from your server and borrow its IP reputation. The 3.2.0 change widens issuance to server-owned hosts — verify the gate covers those names too and isn't just "any host that resolves here".validate_client_ips behind a proxy. If the server sits behind a load balancer, forwarded-header trust must be configured or every client is rejected with a validation error. Symptom is a clean connect then immediate close.Http:: calls) to the public tunnel URL will fail on TLS unless the CA bundle is trusted — usually better to call localhost directly and use the tunnel only for inbound.openssl s_client -connect host:443 -servername <subdomain> to separate DNS/cert problems from tunnel problems. A working TLS handshake with a 502 means the client disconnected, not that the server is broken.^3.2 and restart the service after upgrading so the new issuance path is loaded.How can I help you explore Laravel packages today?