Documentation for Jetty

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

  1. Architecture
  2. Create vs resume
  3. Reserved labels and stable URLs
  4. Upstream health check
  5. Redirect rewrite host discovery
  6. Edge WebSocket and automatic reconnect
  7. Per-project configuration
  8. Request samples and the dashboard
  9. Replay and read-only observer links
  10. Tunnel notes
  11. Where to click in Bridge
  12. Plan limits (Dinghy vs paid)
  13. API overview
  14. Environment variables (cheat sheet)
  15. Team roles and permissions
  16. Timeouts
  17. Troubleshooting
  18. Routing rules
  19. Tunnel settings

Architecture

Traffic flow:

  1. Browser / webhook → HTTPS → your tunnel's public URL.
  2. Jetty's edge servers relay the request over a WebSocket to jetty share on your machine.
  3. jetty share → HTTP → your local app (local_host:local_port).
  4. 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_target equals your current upstream, e.g. beacon.test:443 (TLS-first for Valet/Herd-style hostnames), beacon.test:80, or 127.0.0.1:8000 (from --site / port). For non-IP dev hostnames only, host:80 and host:443 are 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 --json for 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=/ready or JETTY_SHARE_HEALTH_PATH=/ready
  • --no-health-check or JETTY_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_INTERVAL seconds (default 8). Override with JETTY_SHARE_WS_PING_INTERVAL=12 (allowed 2--120). Disable pings with JETTY_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.

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 (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=1 allows 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 probe
  • JETTY_SHARE_NO_HEALTH_CHECK=1 — skip probe

Edge / capture

  • JETTY_SHARE_NO_EDGE_RECONNECT=1 — disable WS reconnect loop
  • JETTY_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 (see jetty 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 is event, 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 connected with successful heartbeats
  • Browser shows "Tunnel unavailable" or 302 redirect
  • curl to 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

Send feedback

Found an issue or have a suggestion? Let us know.