Skip to content

[Feature]: Ordered fallback endpoints for a saved environment (e.g. LAN primary, Tailscale secondary) #5233

Description

@eljose47

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I am describing a concrete problem or use case, not just a vague idea.

Area

apps/web (the change is in packages/client-runtime, shared by web and mobile; the template has no option for it)

Problem or use case

I self-host a headless t3 serve backend that is reachable at two addresses:

  • http://192.168.178.36:3773 on my LAN
  • https://machine.tailnet.ts.net over Tailscale Serve

Which one works depends on where the client is: a laptop docked at home can use the LAN address, the same laptop on another network can only use the tailnet address, and a phone alternates between the two several times a day.

A saved environment stores exactly one endpoint, so I have to edit the environment's URL by hand every time I change networks. When the stored endpoint is unreachable, EnvironmentSupervisor retries that single URL forever with backoff capped at 16s and never tries the other address I know is up.

Saving the backend twice is not a workaround: the catalog is keyed by environmentId (packages/client-runtime/src/connection/registry.ts), and connectionId is derived as bearer:${environmentId} (connection/onboarding.ts), so pairing the same server at a second URL overwrites the first entry instead of adding one.

Proposed solution

Let a bearer environment hold an ordered list of endpoints instead of a single httpBaseUrl / wsBaseUrl pair, and have the connection runtime try them in order.

  • BearerConnectionProfile (packages/client-runtime/src/connection/catalog.ts) gains an ordered endpoint list. Existing single-URL profiles migrate to a one-element list.
  • ConnectionResolver.prepare (connection/resolver.ts) walks the candidates, and EnvironmentSupervisor (connection/supervisor.ts) advances to the next candidate on transient reasons (network, timeout, transport, endpoint-unavailable) instead of re-attempting the same URL.
  • Remember the last endpoint that connected and start from it next time, so the common case stays a single attempt.
  • Settings → Connections gains add / remove / reorder for endpoints on an existing environment, extending updateBearerConnection (connection/onboarding.ts).

Bearer credentials are scoped per environment rather than per URL, so the pairing flow should not need to change.

This would also give the existing AdvertisedEndpoint concept somewhere to land. Today the server advertises candidate endpoints, but per docs/internals/remote.md they are a pairing-time UI hint that the connection runtime never consumes.

Why this matters

Reaching one backend from more than one network is the normal case for a self-hosted setup, not an edge case. Right now the options are to pick the single URL that is correct on one network and hand-edit it elsewhere, or to route everything through one overlay and accept that the environment is unreachable whenever that overlay is down.

The same mechanism generalizes past my case: LAN + Tailscale, Tailscale + relay, direct + SSH-forwarded, or a stable hostname plus a raw IP as a DNS-outage fallback. It turns "my laptop moved" from a manual settings edit into a reconnect.

Smallest useful scope

Two endpoints per bearer environment (primary + fallback), tried in order, with the last working one remembered and tried first.

Not needed for a first pass: reordering UI beyond add/remove, automatic endpoint discovery, parallel/happy-eyeballs racing, or any change to relay, SSH, or primary targets.

Alternatives considered

  • Use only the Tailscale MagicDNS URL. This is what I do today and it works well: Tailscale connects same-LAN peers directly (tailscale status shows direct 192.168.178.21:33593 for my phone), so home traffic stays on the local wire and I get HTTPS for the hosted web app. The cost is that every client must have Tailscale running and logged in; if it is down, the LAN path is not reachable even when the machine is three meters away.
  • Save the backend twice, once per URL. Not possible today, per the environmentId keying described above.
  • Split-horizon DNS, resolving one hostname to the LAN address at home and the tailnet address elsewhere. Breaks HTTPS, because the Tailscale Serve certificate is only valid for the *.ts.net name.

Risks or tradeoffs

  • Failover must distinguish transient failures from auth/config failures. An expired credential on the primary should block rather than silently shift traffic to the fallback.
  • Serial candidate attempts add latency when the first endpoint is black-holed rather than refusing. This probably needs a short per-candidate connect timeout.
  • Endpoint identity matters: after failing over, the client should confirm the environmentId from /.well-known/t3/environment still matches before treating the candidate as the same environment. A stale LAN IP can be reassigned to a different machine by DHCP.
  • Changing a persisted profile schema needs a migration for existing saved environments.

Examples or references

  • docs/internals/remote.md — "resolves one access endpoint"; advertised endpoints are hints the connection attempt does not use.
  • docs/internals/connection-runtime.md — supervisor retry ladder, backoff capped at 16s.
  • Observed on server 0.0.31, Linux, paired from the web app and the Android app.

Contribution

  • I would be open to helping implement this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions