Tunnels: CLI and Bridge reference
This page explains how Jetty tunnels work end to end: Bridge (web dashboard and API) and the jetty CLI. Use it alongside Sharing local sites for day-to-day commands and Installation for setup.
Table of contents
- Architecture
- Create vs resume
- Reserved labels and stable URLs
- Upstream health check
- Redirect rewrite host discovery
- Edge WebSocket and automatic reconnect
- Per-project configuration
- Request samples and the dashboard
- Replay and read-only observer links
- Tunnel notes
- Where to click in Bridge
- Plan limits (Dinghy vs paid)
- API overview
- Environment variables (cheat sheet)
- Team roles and permissions
- Timeouts
- Troubleshooting
- Routing rules
- Tunnel settings
Architecture
Traffic flow:
- Browser / webhook → HTTPS → your tunnel's public URL.
- Jetty's edge servers relay the request over a WebSocket to
jetty shareon your machine. jetty share→ HTTP → your local app (local_host:local_port).jetty share→ REST (Bearer token) → Bridge (heartbeats, optional request samples).
Internet your laptop
--------- -----------
[Client] --HTTPS--> [ Jetty cloud ]
|
WebSocket (TLS)
|
[ jetty share ] ----HTTP----> [ Local app ]
|
REST (API token)
v
[ Bridge API ]
Components:
| Piece | What it does |
|---|---|
| Bridge | Web dashboard you log into. Stores teams, tunnels, API tokens, reserved labels, optional request samples. Exposes /api/* for the CLI. |
jetty share |
Registers or attaches a tunnel via REST, then opens a WebSocket to Jetty's edge servers. For each incoming HTTP request the edge sends a frame; the CLI forwards to local_host:local_port and returns the response. |
| Heartbeats | While jetty share runs, the CLI periodically POSTs /api/tunnels/{id}/heartbeat so Bridge knows the tunnel is alive and can update byte/request counters. |
Nothing listens inbound on your laptop for the public internet; the tunnel is outbound from the CLI to Jetty's servers.
Create vs resume
When you run jetty share, the CLI usually lists your existing tunnels (GET /api/tunnels) and looks for a resumable row before creating a new one.
Resume calls POST /api/tunnels/{id}/attach, which rotates the agent token, updates local_host / local_port / server, and returns the same payload shape as create. That way you keep the same public URL and tunnel id when you restart the CLI.
Matching rules (all must match a candidate tunnel):
local_targetequals your current upstream, e.g.beacon.test:443(TLS-first for Valet/Herd-style hostnames),beacon.test:80, or127.0.0.1:8000(from--site/ port). For non-IP dev hostnames only,host:80andhost:443are treated as the same site for resume so switching HTTP↔HTTPS does not create a second tunnel row.server(tunnel server label) matches what you are using now, including both being empty/null when you do not set a server.- If you pass
--subdomain=(or a default from config), the tunnel’s subdomain must match that label exactly.
If --subdomain is not set, the CLI picks the highest numeric id among matching candidates (most recent row).
Disable resume: --no-resume or JETTY_SHARE_NO_RESUME=1 — always POST /api/tunnels (new row) when you need a fresh tunnel record.
Bridge too old: If attach returns 404/405, the CLI prints a short note and creates a new tunnel instead.
Why tunnels do not disappear when you exit jetty share: By design, the row stays in Bridge until you delete it (jetty delete <id>, dashboard, or JETTY_SHARE_DELETE_ON_EXIT=1 / --delete-on-exit). That keeps the same public URL when you come back, counts toward your team’s tunnel limit, and lets heartbeats show “last seen” until you remove it. Stale rows may be pruned automatically after extended inactivity.
Reserved labels and stable URLs
Teams can reserve subdomain labels so only their team can use them on the public tunnel host. Reservations are managed under Bridge → Domains (wording may vary).
CLI:
jetty domains— lists reserved slugs and a full host hint (GET /api/reserved-subdomains). Use--jsonfor scripts.jetty share … --subdomain=label— asks Bridge to allocate that label when allowed.
Config: set a default label in jetty.config.json / .jetty.json (see Per-project configuration) so you do not repeat --subdomain every time.
Enforcement: Bridge rejects labels that are not reserved for your team when policy requires it (see server-side allocator). Prefer reserving labels in the UI before relying on them in scripts.
Upstream health check
Before create or attach, the CLI can GET your local upstream at a path (default /) to avoid registering a tunnel when nothing is listening.
--health-path=/readyorJETTY_SHARE_HEALTH_PATH=/ready--no-health-checkorJETTY_SHARE_NO_HEALTH_CHECK=1— skip (CI, exotic stacks)
On failure you get a non-zero exit and stderr with a clear error (connection refused vs HTTP status), instead of a live tunnel that only returns 502 errors.
Redirect rewrite host discovery
The PHP agent builds a set of hostnames used to rewrite Location, Refresh, HTML/CSS/JS URLs, etc. Sources (all merged):
| Source | Notes |
|---|---|
| Upstream host | The local_host you share (127.0.0.1, myapp.test, …). |
JETTY_SHARE_CLI_UPSTREAM_HOSTNAME |
Optional single hostname (no port) merged into the lookup when the dev server’s canonical URL differs from local_host (e.g. you tunnel 127.0.0.1 but the app redirects to myapp.test). Prefer JETTY_SHARE_REWRITE_HOSTS or --site when possible. |
JETTY_SHARE_REWRITE_HOSTS |
Comma-separated extra hosts (and implied www. variants where applicable). |
JETTY_SHARE_PROJECT_ROOT |
If set, walk that directory for .env APP_URL (see per-project config). |
| Invocation directory | At jetty share start, the CLI sets JETTY_SHARE_INVOCATION_CWD to the real path of the current working directory so discovery stays stable if the agent’s CWD differs later. |
Walked-up .env |
From invocation dir (and from JETTY_SHARE_PROJECT_ROOT when set), APP_URL hosts are loaded from .env / .env.local / .env.development found upward. |
| Adjacent Laravel apps | For each directory along a walk upward from the invocation path (bounded), Jetty merges APP_URL from: (1) immediate subdirectories that contain an artisan file (up to 48 per level), and (2) the scan directory itself if it contains artisan. Opt out entirely with JETTY_SHARE_NO_ADJACENT_LARAVEL_SCAN=1. |
APP_URL env |
If set in the shell running jetty share, its hostnames are added. |
CLI hint: If you share an IP-only upstream (e.g. 127.0.0.1) and the rewrite lookup only knows one or two hosts, jetty share may print a warning suggesting you run from the app source tree, set JETTY_SHARE_PROJECT_ROOT, or use --site= so more canonical hosts enter the lookup (reduces “browser jumps off the tunnel”).
Structured file log (optional): JETTY_SHARE_DEBUG_NDJSON_FILE=/absolute/path.ndjson appends one JSON object per line when set. Each line has top-level event, ts_ms, rewrite_debug_rev (integer; bump when rewrite/NDJSON diagnostics change — use to detect a stale PHAR), and data (payload). Examples: edge.http_upstream_lookup after each proxied response; rewrite.* when JETTY_SHARE_DEBUG_REWRITE=1 and the file path is set. If JETTY_SHARE_DEBUG_REWRITE=1 but the file env is empty, the CLI prints a stderr hint. Unset = no file writes.
Edge WebSocket and automatic reconnect
The connection between the CLI and Jetty’s edge servers uses a WebSocket. If the connection drops (network blip, Wi-Fi change, laptop sleep), the CLI automatically reconnects with exponential backoff and re-registers the same tunnel -- your public URL keeps working.
- Opt out:
JETTY_SHARE_NO_EDGE_RECONNECT=1-- single session; on drop, forwarding stops but heartbeats may continue until you exit. - WebSocket ping keeps the connection alive through proxies and firewalls. Runs every
JETTY_SHARE_WS_PING_INTERVALseconds (default8). Override withJETTY_SHARE_WS_PING_INTERVAL=12(allowed 2--120). Disable pings withJETTY_SHARE_NO_WS_PING=1(not recommended behind strict proxies).
502 on connect: A 502 Bad Gateway when the CLI tries to connect usually means the edge servers are temporarily unavailable. The CLI prints the HTTP status and selected response headers so you can distinguish proxy errors from TLS or DNS problems. Use jetty share -v for verbose connection logging.
Per-project configuration
The CLI looks for a project config file by walking up from the current working directory (similar to finding a git root). It checks these filenames in order: jetty.yml, jetty.yaml, jetty.config.json, .jetty.json. The first match wins.
Precedence (highest first): CLI flags → project file → ~/.config/jetty/config.json (and ~/.jetty.json) → environment variables → defaults.
jetty.yml (recommended)
Create a jetty.yml in your project root:
# jetty.yml - project tunnel configuration
share:
subdomain: my-project
tunnel_server: us-east-1
# Routing rules (same as --route flags)
routes:
- match_type: path_prefix
path_prefix: /api
local_host: 127.0.0.1
local_port: 8080
enabled: true
- match_type: path_prefix
path_prefix: /admin
local_host: 127.0.0.1
local_port: 3001
enabled: true
# Auto-expire after 2 hours
expires: 2h
# Rewrite hosts for redirect handling
rewrite_hosts: "mysite.test,localhost"
JSON format
The same config as JSON (jetty.config.json):
{
"share": {
"subdomain": "my-project",
"tunnel_server": "us-east-1"
}
}
Share config keys
| Key | Purpose |
|---|---|
subdomain |
Default label for jetty share |
tunnel_server |
Default tunnel server label |
skip_edge |
Register the tunnel without opening a WebSocket (use --edge to force when debugging) |
rewrite_hosts |
Maps to JETTY_SHARE_REWRITE_HOSTS (comma-separated extra hosts to rewrite) |
project_root |
Force rewrite discovery from a fixed directory |
| Body/JS/CSS rewrite toggles | Align with JETTY_SHARE_NO_* flags documented in jetty help --advanced |
Use one canonical filename per repo to avoid drift. jetty.yml is recommended for new projects.
Request samples and the dashboard
While jetty share runs, the CLI can POST redacted metadata for each proxied request to Bridge: method, path, query, status, approximate byte sizes, and a sanitized header map (sensitive headers stripped). Request bodies are not stored in this path.
- Opt out:
JETTY_SHARE_CAPTURE_SAMPLES=0 - Retention: Bridge keeps a limited number of samples per tunnel.
- Redaction: Sensitive headers (auth, cookies, API keys) are stripped before storage. All
proxy-*headers are always omitted.
Dashboard: open Tunnels, then Monitor on a tunnel. You will see recent requests, a path prefix filter, and copy curl helpers.
API (authenticated):
GET /api/tunnels/{tunnel}/request-samples— list samples; optional?path_prefix=POST /api/tunnels/{tunnel}/request-samples— ingest (used by the CLI; not normally called by hand)
Replay and read-only observer links
Replay (CLI): jetty replay <sample-id> loads the sample + tunnel binding from GET /api/tunnel-request-samples/{id} and repeats the request against your local local_host:local_port.
- GET and HEAD only by default (avoid duplicating unsafe side effects).
JETTY_REPLAY_ALLOW_UNSAFE=1allows other methods (use with care).
Observer (dashboard): On the tunnel monitor page you can copy a signed, time-limited URL that opens a read-only view of recent samples without logging in. Anyone with the link can see metadata only; treat it like a secret. Optional path_prefix is included in the signature when you filter.
Tunnel notes
Each tunnel can have a note (short annotation), editable on the monitor page. The field appears in GET /api/tunnels and in jetty list --long for quick context (e.g. “QA ticket 123”).
routing_rules: Tunnel payloads include routing_rules -- an array of path- or header-match rules with per-rule local_host / local_port. You may set them on POST /api/tunnels, POST /api/tunnels/{id}/attach, or PATCH /api/tunnels/{id} (routing_rules: null clears). See Routing rules for the full guide.
Where to click in Bridge
| Area | What you use it for |
|---|---|
| Getting started / Tokens | Create API tokens for the CLI |
| Domains | Reserve labels; see full preview hostnames |
| Tunnels | List tunnels; open Monitor for live stats, notes, samples, observer link |
Plan limits (Dinghy vs paid)
Bridge applies per-organization limits derived from your billing plan:
- Dinghy (harbor) (free tier): typically one concurrent tunnel per team, two reserved labels, and no request-sample ingest or read-only observer links (upgrade to Captain/Fleet for webhook inspection).
- Captain / Fleet (paid): unlimited tunnels (unless a global cap applies), full reserved-label budget, request samples + observer links enabled.
API overview
All of these require a Sanctum token (Bearer) with access to a team (current team or token scoped to a team).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/tunnels |
List tunnels |
| POST | /api/tunnels |
Create tunnel |
| PATCH | /api/tunnels/{tunnel} |
Update note and/or routing_rules (at least one field required) |
| POST | /api/tunnels/{tunnel}/attach |
Resume / rotate agent token |
| DELETE | /api/tunnels/{tunnel} |
Delete tunnel |
| POST | /api/tunnels/{tunnel}/heartbeat |
Heartbeat + optional counters |
| GET | /api/reserved-subdomains |
Reserved labels + host suffix |
| GET | /api/tunnels/{tunnel}/request-samples |
List samples |
| POST | /api/tunnels/{tunnel}/request-samples |
Ingest sample (CLI) |
| GET | /api/tunnel-request-samples/{id} |
Single sample (+ tunnel for replay) |
Exact JSON shapes match what jetty/client expects; inspect responses with jetty share --verbose or a REST client.
local_host allowlist: Your organization may restrict which local_host values are allowed. If so, POST /api/tunnels and POST /api/tunnels/{id}/attach return 422 when the requested local_host is not permitted.
Environment variables (cheat sheet)
Resume / create
JETTY_SHARE_NO_RESUME=1— never attach; always create when possible.
Health
JETTY_SHARE_HEALTH_PATH— path for the upstream GET probeJETTY_SHARE_NO_HEALTH_CHECK=1— skip probe
Edge / capture
JETTY_SHARE_NO_EDGE_RECONNECT=1— disable WS reconnect loopJETTY_SHARE_CAPTURE_SAMPLES=0— do not POST request samples to Bridge
Replay
JETTY_REPLAY_ALLOW_UNSAFE=1— allow non-GET replay
Tunnel host hints (CLI output / rewriting)
JETTY_TUNNEL_HOST,JETTY_PUBLIC_TUNNEL_HOST— influence displayed host suffix in some flows (seejetty help --advanced)JETTY_SHARE_INVOCATION_CWD— set automatically by the CLI at share start; override only for unusual tests.JETTY_SHARE_CLI_UPSTREAM_HOSTNAME— optional single hostname merged into redirect rewrite lookup (see Redirect rewrite host discovery).JETTY_SHARE_PROJECT_ROOT— force rewrite APP_URL discovery from a fixed directory.JETTY_SHARE_NO_ADJACENT_LARAVEL_SCAN=1— disable adjacent-artisan/ walk-up merge for rewrite hosts.JETTY_SHARE_DEBUG_NDJSON_FILE— optional append-only NDJSON log path; each line isevent,ts_ms,rewrite_debug_rev,data(see Redirect rewrite host discovery).
For the full list (rewrite engine, idle teardown, static --serve, etc.), run jetty help --advanced in the package or PHAR.
Team roles and permissions
Jetty supports four team-level roles with a clear hierarchy. Organization admins (owner/admin) automatically receive Owner permissions on all teams in their organization.
| Role | Description |
|---|---|
| Owner | Full team control: manage members, delete team, all resources |
| Manager | Manage tunnels, tokens, domains, routing rules (cannot manage roster) |
| Developer | Create and use tunnels, create personal API tokens |
| Viewer | Read-only access to tunnels, request samples, and monitoring |
Permission matrix
| Action | Owner | Manager | Developer | Viewer |
|---|---|---|---|---|
| Manage team members | Yes | - | - | - |
| Create tunnel | Yes | Yes | Yes | - |
| Delete own tunnel | Yes | Yes | Yes | - |
| Delete any tunnel | Yes | Yes | - | - |
| Configure routing rules | Yes | Yes | - | - |
| Reserve subdomain | Yes | Yes | - | - |
| Add custom domain | Yes | Yes | - | - |
| Create API token | Yes | Yes | Yes | - |
| View tunnels | Yes | Yes | Yes | Yes |
| View request samples | Yes | Yes | Yes | Yes |
| Post request samples (CLI) | Yes | Yes | Yes | - |
Migration from previous roles
Existing member roles are automatically converted to developer during migration, preserving all current access. No action is required.
Default role
New team invitations default to developer. Organization admins can choose any role when inviting.
Timeouts
Two CLI-side timeouts you may want to adjust:
| Setting | Env Var | Default | What it controls |
|---|---|---|---|
| Upstream connect | JETTY_SHARE_UPSTREAM_CONNECT_TIMEOUT |
10s | How long the CLI waits to connect to your local app before returning an error. |
| WebSocket ping | JETTY_SHARE_WS_PING_INTERVAL |
8s (range 2--120) | Keepalive interval for the tunnel connection. Lower values survive aggressive corporate proxies; higher values reduce overhead. |
If your local app has slow endpoints (e.g. file uploads, long-running jobs), Jetty allows up to 60 seconds by default before returning a 504 Gateway Timeout. Per-tunnel overrides are available in Tunnel settings on the dashboard.
Troubleshooting
"Tunnel unavailable" even though CLI shows connected
This usually means multiple jetty share processes are running for the same tunnel. Each process connects and registers with the same subdomain, causing sessions to repeatedly replace each other. When the active connection goes stale, there is no valid agent for incoming HTTP -- so requests redirect to the "tunnel unavailable" page.
Symptoms:
- CLI shows
Edge agent connectedwith successful heartbeats - Browser shows "Tunnel unavailable" or 302 redirect
curlto the tunnel URL also returns 302- Repeated connection churn in CLI output for the same label
Fix: Kill duplicate processes and restart with a single jetty share:
# Find jetty share processes
ps aux | grep 'jetty share'
# Kill the extra ones (keep only one)
kill <pid1> <pid2> ...
# Restart fresh
jetty share --site=mysite.test
Prevention: Since CLI version 0.1.19, jetty share detects existing processes for the same tunnel and refuses to start by default. Use --force or -f to override (not recommended).
[jetty share rewrite] redirect_headers: skipped (no tunnel Host on request)
A request was forwarded, but the Host header your app sees may not match the public tunnel hostname until traffic flows through the real public URL. Ensure JETTY_SHARE_REWRITE_HOSTS includes every host your app emits in redirects (Valet/Herd site names, localhost, etc.). See README sections on rewrite and JETTY_SHARE_DEBUG_REWRITE=1.
502 after “connected”
Usually local upstream refused connection or returned an error. Run the health check path manually with curl to the same --site and port.
Resume attached the wrong tunnel
Matching uses local target + server + optional subdomain. Align --site, port, --server, and --subdomain with the tunnel row in Bridge, or use JETTY_SHARE_NO_RESUME=1 once to create a fresh tunnel.
Samples empty
Confirm JETTY_SHARE_CAPTURE_SAMPLES is not 0, the tunnel is receiving traffic, and you are looking at the correct tunnel in Monitor.
Routing rules
Path- and header-based routing lets you forward requests from a single public URL to different local upstreams. Rules are evaluated top to bottom; the first match wins; unmatched requests go to the tunnel’s primary upstream.
Configure rules from the tunnel detail view in Bridge under Routing Rules (Advanced). Available on Captain and Fleet plans.
See Routing rules for the full guide.
Tunnel settings
Each tunnel has configurable settings for rate limiting, upstream timeouts, max body size, header redaction, HTTP basic auth, webhook signature verification, and automatic expiry.
See Tunnel settings for the full guide.
Testing and simulation
Jetty includes built-in tools for testing without writing backend code:
- Virtual endpoints -- define mock HTTP responses directly in Bridge; requests are handled at the edge without a local server.
- Record/replay -- capture real traffic flowing through your tunnel and replay it later for regression testing or offline development.
- Fault injection -- inject latency, errors, or connection failures to test how your app handles degraded conditions.
- Response templates -- use dynamic variables (
{{timestamp}},{{request.path}},{{uuid}}) in virtual endpoint responses.
See Testing and simulation for the full guide.
See also
- Sharing local sites -- quick recipes (port,
--site, subdomains, Valet TLS) - Installation
- Routing rules -- path and header-based routing
- Tunnel settings -- per-tunnel configuration
- Testing and simulation -- virtual endpoints, record/replay, fault injection
Send feedback
Found an issue or have a suggestion? Let us know.