Skip to content

Relay

The relay bridges public HTTPS endpoints to local xPort clients. Zero-storage by design: content is relayed, never persisted.

Build from source (Go 1.27+):

Terminal window
go build -o relay ./cmd/relay
  • Go 1.27 or later
Terminal window
git clone https://github.com/xport-dev/xport-relay.git
cd xport-relay
go build -o relay ./cmd/relay

Copy the example configuration:

Terminal window
sudo mkdir -p /etc/xport
sudo cp config.example.yaml /etc/xport/relay.yaml
Key Default Description
server.host 0.0.0.0 Address the relay listens on
server.port 8080 Port the relay listens on
server.public_base https://{name}.xport.sh URL template advertised to clients; {name} is replaced by the session name
server.free_base (empty) Separate URL template for anonymous and free-tier (non-vanity) sessions, e.g. https://{name}.xport.free. Empty serves every tier from public_base. Requires wildcard DNS + a certificate for that domain
Key Default Description
tls.cert_file (empty) TLS certificate. Empty = plain HTTP (dev or behind a TLS proxy)
tls.key_file (empty) TLS private key
Key Default Description
auth.mode token token (static bearer) or oauth (validate login tokens via introspection)
auth.token (empty) Bearer token clients must present in token mode. Empty = auth disabled (dev only)
auth.introspect_url (empty) RFC 7662-style endpoint for oauth mode, e.g. https://xport.dev/oauth/introspect
auth.cache_ttl 60s How long introspection answers are cached
auth.allow_anonymous false In oauth mode: let requests without a token through as anonymous free-tier clients (hosted free tier)

Enforced in oauth mode only — token mode keeps full freedom (any valid name, nothing persisted).

Key Default Description
names.anon_random_length 20 Random name length for anonymous clients (logged-in users keep short 5-char randoms)
names.blocked [] Extra blocked words on top of the built-in infrastructure/brand list (exact, case-insensitive match)
names.free_limit 3 Reserved custom names for logged-in free users (the fixed name never counts)
names.pro_limit 10 Reserved custom names for vanity (pro/team) users
Key Default Description
storage.names_file (empty) JSON file persisting reserved names across restarts (written 0600). Empty = in-memory only

The hosted policy at a glance: anonymous clients always get random names; requesting a custom name requires an account (xport login) and counts against the per-plan limit; blocked words are rejected for everyone; reserved names survive relay restarts.

On the hosted relay, the advertised domain mirrors the plan: anonymous and free-tier sessions are served from the free zone (free_base, e.g. *.xport.free — long random names, isolated from the paid domain’s reputation), while paid (vanity) tiers get public_base (e.g. *.xport.sh). Self-hosters leave free_base empty and serve everything from one domain.

Key Default Description
logging.level info debug, info, warn, error
Key Default Description
limits.session_ttl 24h Default lifetime of a tunnel session (safety cap; tunnels die with the client anyway)
limits.anon_max_session_ttl 4h Maximum lifetime for anonymous (not logged-in) sessions, explicit requests and the default alike
limits.free_max_session_ttl 24h Maximum lifetime for registered free-plan sessions
limits.max_session_ttl 720h (30 days) Maximum lifetime for pro/team sessions
limits.enterprise_max_session_ttl 8760h (365 days) Maximum lifetime for enterprise sessions; values <= 0 remove the cap entirely
limits.cleanup_interval 1m How often expired sessions are reaped
Key Default Description
internal.token (empty) Bearer token for the account website’s admin API (/internal/v1/names — list/release a user’s reserved names). Empty = endpoint not mounted
Key Default Description
domains.url (empty) Account server’s /internal/domains endpoint the relay polls for the verified custom-domain (BYOD) list. Empty = --domain rejected
domains.token internal.token Bearer token for the poll
domains.cache_ttl 60s How long the fetched list is used before refreshing

Raw TCP tunnels (xport tcp): any protocol, not just web services. Disabled by default; the relay rejects tcp registrations otherwise.

Key Default Description
tcp.enabled false Turn the TCP listener pool on
tcp.host 0.0.0.0 Address the per-session listeners bind
tcp.port_min / tcp.port_max 12000 / 12999 Port pool allocated per session
tcp.public_host localhost Host advertised to clients (tcp://<public_host>:<port>)

GET /v1/control is the WebSocket control plane (see the protocol spec): clients register tunnel sessions and keep them alive with heartbeats. It sits behind the bearer-token middleware like every route except /healthz.

Clients may attach a per-session traffic policy to the register message; the relay compiles it (failing closed on invalid policies) and enforces it on every public request and response.

Every option also works as an environment variable: uppercase, XPORT_RELAY_ prefix, dots become underscores — e.g. XPORT_RELAY_SERVER_PORT, XPORT_RELAY_AUTH_TOKEN. The config directory defaults to /etc/xport and can be moved with XPORT_RELAY_CONFIG_DIR.

Terminal window
./relay

Stop with Ctrl-C — in-flight requests finish before shutdown. GET /healthz is public and returns {"status":"ok"}; all other routes require the bearer token when one is configured.

Point the CLI at your relay with relay.url.