Clear ExchangeDocs Live demo

Hostnames and DNS

One Fly app, clear-exchange, serves every hostname. The request's Host header decides what it gets.

HostnameServesSet by
clearexchange.aiThe landing page. Links go straight to the app and docs hostnamesSITE_HOSTS (first entry)
www.clearexchange.aiRedirects (301) to https://clearexchange.ai, keeping the pathSITE_HOSTS (other entries)
app.clearexchange.aiThe app at /, and the API (/v1/*, /.well-known/jwks.json)APP_HOSTS
docs.clearexchange.aiThe docs at / (see Docs site)DOCS_HOSTS
clear-exchange.fly.dev, localhost and any other hostnameEverything, as before: the landing page at /, the app at /app, the docs at /docs/ and the API, with relative links(default)

All three variables are comma-separated bare hostnames, set in fly.toml. A hostname can have only one role; startup fails if one is listed twice. With none set, as in local dev and tests, every hostname serves everything.

Redirects from old paths

RequestGoes to
clearexchange.ai/apphttps://app.clearexchange.ai/ (301)
clearexchange.ai/docs/<page>https://docs.clearexchange.ai/<page> (301)
clearexchange.ai/v1/..., clearexchange.ai/.well-known/...The same path on app.clearexchange.ai (308, so a POST stays a POST with its body)
app.clearexchange.ai/app/ (301)
app.clearexchange.ai/docs/<page>https://docs.clearexchange.ai/<page> (301)
docs.clearexchange.ai/docs/<page>/<page> (301)

Query strings are kept. Redirect targets are always a configured hostname, never anything taken from the request. The code is src/http/hosts.ts.

The HTML files in web/ use plain root-relative links (/app, /docs/, /). On a site or app hostname, the server rewrites them at startup so each link points at the canonical hostname of the page it leads to. For example, the landing page's "Open the demo" becomes https://app.clearexchange.ai/ and the app's "Overview" becomes https://clearexchange.ai/. On clear-exchange.fly.dev and locally the links stay relative, so those work with no DNS. Assets under /ui/ are served on every hostname and are never rewritten.

Canonical issuer: app.clearexchange.ai

PUBLIC_URL is https://app.clearexchange.ai. It is the iss and aud of every entitlement. That is the hostname where the API and the signing keys (/.well-known/jwks.json) live, so a verifier that calls the exchange or fetches its keys uses the same name as the token's issuer. The apex is a marketing page and redirects API calls, so it is not the issuer.

  • The exchange verifies tokens against its own PUBLIC_URL whichever hostname a request arrives on, so agents that still call clear-exchange.fly.dev keep working.
  • Changing PUBLIC_URL invalidates entitlements issued under the old value. They last 15 minutes, so a deploy that changes it disturbs at most the tokens issued in the 15 minutes before it.

Security headers, CSP and the admin token

Every page and API response is same-origin on the hostname that serves it. The app on app.clearexchange.ai calls the API on app.clearexchange.ai, and the docs search fetches its index from the docs hostname. So the Content-Security-Policy stays 'self' only, with no third-party or cross-host origins and no CORS. Links to the other hostnames are ordinary navigations, which the CSP does not restrict.

The exchange sets no cookies. The app keeps the admin token, if you enter one, in sessionStorage for that browser tab, scoped to the hostname. A token entered on clear-exchange.fly.dev/app is not seen on app.clearexchange.ai, so you enter it again there. Closing the tab forgets it.

TLS certificates and IP addresses (CI)

The deploy job in .github/workflows/ci.yml does this after every deploy, with the FLY_API_TOKEN secret. Nobody runs flyctl by hand.

  1. IP addresses. Makes sure the app has a public IPv6 address and an IPv4 address, allocating a dedicated IPv6 and a shared IPv4 (both free) only if it has none. It never allocates on output it cannot read. It prints the addresses and the DNS records for every hostname in the log and in the job summary (scripts/fly-dns.ts).
  2. Certificates. Runs flyctl certs add for each hostname in CERT_HOSTNAMES, treating "already exists" as success. Fly issues each certificate once that hostname's DNS points at the app. The job summary shows each certificate's status.
  3. Smoke check. Checks the fly.dev URL (which must pass), then tries https://<hostname>/healthz on each custom hostname. A miss is a warning, because a hostname only works once its DNS record is in place.

CERT_HOSTNAMES defaults to all four hostnames. A repository variable FLY_CERT_HOSTNAMES overrides it.

DNS records

DNS for clearexchange.ai is managed outside this repository, at the registrar (GoDaddy). The records:

TypeNameValue
A@66.241.124.32
AAAA@2a09:8280:1::1d1:da7e:0
CNAMEwwwclear-exchange.fly.dev
CNAMEappclear-exchange.fly.dev (or the <id>.clear-exchange.fly.dev target Fly shows; both work)
CNAMEdocsclear-exchange.fly.dev

The apex (@) uses A and AAAA records because DNS does not allow a CNAME at the apex. The two addresses are the app's own: the shared IPv4 and dedicated IPv6 that clear-exchange.fly.dev resolves to. The deploy log prints the current values, so check there if the app's addresses ever change. Remove any other A records on @ (for example a registrar's parking or forwarding addresses) and turn off any domain forwarding, or some visitors will reach the old page.

Some providers offer ALIAS, ANAME or "CNAME flattening" at the apex, which can point @ at clear-exchange.fly.dev instead of the two addresses. Either way works. The A and AAAA records work at every provider.

If the provider is Cloudflare, set each record to "DNS only" (grey cloud). Fly terminates TLS itself.