diff --git a/openspec/changes/custom-public-host/.openspec.yaml b/openspec/changes/custom-public-host/.openspec.yaml new file mode 100644 index 0000000..caac517 --- /dev/null +++ b/openspec/changes/custom-public-host/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-22 diff --git a/openspec/changes/custom-public-host/design.md b/openspec/changes/custom-public-host/design.md new file mode 100644 index 0000000..c1d2a3b --- /dev/null +++ b/openspec/changes/custom-public-host/design.md @@ -0,0 +1,46 @@ +## Context + +`fetch.Store` constructs public redirect URLs via `PublicURL(key string) string`, which currently hardcodes the Tigris virtual-hosted URL pattern: `https://.fly.storage.tigris.dev/`. This is derived from the bucket name at `Store` construction time. + +The change is small and self-contained: one field added to `Store`, one parameter added to `New`, one env var read in `main.go`. No new dependencies, no schema changes. + +## Goals / Non-Goals + +**Goals:** +- Allow operators to override the bucket public hostname via `BUCKET_PUBLIC_HOST` +- Preserve existing behavior when the variable is unset + +**Non-Goals:** +- Supporting non-HTTPS schemes (the service always redirects to HTTPS) +- Per-request or per-key host selection +- Validating that the custom host actually serves the bucket content + +## Decisions + +### `BUCKET_PUBLIC_HOST` is a bare hostname, not a full URL +The env var value SHALL be a hostname only (e.g., `cdn.example.com`), with no scheme and no trailing slash. The service always prepends `https://` and appends `/`, keeping the construction uniform with the existing fallback path. + +*Alternative considered*: Accept a full base URL (e.g., `https://cdn.example.com`). Rejected — introduces parsing and normalization complexity (strip trailing slash, validate scheme) for no practical benefit since HTTP redirects for public images should always use HTTPS. + +### Pass `publicHost` as a parameter to `fetch.New` +`fetch.New` gains a `publicHost string` parameter (empty string = use computed Tigris default). This is explicit and keeps `Store` self-contained. + +*Alternative considered*: Functional options pattern (`fetch.WithPublicHost(...)`). Rejected — over-engineering for a single optional field with a clear zero-value default. + +## Risks / Trade-offs + +- **Silent misconfiguration**: If `BUCKET_PUBLIC_HOST` is set to an incorrect hostname, redirects will silently point to the wrong host. → Mitigation: document the expected value clearly; the operator is responsible for configuring their custom domain correctly on the Tigris side. + +## Migration Plan + +Activation (no redeploy of infrastructure needed): +```bash +flyctl secrets set BUCKET_PUBLIC_HOST= +flyctl deploy +``` + +Rollback: +```bash +flyctl secrets unset BUCKET_PUBLIC_HOST +flyctl deploy +``` diff --git a/openspec/changes/custom-public-host/proposal.md b/openspec/changes/custom-public-host/proposal.md new file mode 100644 index 0000000..d560374 --- /dev/null +++ b/openspec/changes/custom-public-host/proposal.md @@ -0,0 +1,26 @@ +## Why + +The service always constructs public redirect URLs using the computed Tigris domain (`.fly.storage.tigris.dev`). Tigris supports custom public domains, but there is currently no way to tell the service to use one — the bucket's public host is hardcoded in `fetch.PublicURL`. + +## What Changes + +- Add an optional `BUCKET_PUBLIC_HOST` environment variable; when set, the service SHALL use it as the host for all public bucket object URLs instead of the computed Tigris hostname +- `fetch.Store` and `fetch.New` updated to accept and apply the custom host +- `main.go` reads `BUCKET_PUBLIC_HOST` optionally (no-op if unset — existing behavior preserved) + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + +- `fly-config`: adds optional `BUCKET_PUBLIC_HOST` env var to the service's environment variable contract +- `tigris-bucket`: public URL construction is now configurable — `BUCKET_PUBLIC_HOST` overrides the computed Tigris hostname when set + +## Impact + +- **Modified files**: `internal/fetch/fetch.go`, `cmd/server/main.go` +- **No breaking change**: `BUCKET_PUBLIC_HOST` is optional; omitting it preserves current behavior exactly +- **Deployment**: set `flyctl secrets set BUCKET_PUBLIC_HOST=` to activate; no changes to the Tigris bucket itself required diff --git a/openspec/changes/custom-public-host/specs/fly-config/spec.md b/openspec/changes/custom-public-host/specs/fly-config/spec.md new file mode 100644 index 0000000..9036218 --- /dev/null +++ b/openspec/changes/custom-public-host/specs/fly-config/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Optional BUCKET_PUBLIC_HOST env var +The service SHALL accept an optional `BUCKET_PUBLIC_HOST` environment variable. When set, its value SHALL be used as the hostname for all public bucket object URLs in place of the computed Tigris default. The value SHALL be a bare hostname with no scheme and no trailing slash (e.g., `cdn.example.com`). When unset or empty, the service SHALL fall back to the computed Tigris URL, preserving existing behavior. + +#### Scenario: Service starts with BUCKET_PUBLIC_HOST set +- **WHEN** `BUCKET_PUBLIC_HOST` is present in the environment with a non-empty value +- **THEN** the service SHALL use that hostname when constructing all redirect URLs for cached objects + +#### Scenario: Service starts without BUCKET_PUBLIC_HOST +- **WHEN** `BUCKET_PUBLIC_HOST` is absent or empty +- **THEN** the service SHALL behave identically to before this change, using the computed Tigris hostname diff --git a/openspec/changes/custom-public-host/specs/tigris-bucket/spec.md b/openspec/changes/custom-public-host/specs/tigris-bucket/spec.md new file mode 100644 index 0000000..85522c9 --- /dev/null +++ b/openspec/changes/custom-public-host/specs/tigris-bucket/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Public URL host is configurable +When `BUCKET_PUBLIC_HOST` is set, the service SHALL construct public object URLs using that hostname instead of the computed Tigris virtual-hosted hostname. The URL format SHALL be `https:///`. When `BUCKET_PUBLIC_HOST` is unset, the URL format SHALL remain `https://.fly.storage.tigris.dev/`. + +#### Scenario: Redirect uses custom host when configured +- **WHEN** `BUCKET_PUBLIC_HOST=cdn.example.com` is set and a cache hit occurs for key `avatars/did:plc:abc/bafkrei123/default.webp` +- **THEN** the service SHALL redirect to `https://cdn.example.com/avatars/did:plc:abc/bafkrei123/default.webp` + +#### Scenario: Redirect uses Tigris host when custom host not configured +- **WHEN** `BUCKET_PUBLIC_HOST` is unset and a cache hit occurs +- **THEN** the service SHALL redirect to `https://.fly.storage.tigris.dev/` diff --git a/openspec/changes/custom-public-host/tasks.md b/openspec/changes/custom-public-host/tasks.md new file mode 100644 index 0000000..76f1d5d --- /dev/null +++ b/openspec/changes/custom-public-host/tasks.md @@ -0,0 +1,15 @@ +## 1. Update fetch.Store + +- [ ] 1.1 Add `publicHost string` field to `fetch.Store` +- [ ] 1.2 Update `fetch.New` signature to accept `publicHost string` as a parameter +- [ ] 1.3 Update `PublicURL` to return `https:///` when `publicHost` is non-empty, falling back to the computed Tigris URL otherwise + +## 2. Wire env var in main.go + +- [ ] 2.1 Read `BUCKET_PUBLIC_HOST` via `envOr("BUCKET_PUBLIC_HOST", "")` in `main.go` +- [ ] 2.2 Pass the value to `fetch.New` + +## 3. Tests + +- [ ] 3.1 Add a test to `fetch_test.go` verifying `PublicURL` returns the custom host URL when `publicHost` is set +- [ ] 3.2 Add a test verifying `PublicURL` returns the Tigris fallback URL when `publicHost` is empty