Your own Node.js server
RoutingCat runs on a Next.js site that your own server serves with next start as it does on Vercel, in proxy.ts, before your pages. This page covers what differs: how setup creates the site, how each build proves itself, how to serve the app, and how Cloudflare in front lets routing use each visitor’s location.
Set up with --host node
Run setup with your own server as the host and your site’s production address:
npx routingcat init --host node --origin https://shop.example.comSetup makes the same changes to your code as on Vercel, and creates the site bound to no host, since no host’s token can prove your builds. It also finds out whether Cloudflare’s proxy stands in front of your address, as the next sections explain, and ends with the steps they detail:
✓ production address https://shop.example.com, from --origin ✓ Cloudflare in front of shop.example.com, from its addresses and Cloudflare's request id on its answer: routingcat.config.ts declares it…✓ site fresh created (op_01m3yesth0fjfsctr17tbq1z82), on your own server, with no host bindingGive each build its credential
next build registers each deploy with RoutingCat, as on Vercel, and needs a credential to do so:
- In a GitHub workflow, link the site to its repository once, with
npx routingcat link github --branch main, naming your production branch, and give the jobpermissions: id-token: write. Each run then proves itself with GitHub’s own token, with no secret to keep: production on that branch, a preview on any other. - Anywhere else, the site’s owner mints the deploy key with
npx routingcat keys deploy --branch main, and you set it asROUTINGCAT_DEPLOY_KEYwhere the app builds. The production key proves production only for builds of its branch, as the build’s CI reports it: GitHub Actions, GitLab, Bitbucket, CircleCI, Jenkins’ multibranch pipelines, Azure Pipelines and Buildkite report it themselves. Anywhere else, as a build on the server itself, setROUTINGCAT_BRANCHto the branch the build deploys, or the production key is refused.
A build without either still ships, serving your defaults, and says what to set. Access and keys covers rotating the key.
Serve with next start
Run next start as you do: without -H, with -H 0.0.0.0, -H :: or -H localhost. Don’t bind it to a loopback address such as -H 127.0.0.1 or -H ::1: there, Next.js fetches its proxy’s rewrites over HTTP instead of serving them itself, and they never reach the copies RoutingCat builds of your static pages. RoutingCat reads that binding as the server starts, from next start’s arguments or from what Next.js’s own server records, under PM2 and with the standalone output’s server.js too, so your static pages serve their defaults, unrouted, while pages rendered at each request keep routing. The server’s log says so once, and doctor warns in its server binding check.
A custom server listens where its own code says: give it localhost. Bound to a loopback address, RoutingCat reads at its first request the hostname Next.js keeps for it, so it serves your defaults, unrouted, as next start does. Should your Next.js keep no hostname, RoutingCat learns the binding from Next.js’s fetches of its rewrites instead, each process from the first that reaches it: every visitor still gets your defaults, but until a process has learned, the decisions it records say the visitor was routed.
Each server process registers itself at its first request, so you can run as many as you like, behind a load balancer or not.
Declare Cloudflare in front
On your own server, RoutingCat’s proxy sees each request’s headers and nothing else, not even the address it came from. Cloudflare’s proxy adds each visitor’s address and location as headers, which anyone could also write into a request sent straight to your server. So RoutingCat uses them where your site declares that Cloudflare stands in front, with behindCloudflare: true in routingcat.config.ts, as setup writes it:
import { config } from 'routingcat/next'
export default config({ site: 'fresh', publicKey: 'pk_fresh_XMufiEsf', // public: names the site; write-only origin: 'https://shop.example.com', behindCloudflare: true, // Cloudflare's proxy stands in front of your server})Setup writes it for you where both signs say Cloudflare answers for your address: the address resolves into Cloudflare’s published ranges, and its answers carry Cloudflare’s request id, cf-ray. Where neither does, it writes nothing. Where it can’t tell, as when the address doesn’t answer yet, it asks you at a terminal, and your agent asks you otherwise: answer with npx routingcat init --host node --behind-cloudflare, or --no-behind-cloudflare. Where --no-behind-cloudflare contradicts a declaration your config already makes, setup removes the line it writes; a declaration written any other way, it prints the edit for you to make.
Then turn on Cloudflare’s visitor location headers: in Cloudflare, your domain → Rules → Settings → Managed Transforms → “Add visitor location headers”.
Without the declaration, routing goes without visitors’ locations, and without filtering bots by their address. Declare Cloudflare only where its proxy, the orange cloud, stands in front of your address: with nothing in front, the declaration would trust headers anyone can send. A visitor who reaches your server directly can still forge them, which changes little beyond their own visit, so each record marks its location as declared rather than verified.
Verify it with the edge secret, optionally
You can also prove to RoutingCat that each request came through Cloudflare, so that its records count as verified rather than declared. It needs a secret on both sides:
- Make a secret of at least 22 characters, as
openssl rand -base64 24prints, and set it asROUTINGCAT_EDGE_SECRETin your server’s environment. - In Cloudflare, your domain → Rules → Overview → Create rule → Request Header Transform Rule: on all incoming requests, set static the header
x-routingcat-edgeto the same secret. - Restart the server, so that it reads the secret.
Cloudflare’s rule replaces any x-routingcat-edge a visitor sends, so only requests Cloudflare forwards carry your secret. RoutingCat removes the header before your pages see it, and never records it. Where the secret is set but a request through Cloudflare lacks it or carries another value, the server’s log says so once, and that request counts as declared, or untrusted without the declaration. To change the secret, set the new one on both sides.
Check it
Run npx routingcat doctor once visits have come through:
- A server bound to a loopback address, whose static pages serve their defaults, unrouted, is a warning in doctor’s
server bindingcheck and instatus, until a static page is routed again. - A site that declares Cloudflare, but whose requests stopped carrying its headers, as when the address’s DNS record stops being proxied, is a warning in doctor’s
Cloudflare headerscheck and instatus. - Cloudflare in front without the declaration is reported as information, and so is no Cloudflare in front: your site works, without visitors’ locations.
- Cloudflare in front without its location headers is the same warning as on Vercel.
Next
RoutingCat routes each visitor of your site to the variants that convert.