Skip to content

feat(connect): release the Cloudflare tunnel when the environment shuts down - #4531

Merged
juliusmarminge merged 7 commits into
mainfrom
t3code/340f7869
Jul 27, 2026
Merged

feat(connect): release the Cloudflare tunnel when the environment shuts down#4531
juliusmarminge merged 7 commits into
mainfrom
t3code/340f7869

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

Cloudflare bills per provisioned tunnel regardless of traffic, so per their team's guidance ("delete the tunnel when the user stops the server, otherwise you will have a bunch of idle tunnels") the managed tunnel is now disposed when the environment goes offline, while reconnects keep the exact same URL.

Disabling Connect (t3 connect unlink, web/mobile unlink, publish-only downgrade) already deleted the tunnel via the relay deprovision path. The gap was stopping the server: only the local cloudflared process was killed, and the provisioned tunnel idled in Cloudflare indefinitely. This PR covers that case.

Relay

  • New ManagedEndpointProvider.release: deletes only the Cloudflare tunnel (404-tolerant) and deliberately keeps the allocation row and DNS record.
    • The hostname is a pure function of (stage, userId, environmentId) and the tunnel-name reservation persists, so the next provision recreates the tunnel under the same name, repoints the CNAME to the new tunnel id, and returns a fresh connector token — same endpoint URL.
    • Keeping the recorded (now stale) tunnelId also keeps connect/status authorization intact, so an offline environment still reports "offline" from the health probe instead of regressing to "not authorized".
  • New authenticated endpoint: DELETE /v1/client/environment-links/:environmentId/tunnel.

Server

  • New releaseManagedTunnelOnShutdown: stops the connector, calls the release endpoint with the stored CLI token, then drops the cached endpoint runtime config — the connector token dies with the tunnel, so the next boot waits for the link reconcile instead of respawning cloudflared with a dead token.
  • Registered as a best-effort shutdown finalizer (10s timeout) next to the startup reconcile layer. On failure the cached config is kept: the tunnel still exists, so the stored token keeps working across the restart (today's behavior as the fallback).
  • Reconnect is unchanged for the user: the existing reconcileDesiredCloudLink on startup re-links and re-provisions automatically.

A hard kill (crash/power loss) still leaves a tunnel behind since nothing can run; the next link reuses it, so nothing accumulates. A relay-side idle sweep would be a possible follow-up.

Tests

  • Provider: release → re-provision yields an identical endpoint with the DNS record repointed (never deleted); no-op without a recorded tunnel; 404-tolerant delete; non-404 failures surface for retry.
  • Server: success path (DELETE request shape, connector stop, token drop), publish-only no-op, config retention when the relay request fails.
  • All touched suites pass: relay provider (21), linker (4), Api/contracts/CliState/runtime (18), cloud http (11), server (112). Typecheck, lint, and fmt clean.

🤖 Generated with Claude Code

Note

Release the Cloudflare tunnel via relay DELETE endpoint when the environment shuts down

  • Adds releaseManagedTunnelOnShutdown in http.ts that stops the local connector and calls DELETE /v1/client/environment-links/:environmentId/tunnel on the relay with a bearer token.
  • On a successful relay response, removes the stored CLOUD_ENDPOINT_RUNTIME_CONFIG only if it still matches the released config, preserving a freshly written config from a concurrent fast restart.
  • Adds ManagedEndpointProvider.release in the relay, which uses a new claimRelease optimistic-lock check (matching tunnelId and updatedAt) before deleting the Cloudflare tunnel, leaving the allocation row and DNS record intact.
  • Registers the shutdown call in server.ts as an Effect finalizer with a 10-second timeout, logging success or warning on failure.
  • Risk: A failed or timed-out release leaves the tunnel alive; the next link reuses the existing tunnel rather than provisioning a new one.

Macroscope summarized 59b16af.


Note

Medium Risk
Changes managed tunnel lifecycle, shutdown finalizers, and relay allocation locking; failures leave tunnels running (existing fallback) but incorrect release could affect connectivity or billing.

Overview
Managed tunnels are torn down when a CLI-linked environment stops, so idle Cloudflare tunnels are not left billing after the server goes offline. The environment link and hostname stay in place; startup reconcile can provision a replacement tunnel at the same URL.

On the relay, a new authenticated DELETE …/environment-links/:environmentId/tunnel calls ManagedEndpointProvider.release, which deletes only the Cloudflare tunnel and keeps the allocation row and DNS. claimRelease uses updatedAt as a generation lock so a release that races a concurrent provision returns { ok: false } and does not delete a freshly provisioned tunnel.

On the server, releaseManagedTunnelOnShutdown stops the local connector, issues that DELETE with the stored CLI token, and removes CLOUD_ENDPOINT_RUNTIME_CONFIG only when the relay confirms release and the stored config still matches what was released (so a fast restart or ok: false keeps the live token/config). This runs as a 10s best-effort shutdown finalizer for cloud-enabled servers; it applies only to CLI-desired managed links—not publish-only or web/mobile-installed links.

Reviewed by Cursor Bugbot for commit 59b16af. Bugbot is set up for automated code reviews on this repo. Configure here.

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant