Skip to content
tsrelay

Callbacks from your tailnet, delivered to localhost

Deliver HTTP callbacks from your tailnet to apps running locally, without letting production connect to developer machines.

$ tsrelay http 8080
tsrelay  forwarding to http://localhost:8080
https://tsrelay.example.ts.net/~moritz/amber-otter-lamp
hook h_3k2  ephemeral  channel default
connected
14:02:11 POST /payments/done → 200  12ms  tag:prod-worker
14:02:19 POST /payments/refunded → 200  9ms  tag:prod-worker
14:03:02 POST /invoices/sent → 404  4ms  tag:billing
14:03:40 POST /payments/done → 200  15ms  tag:prod-worker

Features

How a call travels

Production never connects to a developer machine. Both sides connect to the relay, and the relay only forwards to hooks that someone created.

  1. Your production service

    tag:prod-worker

    Calls the hook's URL like any other webhook. Its grant says which channels it may send to.

  2. The relay

    tag:tsrelay

    Its own node on your tailnet. It checks the sender's grant, then hands the call down the tunnel.

  3. Your laptop

    tsrelay http 8080

    Dials out to the relay, so nothing ever connects to it. Your app answers on localhost.

Why not a reverse grant, or a public tunnel?

Calls from a laptop to a tailnet service take one grant. Calls the other way have had a few answers, each with a catch for callbacks from production.

A reverse grant

  • Production gets a standing route to every developer device it's granted.
  • Your policy has to name developer machines, and keep up as they change.
  • Calls belong to no hook and no owner, so there is nothing to inspect or replay.

A public tunnel such as ngrok or Cloudflare Tunnel

  • Internal callbacks leave your tailnet and cross the internet.
  • You can restrict who may call, but in the provider's account, not in your tailnet policy.
  • Each developer runs and secures their own.

Funnel on the laptop Tailscale Funnel, run on each laptop

  • A local port becomes reachable from the whole internet.
  • Production calls it like any public URL, so the call carries no tailnet identity.
  • Your policy has to allow Funnel on developer devices.

A webhook forwarder such as the Stripe CLI, the GitHub CLI or smee.io

  • Delivers one provider's events to localhost, through that provider or a hosted service.
  • Works well for that provider, but not for callbacks from your own services.

tsrelay

  • Laptops dial out, and production reaches only the hooks their owners made.
  • Traffic stays on your tailnet; a hook is public only when its owner and the operator opt in.
  • Your grants decide who sends and receives, and calls are written to an audit log.

When you don't need tsrelay

  • You only receive webhooks from one provider that has a forwarder, such as Stripe or GitHub: use its forwarder.
  • You work alone and a public URL on your laptop is acceptable: Tailscale Funnel on your laptop needs no relay.
  • You need calls queued and retried until your app takes them: tsrelay holds a call only briefly while your tunnel reconnects and never retries one. Use a queue.

The security model says what the relay guarantees, and what it leaves to you.

Where to start

Your tailnet already runs a relay

Install the CLI from it, and forward your first callback.

$ tsrelay http 8080

Get started

You run the tailnet

Add the relay, and grant who may send and receive.

$ tsrelay serve

Run a relay