Every customer link built on the right domain

One resolver decides the address used in every outgoing customer link, and it refuses to ever emit a developer machine's address.

What it is

getOrgSiteUrl() is the single function every link-building path calls. It walks a fixed priority — the tenant's verified custom domain, then the platform canonical URL, then a legacy alias, then the deploy preview URL, then the request origin — and returns a value with a protocol and no trailing slash. Anything that looks like localhost or a loopback address is treated as unset.

Also called: link domain · portal link URL · links point to localhost · estimate link address

See it
Every customer link built on the right domain
🔒ridgelinepostframe.com/project/custom472
40×64 Shop
Framing · on schedule
Side-by-side of the same estimate email with the link host swapped: platform default versus the tenant's own domain. No dedicated component — show the generated link inside the email preview frame (src/components/settings/EmailPreviewFrame.tsx). Sample data — no customer information appears here.
How it works
  1. 1The tenant's verified custom domain is looked up (cached 60 seconds) and wins if present.
  2. 2Otherwise the platform's canonical site URL environment values are used.
  3. 3Preview deployments fall back to their own URL; local development falls back to the request origin or referer.
  4. 4nonLocal() strips any candidate matching localhost, 127.x, 0.0.0.0 or [::1] before it can be returned.
  5. 5invalidateOrgSiteUrlCache() is called the moment a custom domain is attached, removed or changes status.
Why we built it

The guard is written as a hard rule with two dates attached: 'Customer links must NEVER point at localhost (hard rule, 07-12 + the 07-23 dev-send incident): a dev machine emailing a real customer would bake dead links.' A quote or invoice is often forwarded and opened days later, so a dead link is not a transient bug — it is a permanently broken document in a customer's inbox. Making it one resolver rather than a per-feature concatenation means a domain change lands everywhere at once.

The problem
  • Links generated during development or preview builds could reach real customers with an unreachable host.
  • A domain change had to be remembered at every link-building call site.
  • Different features disagreed about which host a customer link should use.
Sound familiar?
What you get
Every customer-facing link uses the same, correct address.
Attaching a domain updates outgoing links immediately, not after a cache expires.
A developer machine can never emit a live customer link.

See it on your own jobs

Twenty minutes, your numbers, no slide deck. We’ll build one of your real buildings in front of you and send you the estimate link at the end — yours to keep either way.

or keep browsing features →