Skip to content

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:

Terminal window
npx routingcat init --host node --origin https://shop.example.com

Setup 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 binding

Give 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 job permissions: 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 as ROUTINGCAT_DEPLOY_KEY where 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, set ROUTINGCAT_BRANCH to 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:

  1. Make a secret of at least 22 characters, as openssl rand -base64 24 prints, and set it as ROUTINGCAT_EDGE_SECRET in your server’s environment.
  2. In Cloudflare, your domain → Rules → Overview → Create rule → Request Header Transform Rule: on all incoming requests, set static the header x-routingcat-edge to the same secret.
  3. 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 binding check and in status, 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 headers check and in status.
  • 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.