Files
ds-galo/openspec/changes/host-agnostic-object-urls/design.md
T
iu eef7c6c5d0 fix(storage): emit origin-relative object URLs
Build signed object URLs as path-only (/api/objects/...) instead of
prefixing APP_BASE_URL, so media loads from whatever host or reverse
proxy served the app. Remove the now-unused base URL from the signer and
drop the APP_BASE_URL / Server.BaseURL config.
2026-10-12 12:14:46 +03:00

2.9 KiB

Design

Context

See proposal.md - Why. Relevant current state:

  • storage.Signer.URL builds baseURL + "/api/objects/<key>?e&s" (signer.go), with baseURL from cfg.Server.BaseURL (APP_BASE_URL, default http://localhost:8080).
  • That absolute URL is the only use of Server.BaseURL; SSE and API traffic are already root-relative (/api/...).
  • photos.Service.attachURLs runs outside an HTTP request, so it cannot see the request host to derive a base.

Goals / Non-Goals

Goals:

  • Photo/preview URLs work on any host or reverse proxy with no per-deployment base URL.
  • Preserve signing, expiry, quantization and caching behavior exactly.

Non-Goals:

  • Serving the app under a URL subpath.
  • Absolute URL generation for non-browser consumers (no such consumer today).
  • Changing the object route, storage backends, or delivery semantics.

Decisions

Emit origin-relative (path-only) object URLs

Signer.URL(key) returns "/api/objects/<escaped-key>?e=<exp>&s=<sig>". A leading slash makes it origin-relative: the browser resolves it against the page's scheme/host/port, which is exactly the backend that served the page.

  • Alternative considered: keep absolute URLs and require operators to set APP_BASE_URL. Rejected - this is the current footgun; a wrong or default value silently breaks all images.
  • Alternative considered: derive the base from the request (Host, X-Forwarded-Proto). Rejected - URLs are minted in attachURLs/SSE paths that have no request; threading a base through every service is invasive for no benefit once URLs are relative.
  • Alternative considered: document-relative URLs without a leading slash. Rejected - the app and its API calls already use root-relative paths, so root-relative is consistent.

Remove the base URL from the signer and config

Drop the baseURL field and parameter from Signer/NewSigner, stop reading cfg.Server.BaseURL in storage.Open, and remove Server.BaseURL from config, .env.example and the README. Leaving a now-unused variable would invite the same misconfiguration.

Verification unchanged

VerifyRequest checks the key and query (e, s) only; it never used the host, so signing and validation are untouched.

Risks / Trade-offs

  • [Subpath deployment (https://host/galo/) would break with root-relative URLs] -> The app already assumes root (/api/...); subpath support would be a separate change adding a base-path prefix. Documented as out of scope.
  • [Non-browser clients need a base to resolve object URLs] -> None consume them today; if one appears, it resolves against the instance base like any HTTP client.
  • [Tests that assumed an absolute URL] -> They parse the URL and use Path/RawQuery; update NewSigner call sites to drop the base argument.

Migration Plan

Remove APP_BASE_URL from the server environment and deploy the backend. No data or schema change. Rollback is reverting the deploy.

Open Questions

None blocking.