Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Expose Laravel Package

beyondcode/expose

Expose by BeyondCode shares local Laravel or PHP sites through secure public URLs. Create tunnels, use custom domains, inspect requests, and test webhooks or mobile apps without deploying.

View on GitHub
Deep Wiki
Context7

Getting Started

Expose is a CLI sidecar, not a Composer library. Nothing lands in app/, config/, or your service providers — you install it globally, point it at a port or a Herd/Valet hostname, and get a public HTTPS URL back. Adoption and rollback are both "stop running the command," which is why it's a low-regret thing to try on any given afternoon.

Install and pin it:

# PHAR (checksum-verifiable, easiest to pin)
curl -s https://beyondco.de/expose.sh | bash

# or via the Composer toolchain you already have
composer global require beyondcode/expose

# or Homebrew / Docker, if that's your existing standard

Authenticate once:

expose auth token <your-token>   # from the Expose dashboard

First public URL, in about a minute:

# terminal 1
php artisan serve                      # localhost:8000

# terminal 2
expose share localhost:8000            # → https://random-words.expose.dev

# Herd / Valet site — use the hostname, not the port
expose share myapp.test

The first use case to prove out: webhook iteration. Take a Stripe test-mode endpoint, paste the tunnel URL plus your route path into the provider dashboard, fire a test event, and watch it land in storage/logs/laravel.log. That one loop — edit controller, replay event, read log — is where Expose pays for itself, and it takes five minutes to validate.

Where to look first:

  1. expose share --help — subdomain, region, server, and auth flags drift between versions; trust --help over any blog post.
  2. The request inspector dashboard — request/response bodies, headers, and replay. This is the real differentiator versus ngrok and where you'll spend most of your debugging time.
  3. expose serve — the self-hostable relay, only if data-handling or egress rules force it.

Implementation Patterns

Two-pane webhook loop. Keep the tunnel alive in its own terminal for the whole session so the URL stays stable while you iterate:

# pane 1
php artisan serve

# pane 2 — leave running ALL session
expose share localhost:8000 --subdomain=myteam-webhooks

Then script the provider side rather than clicking through dashboards. A Makefile target or Composer script that prints the current tunnel URL (or calls the Stripe/GitHub API to update the endpoint) turns "re-point the webhook every morning" into one command.

Use the hostname, not the port, for Herd/Valet and multi-tenant apps. expose share myapp.test preserves the Host header your app expects, so Route::domain(), tenant resolution from host, and TrustHosts middleware behave exactly as they do locally. expose share localhost against a Valet site usually 404s because the proxy forwards the wrong host.

OAuth and social-login callbacks. Register https://<tunnel-host>/auth/callback with the provider, but understand the coupling: if the tunnel host rotates next session, the registered callback breaks. Either reserve a stable subdomain per project or keep a single throwaway OAuth app dedicated to local dev with a tolerant redirect config.

Demos and QA handoff. Reserve a subdomain, run against a seeded local database (never production), and treat the tunnel as a short-lived preview link. If your version supports tunnel-level basic auth, use it as a cheap gate — but it is not a substitute for the app's own authorization.

Docker / Sail / devcontainers. The client has to reach the port the way your browser would. Most reliable: run expose on the host against the published port. Inside Compose, localhost means the container — use the service name or host.docker.internal. In a devcontainer, add install + auth to postCreate, but launch manually per session; you don't want it silently running in every container.

Self-hosted relay — the only real integration work in the package:

# on a VPS: wildcard DNS *.tunnel.example.com → this host, ports 80/443 open
expose serve

# on your machine
expose share localhost:8000 --server=https://tunnel.example.com

Do this only when compliance, contracts, or egress rules require it. It converts a free tool into wildcard DNS, certificate renewal, patch cadence, and an on-call rota you own.

WebSockets and streaming. Reverb, SSE, and long-polling generally pass through, but benchmark before you depend on it — streaming buffering and long-lived connections are exactly where a relay becomes the bottleneck. If your day-to-day is real-time, self-host and test it yourself.

Gotchas and Tips

The 3.2.4 Host change is the one to watch. Since 3.2.4 the client no longer appends the default port to the Host header — correct per RFC 9110, but it changes what your app observes on ports 80/443. Anything reading request()->getHost(), using TrustHosts or a host allowlist, resolving tenants from host, or generating absolute URLs (including URL::signedRoute() / temporarySignedRoute(), where host is part of the signature) can shift behaviour. Write a regression test for host-based routing before rolling it out, and keep 3.2.3 available as a fallback for one cycle.

APP_URL and generated absolute URLs will point at localhost. Links in emails, redirects, and signed URLs come out wrong unless you either set APP_URL to the tunnel URL for the session or configure TrustProxies so Laravel honours the forwarded host/proto. For a session-scoped fix, override APP_URL in .env (and revert it afterwards) or export it in the shell running php artisan serve.

Clear SESSION_DOMAIN locally. If you've set something like .myapp.test for SSO or subdomain cookie sharing, the tunnel host won't receive the session cookie and you'll get login loops that look like an auth bug. Set it to null in your local .env.

Free-tier subdomains rotate every run — that's the real cost, not latency. The friction isn't tunnel speed, it's re-pointing Stripe/GitHub/Shopify endpoints and re-registering OAuth callbacks. If a workflow is more than a one-off, reserve a subdomain or self-host where you control allocation.

Read the status code to know which side failed. 401/403 straight from the relay is a token problem (expose auth token again; tokens expire). 502 means the relay reached your client but the local server is down or on the wrong port. 404 is ambiguous — check the inspector: if the request shows there with a body, the tunnel did its job and your route matched nothing (usually a Host/domain constraint).

Don't debug large uploads or truncated payloads through the inspector. Bodies are capped for display; a "missing field" in the dashboard may still be present in the real request. Verify against storage/logs/laravel.log or a Log::info($request->all()) before chasing ghosts.

Raw-body signature verification still works — the proxy doesn't re-encode the body — but only if your middleware reads the raw input. If you've already parsed the JSON before verifying the Stripe/GitHub HMAC, that's a pre-existing bug the tunnel just made visible.

Keep it out of require. Global install or PHAR only. If you script it for a team, pin the version and verify checksums in provisioning; a downloaded PHAR is executable code on a developer laptop and deserves the same provenance policy as any other binary.

Relay restarts drop every tunnel at once. If you self-host, deploying the relay is a fleet-wide disconnect — put it in the runbook before someone hits it mid-demo. Subdomain allocation is held in memory, so horizontal scaling needs sticky routing or a shared registry.

Tunnel-level auth is not app auth. A basic-auth gate keeps casual visitors out of a demo; it does nothing about an unauthenticated admin route once someone is past it. Revoke tokens when a developer leaves, and never tunnel a database admin UI or queue dashboard to a public hostname.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource