Run one app on two domains
You'll make one binary answer for two host names and serve something different on each. The loader reads which domain it was asked on. The wrong header can't forge it.
Before you start
Straight from crates.io. No Node, no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Nothing reads the Host header by default
That's deliberate. A Host header is whatever the client wrote, so FSR ignores it until you say which names your deployment actually answers on.
[server]
hosts = ["acme.example", "widgets.example"]The list is an allowlist, not a pattern. A request's Host is lowercased and compared whole, port included. localhost:3000 and localhost are two different entries. Anything not on the list reads as null rather than the string the caller sent, so a body only ever sees a name you named.
Read it in a loader
ctx.host is string | null. Null means either you left the key out or the request named something you don't serve.
import type { Ctx } from "@snapfire/fsr";
const BRANDS: Record<string, { name: string; accent: string }> = {
"acme.example": { name: "Acme", accent: "#c2410c" },
"widgets.example": { name: "Widgets Inc", accent: "#1d4ed8" },
};
export async function load({ host }: Ctx<"/">) {
const brand = BRANDS[host ?? ""] ?? { name: "Acme", accent: "#c2410c" };
return { brand };
}One binary. One plan. Two brands, chosen on the server before any markup exists.
Check the boot report
The host tells you the arrangement is live, every start, while the key is set.
hosts acme.example
widgets.example
ctx.host reads the request's Host against these; the server in front must set it; a client otherwise names its own
The part people get wrong
Read that last line again. ctx.host is exactly as trustworthy as whatever terminates the connection. A value that happens to be on your allowlist is indistinguishable from the same value typed by hand into a curl command.
So the proxy has to set it. With nginx:
proxy_set_header Host $host;under a server_name that matches, plus a default server for the socket so a request naming something you don't serve is answered there rather than passed through. Without the default server, someone can pick which of your brands they get. Usually harmless, sometimes not. Decide it on purpose.
Canonical links
document.origin is your deployment's one preferred origin, not the host a request arrived on. Those are different on purpose. A canonical link is there to collapse two names serving the same pages. Emit one per host and you claim both are the original, which is the duplicate you were trying to avoid.
If you genuinely serve a different site per host, build the canonical yourself from ctx.host:
export const meta = ({ data }: MetaCtx<Data>) => ({
title: data.title,
head: [canonical(`https://${data.host}/docs/${data.slug}`)],
});An absolute URL passes through untouched.
One catch
A loader that reads ctx.host is never prerendered, because two configured hosts are two different answers and a prerendered file is one. You traded the static file for the per-domain content. That's the right trade when you meant it. 100 is where to look when you did not.
One deployment, not two
One binary, one plan, one build. No middleware rewriting paths by domain, no environment variable per brand.
Next up: 070. Put a docs site inside your app, where a whole second application mounts inside this one.