Start

serve documentation

serve is hosting and tunneling for AI agents. Learn how live tunnels and published hosting work, set up your agent or CLI, manage links and billing, and troubleshoot.

Hosting and tunneling for AI agents.

serve lets an agent choose live tunneling or published hosting for whatever the user is sharing. The user gets one public link (https://<subdomain>.servelink.cc); the agent creates it and manages the rest. Primary interface is MCP; a CLI is available for direct use.

Two ways to share

Every link is one account-owned URL whose content lives in exactly one of two residencies at a time:

AspectLive (residency=local)Published (residency=remote)
What it needsA local process and machine that stay availableStored bytes on serve's servers
Typical tasksDev-server previews, webhooks, OAuth/payment callbacks, WebSockets, teammate reviewStatic sites, generated reports and decks, file sharing that must outlive the session
Works while the machine is offNoYes
Teardownstop_tunnel / release_linkstop_remote (deletes the bytes)

The agent picks the mode from what the user asked for; serve does not switch modes automatically. Publishing can replace a live link on the same label, but a live tunnel never silently takes over a published link.

How to choose

  • Live when the thing being shared is a running process: a dev server, a WebSocket endpoint, a webhook or OAuth callback target. It reflects the machine's current state and goes quiet when the machine sleeps.
  • Published when the thing is finished: a static site, a generated report or deck, a file that must still be there tomorrow. Publish once; the link keeps working with the machine off.

What is unlimited, what is capped

Owned links are unlimited per account and are never recycled, trimmed, or evicted — not when another link is created, not when a plan changes.

What is capped per plan: simultaneous live tunnels (Free 3 / Pro 10), publish size, total storage, monthly live-tunnel transfer, and publish rate. The full table is on the billing and plans page.

Where to start

  1. Getting started — install, log in, first link on the free tier.
  2. Concepts — links, tunnels, ownership, and labels.
  3. Coding agents — set up MCP inside your agent.
  4. MCP tools and CLI reference — the full surface.
  5. Troubleshooting — when something doesn't work.

Next: Getting started.

Start

Getting started with serve

Install serve, log in with your browser, and create your first public link on the free tier — live tunnel or published hosting.

Hosting and tunneling for AI agents.

serve lets an agent choose live tunneling or published hosting for whatever the user is sharing. The user gets one public link; the agent creates it and manages the rest. This page gets you from nothing to that URL on the free tier.


1. Install

npm i -g @servelink/serve
# or, without a global install:
npx @servelink/serve

Alternate: brew install servelink-swyftlabs/tap/serve.

Check the release you got:

serve --version

2. Log in

serve login

Runs an OAuth 2.0 device flow: it opens your browser at the activation page with the code pre-filled, and prints a URL plus an 8-character XXXX-XXXX activation code as a fallback for SSH, headless, or --no-browser.

Sessions are per account, not per machine: one account works across machines.

3. Register MCP (primary interface)

A bare serve starts the MCP server over stdio. Register it in Claude Code, Cursor, opencode, or anything else that speaks MCP:

{
  "mcpServers": {
    "serve": {
      "command": "serve",
      "args": ["mcp"]
    }
  }
}

Ask the agent to serve a file, directory, or running local server. It returns a public HTTPS URL like https://lively-bison-4821.servelink.cc and chooses live or published from what you asked for. See Coding agents for per-agent setup notes.

4. CLI without an agent (optional)

Live tunnel to a local port:

serve 7000

Published hosting (machine can go offline afterward):

serve remote ./dist          # publish a build folder
serve remote stop <label>    # unpublish and delete the stored bytes

Stop a live tunnel with Ctrl-C.

Which mode

  • Live (tunnel): running process — dev server, WebSocket, webhook/OAuth callback target. Machine must stay available.
  • Published (hosted): static site, generated report/deck, file that must outlive the session. Bytes on serve until serve remote stop.

Same account-owned URL either way. Publishing can replace a live link on that label; a live tunnel never silently takes over a published link. serve does not switch modes automatically.


Free tier

  • Unlimited owned links; up to 3 simultaneous live tunnels. At the cap: tunnel_limit — nothing is recycled.
  • Publish up to 100 MB each (104,857,600 bytes), 1 GB total logical storage, 2 GB (2,000,000,000 bytes) live-tunnel transfer per UTC month.
  • Publish rate 30 / hour, 100 / day. Basic open-count analytics.
  • URLs are public to anyone who has them on Free (no viewer auth).

Pro

  • 10 live tunnels; 25 GB (25,000,000,000 bytes) live-tunnel transfer / month.
  • Publish up to 1 GB each (1,073,741,824 bytes); 50 GB storage; 300 / hour, 1,000 / day.
  • Password protection with custom TTLs; named subdomains; fuller analytics.
  • serve links, serve release <label>, serve whoami for inventory and budget.

See Billing and plans for the complete comparison.

Limitations

  • Outbound UDP 443 required. QUIC-only, no fallback. Check with serve doctor.
  • Live tunnels capped, links not. Free 3 / Pro 10 simultaneous tunnels.
  • Relay sees traffic after TLS termination. Do not tunnel production secrets.
  • Old clients cannot connect after transport upgrades. Match serve --version to the current release.

Support & abuse

  • Problems: serve@servelink.cc with serve doctor and serve --version.
  • Abuse: abuse@servelink.cc.

Next: Concepts — how links, tunnels, and labels fit together.

Start

Concepts: links, tunnels, and labels

How serve's model works: account-owned links, the two residencies (live tunnel vs published), label assignment and auto-suffixing, and what the caps model does and does not limit.

Every serve URL is one account-owned link. This page is the mental model behind everything else in the docs.

A link is the account-owned label and URL — lively-bison-4821.servelink.cc, say. Accounts may own an unlimited number of links by count, and no link is recycled, trimmed, quarantined, or deleted because another link is created or a plan changes.

A tunnel is the live transport that can carry a link right now. The term applies to live residency only: it is the secure outbound QUIC connection from your machine to the relay over UDP 443.

Because links and tunnels are separate, the counts diverge: links are unlimited, while simultaneous live tunnels are capped per plan (Free 3 / Pro 10). Stopping a tunnel never deletes its link — the label stays yours. Re-binding the same label closes only that link's previous tunnel.

Residency

A link's content lives in exactly one of two residencies at a time:

  • Local (`residency=local`) — live mode. A tunnel forwards public HTTPS/WSS traffic to a process on your machine. The machine must stay up.
  • Remote (`residency=remote`) — published mode. The artifact's bytes live in serve-managed storage and keep serving with the machine off.

The mode is chosen when you ask, per link, and serve never switches a link between them automatically.

Ownership and security

Ownership is cryptographic, not URL-based. Each device holds an Ed25519 identity key at ~/.serve/identity (PKCS#8 PEM, owner-only permissions) that signs every registration and tunnel bind. Knowing a URL is not enough to take the label: a different key can never take over your subdomain or evict a live tunnel.

See Security for the full trust picture.

Label assignment and suffixing

Labels come from three flows:

  1. Unnamed tunnels reuse the oldest stopped local assigned label the account already owns before generating a new one. Unnamed publishes always create a fresh generated label.
  2. Named requests (--name, name=) claim a specific subdomain (Pro). If the name is taken by another account, the API auto-suffixes it (up to 5 attempts of the form name-<hex>) and returns the result with suffixed_from naming the original request.
  3. If even suffixed acquisition cannot find a free label, the request fails with label_taken_suffixed and suggests a different name.

Labels are 3–32 characters of the allowed grammar, validated server-side.

The caps model

Caps limit concurrency, size, and rate — never ownership:

CappedFreePro
Simultaneous live tunnels310
Largest single publish (logical)100 MB (104,857,600 B)1 GB (1,073,741,824 B)
Total logical storage1 GB (1,073,741,824 B)50 GB (53,687,091,200 B)
Live-tunnel transfer / UTC month (decimal)2 GB (2,000,000,000 B)25 GB (25,000,000,000 B)
Publishes per hour / day30 / 100300 / 1,000

At a cap, nothing is deleted, recycled, or evicted. A new tunnel at the cap is refused with tunnel_limit; a publish over the size or rate cap is refused with its own error code. Your existing links keep working.

Two important asymmetries:

  • Publishing traffic does not consume the account transfer allowance; only live-tunnel payload does.
  • Transfer caps are decimal (2 GB = 2,000,000,000 bytes); size and storage caps are powers of 1024.

Next: Coding agents — setting up MCP inside your agent.

Use

Coding agents

Set serve up inside Claude Code, Cursor, opencode, and other MCP clients: the canonical registration config, what the agent decides for you, and when to use MCP instead of the CLI.

serve's primary interface is MCP: your coding agent calls the tools directly, so you never parse CLI output or copy commands. A bare serve runs the MCP server over stdio; serve mcp is the explicit form.

The canonical registration

Register serve in any MCP-capable client with this exact block:

{
  "mcpServers": {
    "serve": {
      "command": "serve",
      "args": ["mcp"]
    }
  }
}

Works with Claude Code, Cursor, opencode, and anything else that speaks MCP. A logged-in install needs no arguments: the server resolves its relay the same way the CLI does (flags, then environment, then ~/.serve.toml, then the hosted default).

What the agent decides

The agent maps your request to live or published and returns the URL:

"Serve me this so I can check it on my phone."
"Put the deck somewhere my client can open it, and password-protect it."

Your coding agent will:

  • pick live for a running process (preview, webhook, WebSocket) and published for a finished artifact (site, deck, file), and tell you which it chose;
  • refuse to start its own static server — the tools host files themselves;
  • return the public URL, the link label, and the residency, plus artifact version/size/kind for publishes;
  • branch on structured error codes when a call is refused instead of guessing from prose.

Hosted connector (no local machine)

serve also runs a hosted MCP connector (OAuth, Streamable HTTP) at https://mcp.servelink.cc/mcp with a deliberately smaller subset: 8 tools, inline content only. There is no local machine in that process, so it has no ports, tunnels, or diagnostics — it publishes bytes and manages the links they land on.

Use the local stdio server when the task involves anything on your machine (live tunnels, serving a local path); use the hosted connector when you are an agent without access to the user's machine and only need to publish content and manage links.

MCP vs CLI

SituationSurface
Inside a coding agent, one-shot serve/share/publishMCP tools
Scripting, SSH sessions, humansCLI (serve <port>, serve remote)
Inventory, quota, plan checksEither (links / account_status, serve whoami)

Everything the CLI does is also available over MCP; the docs reference the MCP tools and the CLI side by side.

Next: MCP tools — all 25 tools and the error codes.

Use

MCP tools

The complete MCP tool reference: 17 local stdio tools, the 8 hosted-connector tools, their arguments and defaults, and the 18 machine-branchable error codes.

Hosting and tunneling for AI agents. MCP is the primary interface: a bare serve runs an MCP server over stdio so agents create and manage links without shelling out or parsing CLI output. (serve mcp is the explicit form.)

The agent chooses live tunneling or published hosting from what the user asked for; the user gets one public link. serve does not switch modes automatically.

Registering it

{
  "mcpServers": {
    "serve": {
      "command": "serve",
      "args": ["mcp"]
    }
  }
}

Works with Claude Code, Cursor, opencode, and anything else that speaks MCP. A logged-in install needs no arguments: the server resolves its relay the same way the CLI does (flags, then environment, then ~/.serve.toml, then the hosted default).

Local stdio tools (17)

All 17 tools return structured JSON. Create/stop/release/status results embed a usage snapshot (live-tunnel capacity and storage/transfer/publish usage), and refused calls carry machine-branchable error codes; an agent should always be able to answer "what do I own, what's running, and what am I still allowed to do" without a second round trip or any string parsing.

ToolPurposeKey arguments
servePublish a local resource: pass path (auto-hosted) or port (running server)path / port, name, protect, ttl, residency, replace
serve_filePath-only convenience for the file/directory casepath (required), name, protect, ttl, residency, replace
stop_remoteUnpublish a remote link, delete its stored byteslabel (required)
  • serve(port|path, name?, protect?, ttl?, residency?, replace?): create a link from a running dev-server port or a local path. Called without name, an unnamed tunnel reuses the oldest stopped local assigned label, while an unnamed publish always creates a fresh label. name= claims a Pro named subdomain. residency="remote" publishes the bytes to serve's servers instead of tunneling. protect=true turns on Pro protection, with ttl between 1h and 30d (defaulting to 24h). replace=true resolves a residency conflict. Returns the public URL once the link is live, with the link's label and residency, plus version, size_bytes, and kind when the result is a publish.
  • serve_file(path, name?, protect?, ttl?, residency?, replace?): intent-specific, path-only convenience for the file/directory case. It takes no port argument; the optional arguments it shares with serve behave the same way.
  • stop_remote(label): unpublish a remote link, delete its stored bytes, and return the link to local residency. It never starts a tunnel.

Inspecting

ToolPurposeKey arguments
list_tunnelsWhat's live right now
tunnel_statusThe state of one linkport or label
linksEvery owned link, one record each
account_statusPlan, entitlement, and usage
  • list_tunnels: all active tunnels with ports, labels, public URLs, uptime, state, and last error.
  • tunnel_status(port or label): the state of one tunnel; list_tunnels covers every active tunnel.
  • links: every link the account owns — kind, URL, live/stale markers, protection status, last bound/viewer activity, and quarantine window — with live-tunnel capacity and storage/transfer/publish usage as context.
  • account_status: plan, entitlement, and how much of the storage, transfer, and publish allowance is used.

Tearing down

ToolPurpose
stop_tunnelStop one live tunnel (by port or label); idempotent
stop_all_tunnelsStop every active tunnel, revoking all public URLs
release_linkGive up a link you no longer want
  • stop_tunnel(port or label), stop_all_tunnels: close live tunnels. To change a live tunnel's port or label, stop it and call serve again with the new settings; there is no in-place reconfiguration.
  • release_link(label): give up a link you no longer want. Fails with remote_link while a published artifact is still on the label; unpublish first with stop_remote.

Protecting

ToolPurpose
protect_linkEnable Pro protection; returns share URL + readable secret
unprotect_linkClear protection, return to public access
renew_linkRotate credentials; previous ones stop working
revoke_linkInvalidate credentials now; stays protected
update_ttlChange duration without rotating credentials
create_viewer_grantTwo-minute owner-browser bypass URL
  • protect_link(label?, ttl?): Pro-only: enable protection on an owned link and return its share URL (with #cap=...) plus the readable secret code. With no label it protects the first owned link returned by links; with no ttl protection lasts 24h. Calling it again on the same link replaces the credentials, so previous share URLs stop working.
  • unprotect_link(label): clear protection and return the link to public access.
  • renew_link(label, ttl?): Pro-only: rotate the link's credentials. The previous share URL and secret code stop working immediately and the countdown restarts; an omitted ttl defaults to 24h.
  • revoke_link(label): cut off existing viewers immediately: the active credentials are invalidated, the link stays protected, and no new credentials are minted.
  • update_ttl(label, ttl): Pro-only: change how long protection lasts without rotating credentials. The link must already be protected with an unexpired credential; the expiration is recalculated from the new duration rather than extended.
  • create_viewer_grant(label): Pro-only: mint a single-use owner-browser bypass grant that lasts two minutes and return its bypass URL. It opens a protected link in the owner's browser without sharing the link's viewer secret; it is not a credential for handing to viewers.

Diagnosing

  • doctor: self-diagnostics, same checks as the CLI: identity fingerprint presence, account API health, relay health, and UDP 443 reachability. Best-effort probes that need no login; a UDP probe that sends but gets no reply is inconclusive, not proof of failure.

Hosted connector tools (8)

serve also runs a hosted MCP connector (OAuth, Streamable HTTP) at https://mcp.servelink.cc/mcp with a deliberately smaller remote-publishing subset: 8 tools, inline content only. There is no local machine in that process, so it has no ports, tunnels, or diagnostics; it publishes bytes and manages the links they land on. Authentication is OAuth 2.1 with the console's handoff flow.

ToolPurpose
servePublish inline content (text) or content_base64 (binary) plus filename
linksOwned-link inventory with usage context
stop_remoteUnpublish a remote link
protect_link / unprotect_linkProtection on/off (Pro)
renew_link / revoke_linkRotate / invalidate credentials
account_statusPlan, entitlement, usage

Key differences from the local server:

  • serve takes inline content, never a local path or port.
  • The viewer secret is never returned here: a hosted connector authenticates with a Supabase session (not the device surface), so the account API never hands a hosted call the readable secret.
  • protect_link requires label (no first-owned-link default).
  • No update_ttl, create_viewer_grant, or doctor.

Error codes (18)

Every refused call returns a structured envelope:

{
  "ok": false,
  "error": {
    "code": "tunnel_limit",
    "message": "...",
    "hint": "...",
    "state": {}
  }
}
CodeMeaning
tunnel_limitAt the plan's simultaneous live-tunnel cap (Free 3 / Pro 10)
unknown_subdomainRequested subdomain does not exist or is not yours
label_taken_suffixedEven auto-suffixed acquisition found no free label
pro_requiredThe operation needs Pro (protection, named subdomain)
udp_blockedOutbound UDP 443 (QUIC) is blocked on this network
unauthorizedToken missing, expired, or revoked
relay_unreachableThe relay edge is unreachable
internal_errorSomething failed internally; retry
port_in_useThe local port is already tunnelled
ambiguous_targetAmbiguous arguments — exactly one of port or path must be passed
residency_unsupportedThe requested residency value is not supported for this operation
residency_conflictLabel is publishing; pass replace=true or unpublish first
size_cap_exceededPublish exceeds the per-deployment size cap
publish_rate_limitedPublish budget for the hour/day is exhausted
storage_cap_exceededAccount's total storage cap is reached
transfer_limitMonthly live-tunnel transfer allowance reached
relay_egress_limitGlobal relay egress limit for the month reached (503)
not_remoteOperation targets a label that is not remote-resident

Per-code recovery steps are in Troubleshooting.

Next: CLI reference — the same surface, driven by hand.

Use

serve CLI reference

The complete CLI reference — all named subcommands, every advertised flag, config precedence, and the account, link, and publish commands.

The CLI is the manual-drive surface. Everything here is also available to your agent over MCP — see MCP tools — and most days you'll use that instead.

A bare serve with no arguments runs the MCP server over stdio. Every command below is an explicit override of that default.

The command surface (13 named commands)

serve is hand-dispatched on the first argument; there are 13 named entry points. The usage banner names only four of them — this page is the discovery surface for the rest:

usage: serve [flags] <port>
       serve login [flags]
       serve mcp [flags]
       serve link [list|protect|unprotect|renew|revoke|update-ttl|open]
CommandPurpose
serveStdio MCP server (primary interface)
serve mcpExplicit MCP form
serve <port>Live tunnel to localhost:<port>
serve loginOAuth 2.0 device authorization
serve whoamiAccount, plan, entitlement, usage
serve logoutClears the local token only
serve doctorConfig, identity, health, UDP probe
serve linksOwned-link table
serve link <sub>list / protect / unprotect / renew / revoke / update-ttl / open
serve release <label>Releases a label
serve protect / unprotect / renew / revokeTop-level protection aliases
serve remote <path>Publish a file/dir
serve remote stop <label>Unpublish (deletes stored bytes)

Tunnel flags (what --help prints)

serve <port> accepts these flags:

FlagMeaning
--name <subdomain>Request a specific subdomain (Pro); default random
--protectProtect the tunnel link with capability token + readable secret
--ttl <duration>Protection duration, e.g. 24h, 3d, 7d (1h–30d)
--openOpen the browser with an authenticated owner bypass link
--jsonPrint machine-readable JSON instead of a human line
--replaceReplace a published (remote-resident) link; the artifact is deleted
--versionPrint version and exit
--api-token <token>Account API bearer token (or SERVE_API_TOKEN)
--account-url <url>Account API base URL (or SERVE_ACCOUNT_URL)
--identity <path>Path to the client identity key (or SERVE_IDENTITY); default ~/.serve/identity
flags:
  -account-url string
    account API base URL (or SERVE_ACCOUNT_URL)
  -api-token string
    account API bearer token (or SERVE_API_TOKEN)
  -identity string
    path to the client identity key (or SERVE_IDENTITY); default ~/.serve/identity
  -json
    print machine-readable JSON instead of a human line
  -name string
    request a specific subdomain (default: random)
  -open
    open the browser with an authenticated owner bypass link
  -protect
    protect the tunnel link with capability token and readable secret
  -replace
    replace a published (remote-resident) link without prompting; the artifact is deleted
  -ttl string
    optional duration for the protected link (e.g. 24h, 3d, 7d)
  -version
    print version and exit

Authorizing a device

serve login                 # opens your browser at the activation page
serve login --no-browser    # prints a URL + XXXX-XXXX code instead (e.g. over SSH)
serve logout                # clears credentials from ~/.serve.toml

serve login runs an OAuth 2.0 device flow. Your CLI posts a device public key, you confirm the code, and the relay will only accept registrations from a key bound to your account.

Serving from your machine

serve <port> creates a live public link to a local HTTP/WebSocket service. The transport is a tunnel, so the link reflects the machine's current state and goes quiet when the machine does. For a finished artifact that must outlive the session, use publishing instead.

serve 3000                     # tunnel localhost:3000
serve 3000 --json              # machine-readable output
serve 3000 --name myapp        # request a specific subdomain (Pro)
serve 3000 --protect --ttl 3d  # password-protect the link (1h–30d, Pro)
serve 3000 --open              # open a browser with an owner bypass grant

Publishing to serve's servers

Published links keep working with your machine off. Publishing is explicit: serve never fails a live link over to a published copy, and a live tunnel never takes a published link back without the explicit replace path.

serve remote ./dist              # publish a build folder
serve remote ./dist --replace    # republish over an existing link (Pro)
serve remote stop <label>        # unpublish and delete the stored bytes

A publish prints the public URL and the link label; with --json it also returns version, size_bytes, and kind. serve remote stop deletes the stored bytes and returns the link to local residency; it does not start a tunnel. Releasing a remote link fails until it is unpublished first.

serve whoami                      # plan, budget, and every active URL
serve links                       # list owned links (live / stale / remote)
serve release <label>             # free a link you're not using
serve protect <label> --ttl 24h   # turn on password protection (Pro)
serve unprotect <label>           # clear protection
serve renew <label>               # rotate protection credentials
serve revoke <label>              # revoke viewer credentials immediately
serve doctor                      # self-diagnostics: account, relay, UDP 443

serve whoami prints the email, plan, entitlement state, active URLs, live tunnels, logical storage usage, publish rate, and routing version.

serve link (and the top-level aliases) handle per-link protection:

SubcommandPurpose
serve link listList links with protection state
serve link protect <label>Enable protection (Pro)
serve link unprotect <label>Clear protection
serve link renew <label>Rotate credentials
serve link revoke <label>Invalidate credentials
serve link update-ttl <label>Change protection duration
serve link open <label>Open an owner bypass link

Configuration

Resolution order is flags, then environment variables, then the config file, then the hosted defaults.

FlagEnvironment variable~/.serve.toml
--account-urlSERVE_ACCOUNT_URLaccount_url:
--api-tokenSERVE_API_TOKENapi_token:
--identitySERVE_IDENTITYidentity:

Other environment variables: SERVE_RELAY, SERVE_NAME, SERVE_AUTH_TOKEN, SERVE_CONFIG.

serve login writes ~/.serve.toml for you, so you rarely touch it by hand:

api_token: sv1_...
relay: https://account-api.servelink.cc
account_url: https://account-api.servelink.cc

On first run the client also creates an Ed25519 identity key at ~/.serve/identity (PKCS#8 PEM, owner-only permissions). That key binds this device to your account and signs every register request and QUIC bind handshake. A different key can never take over your subdomain or evict a live tunnel.

When something doesn't work

See Troubleshooting. serve doctor covers the common causes on its own, including whether your network lets QUIC out over UDP 443.

Next: Live tunnels — the live residency in depth.

Modes

Live tunnels

Live residency in depth: the tunnel lifecycle, the QUIC transport over UDP 443, how Origin and WebSockets pass through, and how live-tunnel transfer is metered.

Live mode (residency=local) forwards a public HTTPS/WSS URL to a process on your machine through a secure outbound tunnel. The link reflects the machine's current state and goes quiet when the machine does.

The lifecycle

  1. You (or your agent) call serve <port> / serve(port=...) with a running local HTTP/WebSocket service.
  2. The client creates or reuses a link label and registers a live tunnel with the relay.
  3. The tunnel stays up while the local process and machine are available; the relay terminates viewer HTTPS/WSS traffic and forwards it over the tunnel.
  4. Teardown: Ctrl-C, stop_tunnel, stop_all_tunnels, or the machine going away. The link survives teardown; only the tunnel is gone.

Stopping a tunnel never deletes its link. Re-binding the same label closes only that link's previous tunnel.

Only http://127.0.0.1:<port> can be tunnelled — no IPv6 localhost, LAN host, HTTPS origin, Unix socket, path prefix, or host rewrite.

The transport: QUIC over UDP 443

Live tunnels bind over QUIC with ALPN serve-tunnel/2 and TLS 1.3. Viewer HTTP/WebSocket traffic still arrives at the relay's HTTP port and is forwarded over QUIC streams to your machine.

  • UDP 443 outbound is required. There is no TCP fallback and no HTTP/WS upgrade path. If your network filters UDP 443, live tunnels cannot work here; serve doctor detects this.
  • In production the data plane listens at tunnel.servelink.cc:443 (a DNS-only UDP record; Cloudflare does not proxy it).
  • The account API and tunnel-registration host is account-api.servelink.cc. serve.servelink.cc is marketing-only and is never a routing host.
  • Each bind is cryptographically signed by the device's Ed25519 identity key, so knowing a URL is not enough to take over the label.

Viewer traffic pass-through

serve forwards viewer traffic as it is, with two deliberate exceptions:

HeaderBehaviour
OriginPreserved and forwarded to localhost — never rewritten
Cookie, Authorization, Sec-Fetch-*, Sec-WebSocket-ProtocolPreserved
Hop-by-hop headers (connection, upgrade, host, …)Stripped before forwarding
cf-connecting-ip, x-forwarded-for, x-real-ipNot trusted from viewers; x-real-ip overwritten server-side

Because Origin is preserved, origin-protected dev frameworks must be told to trust your public hostname — see Preview modes.

WebSocket lifecycle

serve upgrades a viewer's WebSocket only after your local server accepts it — a browser is never told "connected" before localhost agrees. The pre-101 status code is a precise boundary signal (full table on the Preview modes page):

Before any 101Meaning
401Protected link; credential missing or invalid
403Relay rejected the viewer's Origin
404No live tunnel for that hostname
502Your local server refused the WebSocket
503Relay busy, or an account/global transfer limit reached
504Local WebSocket open timed out

Transfer accounting

Live-tunnel payload transfer is metered per account per UTC month, and its caps are decimal — Free 2 GB (2,000,000,000 bytes), Pro 25 GB (25,000,000,000 bytes):

  • When the account's allowance is reached, new traffic is denied with transfer_limit and the error carries the exact reset time; the limit resets at the start of the UTC month.
  • Publishing traffic does not consume the transfer allowance — only live-tunnel payload does.
  • The service also enforces a global relay egress limit for the month; when the whole fleet hits it, requests get relay_egress_limit with a 503 and the same reset time.
  • Nothing is deleted when a limit is reached — links stay yours and resume serving next month.

Concurrent-tunnel cap

Simultaneous live tunnels are capped per plan (Free 3 / Pro 10). At the cap a new tunnel is refused with tunnel_limit — nothing is recycled or evicted to make room. Stop one you don't need, or serve release <label> a stale link.

Next: Published hosting — the other residency.

Modes

Published hosting

Publish files and directories to serve's storage so the link keeps working with your machine off: publishing, replacing, size and storage caps, directory index pages, and unpublishing.

Published mode (residency=remote) uploads a file, directory, or inline artifact to serve-managed storage and serves it from there. The source machine can go offline after the artifact is committed — the link keeps working.

Publishing

serve remote ./dist              # publish a build folder
serve remote ./dist --replace    # republish over an existing link (Pro)
serve remote stop <label>        # unpublish and delete the stored bytes

Or over MCP: serve(path=..., residency="remote") / the hosted connector's serve(content=..., filename=...).

A publish returns the public URL, the link label, the residency, and the artifact's version, size_bytes, and kind. kind is file for a single file and dir for a directory (a zip of entries, possibly with an index.html).

Directory index pages

A published directory serves its tree with the standard index: an index.html at the root is the entry page, and other files are reachable by their paths. A published single file serves exactly that file.

Sizes, storage, and publishing rate

LimitFreePro
Largest single publish (logical, uncompressed)100 MB (104,857,600 B)1 GB (1,073,741,824 B)
Total logical storage across all artifacts1 GB (1,073,741,824 B)50 GB (53,687,091,200 B)
Publish attempts per hour / day30 / 100300 / 1,000

These deploy and storage caps use powers of 1024 (unlike transfer, which is decimal). Multipart uploads use fixed 64 MiB parts with a hard ceiling of 10,000 parts, a 24-hour upload lifetime, and 1-hour presigned part URLs.

Every initialized publish attempt counts against the hourly/daily budget — including republish and failed or aborted attempts.

Replacing a published artifact

  • Publishing can replace a live link on the same label: a remote publish to a live-tunnel label closes that tunnel binding and takes the label.
  • Replacing an existing published artifact at the same name requires replace=true (Pro) — otherwise the API returns residency_conflict and you must resolve it with the user first.
  • A live tunnel never silently takes over a published link.

Unpublishing and releasing

  • serve remote stop <label> (or MCP stop_remote) deletes the stored artifact bytes and returns the link to local residency. It does not start a tunnel.
  • release_link on a remote-resident link fails with remote_link until the artifact is unpublished first.

Traffic and analytics

  • Published serving does not consume the account's live-tunnel transfer allowance.
  • Analytics: Free accounts get daily opens plus the range total; Pro accounts additionally get delivered bytes, top referrer hosts, and country counts.

Next: Preview modes — dev preview vs production preview.

Modes

Preview modes

The two ways to run the thing behind a live link — production preview (the default) and dev preview with HMR — plus how to read WebSocket failures on a tunnel.

When you're tunneling a dev server, there are two ways to run the thing behind the link, and they are not interchangeable. Pick per task.

Everything on this page is about live links (residency=local): the link forwards to a process on your machine, which must stay up. If the artifact is done and the link has to outlive the session, publish it instead (serve remote) so it keeps serving with the machine off; see the CLI reference for the publish commands. Publishing and previewing are different jobs for different moments, not settings on one link.

Mode A — production preview (the default, and what review needs)

Use this for anything a human is going to judge: charts, hydration-sensitive UI, stakeholder demos, and any claim that the link represents a release build. serve is a tunnel, so a production server speaks release-like over the link exactly as it would in CI.

npm run build
npm run start -- --port <port>
serve <port> --json

Hot module reload is not needed here. And a page that returns HTTP 200 is not yet a pass — confirm the visual smoke holds: nothing stuck on a loading placeholder, expected output mounted, no hydration errors in the console.

If charts stay in a loading state in production mode, the defect is in the application. A tunnel change won't fix it.

Mode B — development preview with HMR (opt in)

Use this only when you genuinely need live source updates to reach the public link.

npm run dev -- --port <port>
serve <port> --json

serve preserves the browser's Origin and never rewrites it, so an origin-protected dev framework has to be told to trust your public hostname. For Next.js, add the exact hostname and restart:

// next.config.ts
const nextConfig: NextConfig = {
  allowedDevOrigins: ["lively-bison-4821.servelink.cc"],
};

Three things trip people up here:

  • allowedDevOrigins wants a hostname, not a URL with a scheme.
  • It has to match your actual public serve address exactly.
  • There is no flag, environment variable, or config setting that bypasses the framework's origin check, by design. If the framework rejects the origin, configure the framework.

Other frameworks have their own equivalent dev-origin policy.

Reading a WebSocket failure

serve upgrades a viewer's WebSocket only after your local server accepts it, so a browser is never told "connected" before localhost agrees. That makes the status code a viewer sees before any 101 a precise signal about which boundary failed.

Before any 101What it means
401The link is protected and the cookie or capability token is missing or invalid
403The relay rejected the viewer's Origin
404No live tunnel for that hostname
502Your local server refused the WebSocket — a rejection or bad negotiation
503The relay is busy, or an account or global transfer limit was reached
504The local WebSocket open timed out
101, stableBoth handshakes accepted; you're through

A 502 before any 101 is almost always the local application refusing the connection — check its allowed-origin and dev-resource policy rather than serve. A 101 immediately followed by a close is a regression signal worth reporting.

Next: Console — managing everything from the dashboard.

Account

Console (dashboard)

A walkthrough of the serve console at console.servelink.cc: the links list, filters, protection drawer, branded links, devices, activation, and billing.

The console lives at console.servelink.cc — a web app that manages your links, devices, and billing without the CLI. It is served by a Cloudflare Worker and reads the same account API the CLI and MCP tools use.

Signing in

Sign up or log in with email. There is no password-reset flow and no social/SSO provider: signup goes through the email confirmation page, /auth/confirm.

The sidebar

The sidebar groups the console into Endpoints, Insights, and Configuration:

SectionItemPurpose
EndpointsAll LinksEvery owned link, filterable
EndpointsLive tunnelsLinks currently tunnelling
EndpointsPublished artifactsRemote-resident links
EndpointsBranded LinksPro named subdomains (Pro chip when not paid)
EndpointsDomainsComing soon — not available
InsightsAnalyticsComing soon — not available
ConfigurationMCP ServerCopy the canonical MCP config
ConfigurationDevicesDevice tokens, last-used, revoke
ConfigurationActivateDevice activation for serve login
ConfigurationBillingPlan, subscription, invoices

Two sidebar entries — Domains and Analytics — are disabled "Coming soon" items. Do not treat them as available features.

All Links is the default view (also route /). Links are filterable by status (all / online / offline), protection (all / protected / unprotected), type (local / remote), and brand (branded vs not), and sortable by created, alpha, last bound, or last visited.

Each row shows the link's kind, URL, live state, residency, protection state, and last-viewer activity.

Protection drawer

Opening a link's protection drawer shows the folded protection state — protected, the credential state (active / expired / revoked / none), the TTL in hours, and the expiry — and offers Enable / Disable for protection.

The console offers four TTL presets — 24h, 3d, 7d, 30d — while the CLI and MCP accept the full 1h720h (30-day) range.

Branded Links lists your Pro named subdomains. While a subscription is in grace, existing branded claims stay live but new branded names cannot be claimed until billing is fixed.

Devices

Devices lists every authorized device token with its label, token prefix, and last-used time, and lets you revoke a device. Revoking stops new binds; live tunnels drop as revocation reaches the relay. Each device holds its own Ed25519 identity key, so revocation is per device, not global.

Activate

Activate is the destination of serve login's browser flow: it shows the activation code and confirms the device. The CLI posts a device public key, you confirm the code here, and the new device is bound to your account.

Billing

Billing shows your plan, entitlement state, subscription status, current period end, last successful billing sync, and grace period end. From here you upgrade via Polar checkout and manage the subscription via the Polar customer portal.

Next: Billing and plans — the caps table and what happens at each limit.

Account

Billing and plans

Free vs Pro: every cap with exact numbers, how entitlement states and grace periods work, Polar as the merchant of record, invoices, and what happens when a cap denies an action.

serve has two plans. Polar.sh is the merchant of record: you pay Polar, Polar notifies serve, and serve grants Pro entitlement.

The plans

FeatureFreePro
Price$0$9.99/month
Owned linksUnlimitedUnlimited
Concurrent live tunnels310
Largest single publish100 MB (104,857,600 B)1 GB (1,073,741,824 B)
Total logical storage1 GB (1,073,741,824 B)50 GB (53,687,091,200 B)
Live-tunnel transfer / UTC month (decimal)2 GB (2,000,000,000 B)25 GB (25,000,000,000 B)
Publishes per hour / day30 / 100300 / 1,000
Password protectionAny link, TTL 1h–30d
Named subdomainYes
AnalyticsOpens (daily + total)Opens, delivered bytes, referrer, country

Size and storage caps are powers of 1024; transfer caps are decimal bytes. Multipart uploads use fixed 64 MiB parts, max 10,000, a 24-hour upload lifetime, and 1-hour presigned part URLs.

What is unlimited

Owned links are unlimited per account and are never recycled, trimmed, or evicted — not when another link is created, not when a plan changes, and never across accounts.

Entitlement states

Plan and entitlement are two separate axes. The console labels them: unlinked causes a "Free plan" label, active a "Pro plan" label, grace a "Grace period" label, reauth_required a "Reauth required" label, and inactive an "Inactive" label.

The state machine:

StateWhat it means
activePro, entitlement confirmed · (or Free with no subscription)
gracePro payment definitively inactive → 72-hour grace; you keep Pro limits
reauth_requiredPolar needs reauthorization → 7-day reauth grace
inactiveGrace expired (or never subscribed) → back to Free

Upgrading and managing

  • Upgrade/billing in the console calls POST /v1/billing/checkout, which creates a Polar checkout session and returns the hosted checkout URL.
  • ManagePOST /v1/billing/portal opens the Polar customer portal (subscription, payment method, invoices). An account with no active subscription gets not_subscribed.
  • A transient Polar outage fails the call with polar_unavailable rather than changing your entitlement.

Invoices

Polar is the merchant of record, so invoices, receipts, and payment methods are managed in the Polar customer portal, not in the serve console.

Cap-denial behaviour, by error code

WhenErrorWhat to do
4th/11th simultaneous tunneltunnel_limitStop a tunnel or release a stale link
Publish over the size capsize_cap_exceededReduce the artifact or upgrade
Total storage fullstorage_cap_exceededUnpublish something or upgrade
Publish budget exhaustedpublish_rate_limitedWait for the hour/day reset (every attempt counts, including republish)
Monthly transfer reachedtransfer_limitWait for the UTC-month reset; the error carries the exact time
Protection without Propro_requiredUpgrade at /billing

Next: Security — identity, signing, and the trust boundary.

Account

Security

How serve secures links: device-bound Ed25519 identities, signed registrations, revocation, password protection and TTLs, and the TLS-terminates-at-relay trust caveat.

serve's security model is built around cryptographic ownership: subdomain routing and reservations are secured by client Ed25519 keypairs and canonical message signing.

Device identity and binding

On first run the client creates an Ed25519 identity key at ~/.serve/identity, persisted as a PKCS#8 PEM file with owner-only permissions. That key is the device's identity.

  • The key signs every registration request and every QUIC bind handshake. The bind proof is a canonical signature over the subdomain, nonce, and transport parameters.
  • Knowing a URL is not enough to take the label. A different key can never take over your subdomain or evict a live tunnel.
  • You get one identity per machine per login; serve login binds the device's public key to your account through the OAuth 2.0 device flow.

Authorization and revocation

API calls carry an account API token whose hash is stored server-side (devices resolve by token hash, never by raw token). Devices are listed in the console (Devices), each with a label, token prefix, and last-used time.

  • Revoking a device from the console stops new binds; live tunnels drop as revocation reaches the relay.
  • serve logout only clears the token from the local machine — it does not revoke the device. To revoke, use the dashboard.

Password protection (Pro)

Protection gates a whole link with a capability token embedded in a share URL (https://<label>.servelink.cc/#cap=...) plus an optional human-readable secret code.

OperationEffect
protect_linkEnables protection; replaces existing credentials if already protected
unprotect_linkRemoves protection; link returns to public access
renew_linkRotates credentials; old share URLs stop working
revoke_linkInvalidates credentials now; link stays protected, no new credentials minted
update_ttlChanges the duration without rotating credentials

TTLs (X6 note)

The TTL surfaces are deliberately different:

SurfaceAccepted TTLs
CLI / MCPFull range: any duration from 1h to 720h (30 days) — 24h, 3d, 7d, 1w all parse
ConsoleFour presets: 24h, 3d, 7d, 30d

An omitted TTL defaults to 24h. An omitted label on protect_link uses the first owned link returned by links (local server only).

Where the secret lives

The readable secret and raw capability token are returned once, to the calling device (CLI or local MCP) at protect/renew time. The console never receives them — it gets only the stamped share URL.

It is never returned by the hosted MCP connector either (that surface authenticates with a Supabase session, not a device).

The trust boundary: TLS terminates at the relay

Viewer traffic is HTTPS/WSS to the relay, then forwarded over the tunnel. The relay operator can read plaintext tunneled application data after TLS termination. This is an explicit trust trade-off, not end-to-end encryption to localhost.

What is not implemented

  • No per-tunnel IP allowlist, request inspection, or content-level access policy beyond the whole-link gate.
  • An unprotected link is public to anyone who knows the URL.
  • No password-reset flow or social/SSO in the console.

Next: Troubleshooting — errors, serve doctor, recovery.

Help

Troubleshooting

Error-by-error recovery for serve: every MCP error code with what it means and what to do, plus serve doctor, accounts, publishing, WebSockets, network, and limits.

Start with serve doctor. It checks your account, the relay, and whether your network lets QUIC out over UDP 443, which is the single most common blocker.

The 18 error codes, at a glance

Every refused call returns an envelope with a machine-readable code. Agents should branch on code, not prose. Recovery per code:

CodeMeaningWhat to do
tunnel_limitAt the concurrent live-tunnel cap (Free 3 / Pro 10)Stop a tunnel or serve release <label> a stale link; nothing is evicted
unknown_subdomainSubdomain does not exist or is not yoursCheck the label with links / serve links; request an existing owned label
label_taken_suffixedEven auto-suffixing found no free labelTry a different name
pro_requiredOperation needs ProUpgrade at /billing in the console, or via POST /v1/billing/checkout
udp_blockedOutbound UDP 443 (QUIC) is blockedFix the network/firewall; run serve doctor to confirm
unauthorizedToken missing, expired, or revokedserve login again; check the Devices list in the console
relay_unreachableRelay edge unreachableCheck connectivity; retry; serve doctor
internal_errorInternal failureRetry; if persistent, report to serve@servelink.cc
port_in_useLocal port already tunnelledStop the existing tunnel on that port first
ambiguous_targetAmbiguous argumentsPass exactly one of port (a running server) or path (a local file/dir)
residency_unsupportedThe residency value is not supported for this operationOmit residency / pass the supported value, or use the residency-appropriate tool
residency_conflictLabel is publishing an artifactPass replace=true (Pro) or unpublish first (stop_remote)
size_cap_exceededPublish exceeds the per-deployment cap (Free 100 MB / Pro 1 GB)Reduce the artifact or upgrade
publish_rate_limitedHourly/daily publish budget exhaustedWait for the reset; every attempt counts, including republish
storage_cap_exceededTotal storage cap reached (Free 1 GB / Pro 50 GB)Unpublish something or upgrade
transfer_limitMonthly live-tunnel transfer reached (Free 2 GB / Pro 25 GB)Wait for the UTC-month reset; the error carries the exact reset time
relay_egress_limitGlobal relay egress limit for the month reachedRetry after reset; nothing of yours is deleted
not_remoteTarget label is not remote-residentCheck residency with links / serve links

Account and login

`unauthorized` / `401` Your api_token is missing, expired, or revoked. Run serve login again, or check the device list in the dashboard. serve whoami will tell you which of "not logged in", "token revoked", and "service unreachable" you're hitting.

"No relay configured" A logged-in install always has one. Run serve login to write the relay binding, or set SERVE_RELAY.

The URL returns 502 The relay can't reach your local server. Confirm it's actually listening on the port you passed — curl http://localhost:3000 should work locally first.

The URL returns 404 Nothing is bound to that subdomain. Either it never connected, or it dropped and hasn't re-registered. Restart serve.

The page loads but content stays on `Loading…` If this happens in production preview, it's an application defect and no tunnel or HMR change will fix it. See Preview modes.

These are different failures from live-tunnel problems: the bytes are in serve's storage, not on your machine.

The link works even though my machine is off That's expected. Once a publish commits, the artifact serves from serve's storage with no machine attached. An offline source is only a problem for live links.

A publish fails with `size_cap_exceeded` or `storage_cap_exceeded` The deployment exceeds the plan's per-publish cap (Free 100 MB, Pro 1 GB) or the account's current storage cap (Free 1 GB, Pro 50 GB). Unpublish something you don't need (serve remote stop <label>) or upgrade to raise both caps.

A publish fails with `publish_rate_limited` The plan's publish budget is exhausted (Free 30/hour and 100/day, Pro 300/hour and 1,000/day). Every initialized attempt counts, including republish and failed attempts; retry after the hour or day resets.

`serve release <label>` fails with `remote_link` The label still has a published artifact on it. Unpublish first (serve remote stop <label>), then release the link if you no longer want it.

WebSockets

It opens, then closes immediately, over and over (dev mode) Your dev framework rejected the preserved public Origin. Add your exact serve hostname to its dev-origin allowlist — allowedDevOrigins for Next.js — and restart. See Mode B.

It gets a 502 before it ever opens Localhost refused the connection outright. Check the application's origin policy and the port you passed. The full table of pre-101 status codes is in Preview modes.

Network and transport

A QUIC dial fails on the TLS handshake or ALPN The client connects over UDP 443 with ALPN serve-tunnel/2. A handshake failure usually means something on your network interferes with UDP 443, or your client and the relay are on different transport versions. There is no fallback path by design, so upgrade both together.

A QUIC dial times out UDP 443 is likely filtered by a network or host firewall, or your ISP blocks outbound UDP. This is the most common corporate-network problem. serve doctor confirms it.

Limits

A new tunnel fails with `tunnel_limit` You can run 3 simultaneous live tunnels on Free and 10 on Pro. The links themselves are unlimited and are never recycled to make room, so a new tunnel at the cap is simply refused rather than displacing anything. Stop one you don't need, or serve release <label> a stale link.

A request fails with `transfer_limit` or a `503` You've reached your monthly transfer allowance (2 GB on Free, 25 GB on Pro), or the service hit its global transfer ceiling. Both reset at the start of the UTC month, and the error carries the exact reset time. Nothing is deleted when you hit a limit — links stay yours.

Still stuck? Contact serve@servelink.cc with serve doctor and serve --version output. Abuse reports go to abuse@servelink.cc.

Get a public link for whatever you’re sharing.