Skip to content

Add run-scoped public base URLs - #60

Merged
midagedev merged 1 commit into
mainfrom
feature/run-scoped-public-base-url
Jun 10, 2026
Merged

Add run-scoped public base URLs#60
midagedev merged 1 commit into
mainfrom
feature/run-scoped-public-base-url

Conversation

@midagedev

Copy link
Copy Markdown
Owner

Summary

One Billtap server shared by several proxied CI stacks could only generate absolute URLs (checkout session.url, billing portal URLs) against the single global BILLTAP_PUBLIC_BASE_URL. In multi-job CI each stack has its own nginx origin (e.g. https://localhost:19029/billtap), so hosted checkout redirects pointed at https://localhost:8080 where nothing listens — ERR_CONNECTION_REFUSED and 24 cascading checkout/lifecycle test failures.

Each run can now pin its own browser-facing base:

curl -X POST http://billtap:8080/runs/<runId>/v1/config \
  -d public_base_url=https://localhost:19029 \
  -d public_base_path=/billtap

Absolute URLs generated under that run resolve their base in order:

  1. The run's configured public_base_url (+ optional public_base_path)
  2. An explicit X-Billtap-Public-Base-Url request header
  3. The forwarded proxy origin (X-Forwarded-Proto/Host/Prefix) for run-scoped requests
  4. The global BILLTAP_PUBLIC_BASE_URL, then the request host — the previous behaviour, so the default run and single-stack setups are unchanged

The setting lives in memory with the run's per-run API handler (same lifetime as idempotency keys) and is dropped on run deletion. GET/DELETE /runs/<runId>/v1/config inspect and clear it; the first request lazily creates the run, so a seed container can configure it before seeding. Caller-provided success_url/cancel_url values are never rewritten.

Verification

  • go test ./... passes; all pre-existing tests pass unchanged (global-env fallback intact)
  • New server tests: per-run bases isolate (run-a vs run-b vs unconfigured run vs default run), hosted-page session refetch sees the run base, run cleanup drops the config, forwarded origin beats the global base only for run-scoped requests, run config beats the forwarded origin
  • New API tests: config endpoint roundtrip (set/inspect/clear, /api/config alias), validation rejects non-http(s)/query/credential bases and public_base_path without public_base_url, header override works and loses to run config, success_url/cancel_url untouched

Open risks

  • The per-run base is in-memory: a server restart loses it while run DBs persist. CI seeds it at job start, so this matches the existing in-memory idempotency-store trade-off; a settings table can be added later if persistence is needed.

🤖 Generated with Claude Code

One Billtap server shared by several proxied CI stacks could only
generate absolute URLs (checkout session.url, billing portal URLs)
against the single global BILLTAP_PUBLIC_BASE_URL, so hosted checkout
redirects pointed at a port nobody listens on in multi-job CI.

Each run can now pin its own browser-facing base through
POST /runs/<runId>/v1/config (public_base_url, optional
public_base_path). Absolute URLs resolve their base in order: the run
config, an X-Billtap-Public-Base-Url request header, the forwarded
proxy origin (X-Forwarded-Proto/Host/Prefix) for run-scoped requests,
and finally the global base or request host, keeping the default run
behaviour unchanged. The setting lives in memory with the run's API
handler and is dropped with run deletion. Caller-provided
success_url/cancel_url values stay untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@midagedev
midagedev merged commit 58991d0 into main Jun 10, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant