ccnames.dev

cnames.dev / blog

Tutorial12 min read

How to add custom domains to a Node.js / Express app

Handle customer domains in Express: host-based routing, trust proxy for the real client IP, and TLS termination — the DIY way and the one-API-call way.

S
Saeed
July 5, 2026

Adding custom domains to a Node.js / Express app is three pieces: route by the Host header, get the real client IP right behind a proxy, and terminate TLS with a certificate per hostname. The routing is a few lines of Express; the TLS is the part you should not hand-roll. Here is the DIY path and the shortcut.

// tenant.js — resolve the customer domain to a tenant, fail closed
export async function tenantByHost(req, res, next) {
  const host = req.hostname; // requires app.set('trust proxy', true)
  if (host === 'yourapp.com' || host.endsWith('.yourapp.com')) return next();

  const tenant = await getTenantByDomain(host);
  if (!tenant) return res.status(404).send('Unknown domain');
  req.tenant = tenant;
  next();
}

Step 1 — Point DNS at your app

The customer aims DNS at infrastructure you control. Prefer a CNAME for subdomains so you can renumber later without touching their zone:

app.customer.com.  CNAME  proxy.yourapp.com.

Bare domains run into the CNAME-at-apex problem. Confirm resolution with the CNAME checker.

Step 2 — Route by Host

Register the middleware and every downstream route can rely on req.tenant:

import express from 'express';
const app = express();

app.set('trust proxy', true);   // see step 3 — do this before reading req.hostname/req.ip
app.use(tenantByHost);

app.get('/', (req, res) => {
  res.send(`Hello from ${req.tenant.name}`);
});

Step 3 — The trust proxy gotcha

This is the one that bites everyone. The moment Express runs behind a TLS-terminating proxy, req.ip and req.hostname reflect the proxy, not the visitor — unless you opt in:

app.set('trust proxy', true);

With it on, Express reads X-Forwarded-For / X-Forwarded-Host. But only enable it when you genuinely sit behind a proxy you control — otherwise a client can forge X-Forwarded-For and defeat your rate limiter. Scope it tighter with a subnet or hop count in production (for example app.set('trust proxy', 1) to trust exactly one hop).

Step 4 — TLS: use a proxy, not your Node process

Do not run an ACME client inside your app to mint certs per customer domain. Put a reverse proxy in front:

The one-API-call way

Front your app with cnames.dev and register each domain:

curl -X POST https://api.cnames.dev/v1/domains \
  -H "Authorization: Bearer sk_live_..." \
  -d '{"hostname":"app.customer.com","origin":"your-express-app.internal:3000"}'

SSL, renewals, and the real client IP header are handled at the edge; your Express app keeps its trust proxy setup and reads req.tenant as before. The operational reality of doing this at scale is in how we run SSL for 5,000+ domains, and the full picture in the complete guide.

Building a SaaS that needs custom domains? cnames.dev gives every customer their own domain — SSL, edge routing, and per-tenant isolation in one API call. Free for 25 domains, no demo call. Start free · Read the docs

Frequently asked questions

Does Node.js terminate TLS for custom domains?

You can, but you should not do ACME per customer domain inside your app process. In production a reverse proxy (Caddy, Nginx) or a custom-domains service terminates TLS and issues per-hostname certificates; Express receives plain HTTP with forwarded headers.

Why is req.ip wrong behind a proxy?

Without app.set("trust proxy", ...), Express reports the socket peer — your proxy — as req.ip. Enabling trust proxy makes Express read the leftmost X-Forwarded-For entry. Only enable it when you actually sit behind a trusted proxy, or clients can spoof the header.

How do I map a domain to a tenant in Express?

Read req.headers.host (or req.hostname with trust proxy on), look the host up in your tenant store, attach the tenant to req, and 404 if there is no match. Never fall back to a default tenant.

Keep reading

S

SaeedFounder, cnames.dev — runs custom-domain infra for 5,000+ production sites at Lindo.ai.