Tunneling
Connect a public HTTPS URL to a local service, protect access, and capture requests for replay.
lpm tunnel connects a public HTTPS URL to one HTTP service on your computer. LPM CLI opens the connection from your computer.
Use a tunnel for webhooks, callbacks, remote-device tests, and temporary previews. A tunnel is a development tool, not permanent application hosting.
Tunnels need a Pro plan or one of your organization's claimed domains. See Plans and limits.
# Expose an existing HTTP service
lpm login
lpm tunnel 3000
# Start the project and expose its primary endpoint
lpm dev --tunnelHow a tunnel works
public client
│ HTTPS or WebSocket
▼
LPM.dev Registry tunnel service
│ open tunnel connection
▼
LPM CLI
│ local HTTP
▼
your local servicePublic visitors use HTTPS. LPM CLI forwards each request to the local service and returns the local response.
Public WebSocket connections use the same route. You do not need to open an inbound port on your router or firewall.
The tunnel lasts while the LPM CLI process remains open. When the process stops, the public URL stops forwarding requests.
Choose a workflow
| Goal | Workflow |
|---|---|
| Use the same domain for a project | Add tunnel.domain to lpm.json, then run lpm dev |
| Use your Pro account domain for one development run | Run lpm dev --tunnel |
| Expose an existing local port | Run lpm tunnel <port> |
Expose the only active lpm dev endpoint | Run lpm tunnel |
| Use a claimed domain for one development run | Run lpm dev --domain <domain> |
| Use a claimed domain with a standalone tunnel | Run lpm tunnel <port> <domain> |
The lpm tunnel command page contains the complete syntax, recipes, actions, and flags.
Project configuration
Use lpm.json for a stable project workflow:
{
"$schema": "https://cli.lpm.dev/schemas/lpm.json",
"tunnel": {
"domain": "acme-api.lpm.llc"
}
}Start the project:
lpm devThe domain field enables the tunnel automatically. The tunnel object currently accepts only domain.
LPM CLI waits for the final local endpoint before it opens the tunnel. This behavior supports development servers that change their initial port.
Use a different claimed domain for one run:
lpm dev --domain preview-api.lpm.llcDisable the project tunnel for one run:
lpm dev --no-tunnelIf lpm.json contains multiple services, mark exactly one service as primary. LPM CLI sends tunnel traffic to the final endpoint of that service.
Domain types
The LPM.dev Registry provides two tunnel base domains:
| Base domain | Availability | Use |
|---|---|---|
lpm.fyi | Pro and Org | Pro account domains and stable claims |
lpm.llc | Pro and Org | Stable claims |
Run lpm tunnel domains to list the base domains that are currently enabled.
Pro personal domains
A Pro personal tunnel without an explicit domain uses <username>.lpm.fyi. This domain remains the same while the username remains the same.
A Pro user can also claim up to three stable domains. Claims can use lpm.fyi, lpm.llc, or both.
Organization domains
An Org account can claim up to three stable domains. The claim belongs to the organization, not to the member who uses it.
Organization owners and administrators can manage organization claims. Any eligible member can use a domain of the organization. Name the domain in the command, for example lpm tunnel 3000 staging.lpm.fyi. A personal Pro plan is not necessary. If a Pro member names no domain, the member gets the hostname of their personal account.
After the billing of an organization lapses, members cannot use its claims, and LPM.dev Registry releases them. When a Pro plan ends, LPM.dev Registry releases its personal claims.
Claim a stable domain
Claim a personal domain:
lpm tunnel claim acme-api.lpm.llcClaim an organization domain:
lpm tunnel claim staging.lpm.fyi --org acmeA claimed subdomain contains 3–32 lowercase letters, numbers, or hyphens. It starts and ends with a letter or number.
The third and fourth characters cannot both be hyphens. Account names on lpm.fyi are reserved. The subdomains relay and invalidate are reserved on every base domain.
Claims are separate for each base domain. For example, acme.lpm.fyi and acme.lpm.llc use two claim slots.
A reconnect to the same claimed domain replaces the old session slot. This replacement does not temporarily use an additional concurrent-tunnel slot.
See the stable-domain commands for listing, use, and release examples.
Plans and limits
Tunnels need a Pro plan or an organization plan. An organization tunnel uses one of the organization's claimed domains. Limits apply to the selected personal or organization account.
| Limit | Pro | Org |
|---|---|---|
| Concurrent tunnels | 3 | 3 |
| Included requests | 100,000 per billing period | 100,000 per billed seat and billing period |
| Account request rate | 20,000 per minute | 20,000 per billed seat and minute |
| Per visitor IP | 600 per minute | 600 per minute |
| Maximum request body | 11.5 MiB | 11.5 MiB |
| Stable domain claims | 3 | 3 |
--tunnel-auth | Yes | Yes |
| After the included allowance | Proportional overage | Proportional overage |
The Org concurrent-tunnel and domain-claim limits apply across all members and base domains. These limits do not increase with the seat count.
A request body must arrive in two minutes or less. If a body is larger than the limit, it gets 413. If a body is slower, it gets 408. A body also gets 413 if its headers are too large for the relay to send with it. This occurs only near the size limit. If the relay receives many large uploads at the same time, a new upload can get 503 with Retry-After: 5.
A public WebSocket message larger than about 12 MiB closes that socket with code 1009.
The startup output reports the active plan, limits, request usage, and overage state.
If the plan of an account no longer includes tunnels, the relay closes the open tunnels of the account, usually within a minute. This occurs, for example, after a downgrade, or when the billing of an organization lapses.
Request usage
The included allowance belongs to the account, not to each tunnel. Multiple tunnels consume the same account allowance.
One accepted HTTP request counts once. If the local service returns an error or is unavailable, the request still counts.
A public WebSocket upgrade also counts once. It counts also if the local service refuses the upgrade.
Messages on an open WebSocket do not use more tunnel requests. Rejected authentication does not count. A request whose declared Content-Length is over the body limit does not count either. The relay accepts a request before it reads the body. As a result, an accepted request counts, also if its body is then too large, too slow, or incomplete.
Pro and Org usage follows the billing period. Paid overage costs $1 for each 100,000 requests and uses proportional billing.
Complimentary Pro accounts without a subscription receive the Pro allowance each UTC calendar month. These accounts retain Pro domain and connection limits. They stop accepting requests at the included allowance because paid overage requires a subscription.
Paid overage is enabled by default. You can disable it in the personal or organization tunnel dashboard.
If paid overage is disabled, the tunnel stops accepting requests at the included allowance. A change to overage affects only future requests.
Authentication and access protection
Tunnel startup and domain actions require a saved lpm login session. --token, LPM_TOKEN, and CI-only tokens cannot replace this session.
The local inspect, replay, and log actions do not require a login. These actions use the capture database of the project: the nearest directory at or above the current one that has a package.json or an lpm.json, stopping before your home directory. Tunnel startup uses the same project.
Public access
A tunnel URL accepts public requests by default. Application authentication remains the responsibility of your local service.
Tunnels can add tunnel-level access protection:
lpm tunnel 3000 acme-api.lpm.llc --tunnel-authLPM CLI creates a token for the session. Interactive startup shows the public URL and an X-Tunnel-Auth header.
Non-interactive human output hides the token. JSON output includes it. Treat that output as secret. The tunnel service rejects requests without the correct token.
Local HTTP and public HTTPS
An explicit port always selects http://127.0.0.1:<port>/. The public tunnel URL still uses HTTPS.
Without a port, lpm tunnel selects the only active lpm dev endpoint. It preserves the endpoint address, port, and base path.
If no active endpoint exists, pass a port. If multiple endpoints exist, pass the intended port.
lpm dev --https remains compatible with tunnels. LPM CLI tunnels the underlying local HTTP endpoint while it provides trusted browser HTTPS locally.
Do not give a standalone tunnel a framework HTTPS port. Use a plain HTTP child service for tunnel forwarding.
Buffered HTTP responses have a 50 MiB body limit and a 30-second completion deadline. Event streams use the separate behavior described below.
Small HTTP responses can complete during another download, including responses without Content-Length.
If local response buffering reaches its memory limit, the tunnel returns 503 with Retry-After: 1.
If you receive this response, wait at least one second before a retry. Retry automatically only when the request is safe to repeat.
The relay refreshes account limits before their cached validity ends, so routine refreshes can run while requests continue.
Expired or invalidated account limits require a successful check before the relay forwards another request.
If the check finds that the plan of the account no longer includes tunnels, the relay closes the tunnels of the account. Visitors then get 404.
If the check fails, the relay returns 503 with Tunnel quota service unavailable. Retry only requests that are safe to repeat.
Server-sent events
Responses with Content-Type: text/event-stream reach visitors as the local service sends data.
The tunnel waits up to 30 seconds for response headers. An open event stream can continue beyond 30 seconds.
Slow readers limit forwarding, and a visitor disconnect closes the local request.
Tunnel revocation and the end of tunnel access still close active streams.
This behavior requires a relay that supports streamed responses.
LPM CLI forwards Last-Event-ID when an SSE client reconnects. Your local service must retain event history and resume after that ID.
The tunnel does not retain application events or guarantee exactly-once delivery.
Request capture and privacy
LPM CLI records forwarded requests and local responses in the project. Ordinary capture is best effort:
<project>/.lpm/inspector.dbThe database holds raw request and response headers and bodies, and session details. The tunnel service does not store these request bodies.
Protect the database as sensitive development data. .lpm/.gitignore holds *, so git ignores the database and everything else in .lpm. LPM CLI makes sure of this rule before it opens the database. It shows a warning if git tracks the capture files or does not ignore them. See Keep captures out of git.
On Unix, LPM CLI records captures only in a project whose path no other user can change, and stops otherwise. See Keep captures private.
Live capture memory has fixed limits. The request buffer retains up to 1,000 requests or 64 MiB.
The WebSocket buffer retains up to 5,000 events or 32 MiB.
Each captured WebSocket frame keeps a 64 KiB preview and records the original frame size. The inspector marks a partial preview as truncated.
For a completed event stream, the inspector retains up to 64 KiB of response data and marks a partial capture. An interrupted stream can have no completed capture.
The database writer queues up to 256 captures and 64 MiB. If either limit is full, LPM CLI drops new captures.
Session start and end records use a separate reliable path.
Slow browser streams can miss live events. The browser then reloads persisted history through the inspector API.
The browser inspector starts with a tunnel and selects an available local port. --no-inspect disables the browser inspector, but capture continues.
Use the same capture history from the terminal or browser:
lpm tunnel inspect --last 10
lpm tunnel inspect --ui
lpm tunnel replay --last --port 3000
lpm tunnel log --status 5xxReplay sends the saved method, path, headers, and body to the selected local service. This feature helps you repeat a failed webhook during development.
Session names group related captures:
lpm tunnel 3000 --session stripe-testlpm tunnel log --clear clears current capture history and completed sessions for the project, and overwrites the cleared captures in the database. It changes only the capture database.
Auto-acknowledge webhooks
To retain requests for replay after local forwarding fails, use --auto-ack:
lpm tunnel 3000 --auto-ackLPM CLI returns 200 OK only after the request reaches durable local capture storage. The tunnel process must remain open.
If storage fails, the capture queue is full, or persistence takes too long, the request receives 503 with Retry-After: 1. Forwarding overload also returns 503. The provider can then retry according to its delivery policy.
An acknowledgment does not trigger automatic replay:
lpm tunnel inspect --last 10
lpm tunnel replay --last --port 3000CAUTION: A forwarding failure leaves the application's processing outcome unknown. Before replay, check for existing side effects to avoid duplicate work.
Reliability and recovery
A missed relay heartbeat triggers reconnection. LPM CLI reports the retry instead of exiting successfully. The relay closes a tunnel after 90 seconds without a client heartbeat and releases its concurrent-tunnel slot. During that grace period, a suspected disconnect produces a temporary retry response.
An established tunnel retries a capacity rejection while its previous connection expires. A new tunnel still stops at the active-tunnel limit. The CLI makes up to 10 retries with backoff. A connection that lasts at least 60 seconds resets the retry count.
Sleep or a network change can close active HTTP, SSE, and WebSocket connections. Applications must reconnect their streams after tunnel recovery. A recovered tunnel keeps a claimed domain. An account hostname follows your current username, so use the URL from the latest successful connection.
Credential renewal keeps a healthy tunnel connected.
| Symptom | Cause | Recovery |
|---|---|---|
| The public URL stops | The LPM CLI process stopped | Start the tunnel again |
| The tunnel stops because the account cannot run it | The plan does not include tunnels, or the billing of the plan is inactive, for a Pro plan or an organization | Upgrade at lpm.dev pricing. For a Pro plan, update the payment method in billing settings. For an organization, name its claimed domain, or ask an owner to update its billing |
| A request cannot reach the local service | The service is stopped or uses another port | Start the service or select the correct port |
Bare lpm tunnel cannot select an endpoint | No active endpoint exists, or multiple endpoints exist | Pass the intended port |
| The inspector port is busy | An explicit --inspect-port is already in use | Select another port or omit the flag |
| Tunnel authentication fails | The saved login session is unavailable or expired | Run lpm login, then start the tunnel again |
| LPM CLI cannot verify the relay key | The key lacks valid approval, or the connection is unsafe | Update LPM CLI, then retry. If the error continues, contact the relay operator |
Relay certificate pins
LPM CLI stores one certificate pin for each relay host:
~/.lpm/relay-pins/relay.lpm.fyi
~/.lpm/relay-pins/<other-host>With LPM_HOME set, the pins are in $LPM_HOME/relay-pins/.
Normal TLS certificate and hostname checks apply before LPM CLI accepts a key.
For relay.lpm.fyi, each connection attempt requires a signed list of approved public keys, even when the saved pin matches.
LPM CLI verifies the signature, publisher, relay host, and validity window before it stores the pin.
If a saved key changes, LPM CLI connects automatically when the new key has valid approval.
Certificate renewal with the same public key does not require a new pin. Each connection still requires valid approval.
An old ~/.lpm/relay-pin file migrates automatically after successful verification.
Removing a key from the approval list blocks new connections with that key after LPM CLI receives the updated list. An earlier signed list remains valid until its expiration, for a maximum of seven days. Key removal does not close established tunnels.
Unknown keys, invalid certificates, and invalid or expired approval lists stop the command immediately. If the error asks for an update, run:
lpm self-update
lpm tunnel 3000If the error continues, contact the relay operator. Do not delete a saved pin to bypass verification. If the approval service is unavailable, retry the command when the network is available.
Other relay hosts retain per-host trust on first use. Automatic signed rotation applies only to relay.lpm.fyi.
Advanced relay configuration
The default relay URL is wss://relay.lpm.fyi/connect. Most users do not need to change it.
Use an environment variable for one process:
LPM_TUNNEL_RELAY=wss://relay.example.com/connect lpm tunnel 3000Use ~/.lpm/config.toml for a persistent override:
[tunnel]
relay-url = "wss://relay.example.com/connect"The precedence is LPM_TUNNEL_RELAY, ~/.lpm/config.toml, then the built-in default. An empty value moves to the next source.
An override accepts wss://, or ws:// on a loopback host. An invalid override warns and selects the built-in default directly.
Use only a trusted relay endpoint. The selected relay receives your session bearer token and the public traffic for your local service.
Each relay host has a separate certificate pin. A relay override does not reuse the pin for another host.
CI and long-running services
A tunnel needs an open process to keep the connection active. This model does not fit a typical CI job or permanent service.
For automated webhook tests, use provider delivery logs or a deployed test endpoint. Use LPM CLI tunnels for interactive development.
See also
lpm tunnel- command syntax, recipes, domain actions, capture commands, and flagslpm dev- start a project and its tunnel togetherlpm.jsontunnel configuration - configure a stable project domain- Authentication - understand the saved session requirement
lpm cert- use trusted HTTPS without a public tunnel