Whether you've got technical questions or just want to learn more, we're happy to dig into it with you. Get answers from a real, human engineer over email or on a call.
Tutorial
A tutorial guide for supporting user custom domains as a feature in your Next.js app, with an example github repo.
This guide is meant to show you how to allow users of your Next.js app to connect their own domains and load up content specific to them. Custom domains - sometimes called vanity domains, whitelabel domains, or customer domains - are a key feature of many apps.
If you need to manage custom domains with SSL for SAAS, marketplaces, blog hosts, email service providers, or other platforms, then this is the guide for you.
What you'll learn in this guide:
You're likely pretty familiar with web domains, but we'll go over a few of the specifics of how domains work that can sometimes cause confusion.
Domains are bought and managed with registrars like Hover, GoDaddy, Namecheap, and many others. This is where a domain is officialy registered and may or may not be where it's DNS is managed. For our purposes, we can assume that it will be the user that owns their own domain, which they would like to point at your app.
Domains don't do much unless they're pointed at a target server. This is done with DNS, which tells devices where to send requests for a domain, using DNS records. These records are set at the domain's DNS provider, which may or may not be the same as the registrar.
Apex domains are the root, naked domain with no subdomain attached. For instance, example.com is an apex domain.
A subdomain is what we call it when we prepend something to an apex domain. For example, sub.example.com or sub2.sub1.example.com are both subdomains.
Technically, subdomains are domains. For the purpose of this guide though, when we use the word domain, we're referring to an apex domain.When a request is sent out over the internet, there are various points where it's contents could be inspected and tampered with unless they're encrypted.
This is especially important for any private data such as ecommerce or personal information, and may be legally required in many cases. It's generally a very good idea for all requests to be encrypted, in order to prevent censorship, request injections, snooping and other tampering.
SSL certificates - which are technically TLS certificates now, but often still called by the old name - are the standard by which browsers and other applications encrypt these requests. Using them requires a server with a provisioned SSL certificate for each domain. We'll go more into detail on this in the networking section below.
In order for your app to serve custom domains, the requests for each custom domain need to be networked to reach your server(s). For those requests to be secured, you'll also need to provide and manage an SSL certificate for each custom domain.
Each custom domain will need to have DNS records set to point requests at your app - either directly or indirectly. There are two options for how you can go about this, each with significant trade-offs that you should know about:
DNS A records can only be pointed at an IPv4 address, formatted as x.x.x.x, where each x can be any value between 0 and 255.
It's important to note that apex domains - those with no subdomain (including www) - can only be pointed with A records. They cannot be pointed with CNAME records.
This is the easiest and most universal method but also carries the risk that if you ever need to change that IPv4 address, you'll need to have each user change their DNS individually.
CNAME records cannot point an apex domain and cannot point at an IPv4 address, but they can point subdomains at any other domain or subdomain.
For example, you could point subdomain.example.com at test.com and requests would be sent to wherever test.com is pointing.
If it's acceptable in your use case to require custom domains have a subdomain, then you can do the following:
You've likely secured your app already with SSL certificates, but unfortunately those will only apply to requests going directly to that domain. SSL certificates cannot be shared amongst apex domains.
You're going to need SSL certificates for each individual custom domain that points at your app, to encrypt requests as well as ensure security and privacy for that domain specfically.
In order to do that you'll need an automated system to provision SSL certificates automatically when custom domains are created, including:
For those that need to build this in-house, we recommend Caddy as your starting point. However, this is a complex topic and could easily be many guides worth of content.
Building reliable SSL management, distributed networking, and monitoring in-house for more than a few domains can be difficult even with the help of amazing tools like Caddy. There are a wide variety of obscure pitfalls that often come at the worst times, and will usually cause customer-facing downtime. So just be aware that it will be an ongoing effort.
To keep this tutorial from exploding in length, we won't be covering building this in-house here. If you'd prefer to have a managed service handle this for you, please see our bonus section below.
There are multiple ways to handle routing in Next.js, depending on how you want your app to work. We cover the Pages Router and the App Router here, since those are the most typical approaches.
Some aspects are quite different between the two, and each has it's own set of trade-offs. If you haven't already chosen one, you can find a quick comparison here.
The Pages Router was the default way to route requests in Next.js until version 13, when they introduced the App Router. If your app makes use of the Pages Router, here are some options for you to handle custom domains.
You can add middleware to the Pages Router that can capture requests before they reach your page, preparing requests or performing redirects.
Next.js recommends smaller operations when using middleware, such as rewriting a path, rather than complex data fetching or extensive session management. Depending on your app, this may be enough for your custom domain needs.
Here's an example of a simple middleware that could make some change based on the custom domain before continuing on:
// Example repo file: middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
/**
* Check if there's a header with the custom domain,
* and if not just use the host header.
* If you're using approximated.app the default is to
* inject the header 'apx-incoming-host' with the custom domain.
*/
const domain = request.headers.has('apx-incoming-host')
? request.headers.get('apx-incoming-host')
: request.headers.get('host');
// do something with the "domain"
const response = NextResponse.next({
request: {
headers: request.headers,
},
});
return response;
};
getServerSideProps
If you're using the Pages Router, using the getServerSideProps
approach may be the best choice for you.
This method runs domain checking code on the server before rendering your pages, which may be important for some applications, and lets you pass logic as props to your page components.
Note: getServerSideProps
can only be used on top-level pages, not inside components.
For component-level server logic, consider using the app router with React Server Components.
Here's an example of using getServerSideProps
to generate a different response based on the custom domain:
// Example repo file: pages/index.ts
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
type ApproximatedPage = {
domain: string;
}
export const getServerSideProps = (async ({ req }) => {
/**
* Check if there's a header with the custom domain,
* and if not just use the host header.
* If you're using approximated.app the default is to
* inject the header 'apx-incoming-host' with the custom domain.
*/
const domain = req.headers['apx-incoming-host'] || req.headers.host || process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN;
// do something with the "domain"
return { props: { domain } }
}) as GetServerSideProps<ApproximatedPage>;
export default function Home({ domain }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
{domain === process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN ? 'Welcome to the primary domain' : `Welcome to the custom domain ${domain}`}
</main>
)
};
Client-side pages can also handle domain information, utilizing window.location.hostname
or window.location.host
wrapped in a state callback.
This can be useful if you're loading data or content from an API after an initial client-side render, based on the custom domain. However, note that this can be pretty easily spoofed, so don't rely on this alone for authentication or authorization.
Here's an example of using the window.location.hostname
on the client side to do something different when we detect a custom domain:
// Example repo file: pages/page-csr.ts
import { useEffect, useState } from 'react';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function Page() {
const [domain, setDomain] = useState<string>(String(process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN));
useEffect(() => {
// NOTE: consider the difference between `window.location.host` (includes port) and `window.location.hostname` (only host domain name)
const pageDomain = typeof window !== 'undefined' ? window.location.hostname : process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN;
setDomain(String(pageDomain));
}, []);
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
{domain === process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN ? 'Welcome to the primary domain' : `Welcome to the subdomain ${domain}`}
</main>
)
};
When using the app router, you can run server-side code in your components. This allows you to check request details at the component level, which can be useful for conditional rendering based on the request domain, especially when coupled with authentication.
API routes can perform similar functions to middleware, using the domain from the header in the rest of your code.
Here's an example of how you could use an API route to potentially do something different when a custom domain is detected.
// Example repo file: pages/api/host.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type ResponseData = {
message: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
/**
* Check if there's a header with the custom domain,
* and if not just use the host header.
* If you're using approximated.app the default is to
* inject the header 'apx-incoming-host' with the custom domain.
*/
const domain = req.headers['apx-incoming-host'] || req.headers.host || process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN;
// do something with the "domain"
res.status(200).json({ message: `Hello from ${domain}` });
};
The new app
directory allows the use of route handlers, which can access the domain from request headers, similar to other routing methods.
Here's an example of using the App Router route handlers to do something different for each custom domain:
// Example repo file: app/app-hosts/route.ts
import { type NextRequest } from 'next/server';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
/**
* Check if there's a header with the custom domain,
* and if not just use the host header.
* If you're using approximated.app the default is to
* inject the header 'apx-incoming-host' with the custom domain.
*/
const domain = request.headers.has('apx-incoming-host')
? request.headers.get('apx-incoming-host')
: request.headers.get('host');
return Response.json({ message: `Hello from ${domain}` });
};
Note: Not to be confused with the Pages Router, App Pages are something separate.
Server components in the app
directory can make use of server side functions help manage domain-specific logic.
Here's an example of using server side functions in App Pages to render based on the custom domain:
// Example repo file: app/ssr-page/page.tsx
import { headers } from 'next/headers';
export default function Page() {
/**
* Check if there's a header with the custom domain,
* and if not just use the host header.
* If you're using approximated.app the default is to
* inject the header 'apx-incoming-host' with the custom domain.
*/
const domain = headers().has('apx-incoming-host')
? headers().get('apx-incoming-host')
: headers().get('host');
return <h1>{domain === process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN ? 'Welcome to the primary domain' : `Welcome to the custom domain ${domain}`}</h1>
}
For client-side pages in the app
directory, window.location.hostname
or window.location.host
can be used as long as the page uses "use client"
.
Note that this is happening client-side, so you may be more limited in what you can do. This is often useful for small template changes, or calling an API to get data or content for a specific custom domain.
Here's an example of using window.location.hostname
to get the current domain and do something different:
// Example repo file: app/csr-page/page.tsx
'use client';
export default function Page() {
// NOTE: consider the difference between `window.location.host` (includes port) and `window.location.hostname` (only host domain name)
const domain = typeof window !== 'undefined' ? window.location.host : process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN;
return <h1>{domain === process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN ? 'Welcome to the primary domain' : `Welcome to the subdomain ${domain}`}</h1>
}
It is possible to pre-render pages that use a custom domain or otherwise render unique content based on a custom domain. With the right tools this can be quite easy with little or no app modification required.
In other cases this may require significant reimagining of your app architecture, and may need complex logic with frequent re-rendering.
This could include fetching the list of all custom domains, creating a specific .env
for each, running npm run build
, and deploying the static output.
This process would potentially need to be repeated for each client and whenever new custom domains are added.
Remember how we said above that building a system for managing SSL certificates at scale is difficult, expensive, and error prone? Well, we know that from personal (painful) experience.
It's why we built Approximated - so that you don't have to. And at a price you can afford, starting at $20/month instead of thousands.
Approximated is your fully managed solution for automating custom domains and SSL certificates. We stand up a dedicated reverse proxy cluster, with edge nodes globally distributed around the world, for each and every customer.
You get a dedicated IPv4 address with each cluster so that you have the option of allowing your users to point their apex domains. Custom domain traffic goes through the cluster node nearest your user and straight to your app.
The best part? Your cluster will provision and fully manage SSL certificates automatically along the way. No need to worry about them ever again.
All you need to do is call our simple API from your app when you want to create, read, update, or delete a custom domain and it will handle the rest.
So what does that look like in our example repo?
It can be as simple as an API endpoint like this:
// in pages/api/createVirtualHost.ts
import type { NextApiRequest, NextApiResponse } from 'next';
interface ApiResponse {
error?: string;
data?: any;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise {
if (req.method === 'POST') {
try {
const apiKey = process.env.APPROXIMATED_API_KEY; // Accessing the API key from environment variable
const primaryDomain = process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN;
const response = await fetch('https://cloud.approximated.app/api/vhosts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Api-Key': apiKey || '' // Using the API key from .env
},
body: JSON.stringify({
incoming_address: req.body.incoming_address,
target_address: primaryDomain || ''
})
});
const data = await response.json();
if (response.ok) {
res.status(200).json(data);
} else {
res.status(response.status).json({ error: data || 'Error creating virtual host' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to create virtual host due to an unexpected error' });
}
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end('Method Not Allowed');
}
}
In the example above, our endpoint is sending a POST request to the Approximated API with two fields: the custom domain (incoming_address) and the domain of our example app (target_address).
Here's a simple page with a form for adding a custom domain to Approximated, using the endpoint above. It also detects whether we're loading the page from the primary domain, or a custom domain.
// in pages/index.tsx
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next';
import { Inter } from 'next/font/google';
import { useState, FormEvent } from 'react';
const inter = Inter({ subsets: ['latin'] });
type ApproximatedPage = {
domain: string;
}
export const getServerSideProps = (async ({ req }) => {
/**
* Check if there's a header with the custom domain,
* and if not just use the host header.
* If you're using approximated.app the default is to
* inject the header 'apx-incoming-host' with the custom domain.
*/
const domain = req.headers['apx-incoming-host'] || req.headers.host || process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN;
// do something with the "domain"
return { props: { domain } }
}) as GetServerSideProps<ApproximatedPage>;
export default function Home({ domain }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<main
className={`flex min-h-screen flex-col items-center gap-8 p-24 ${inter.className}`}
>
{domain === process.env.NEXT_PUBLIC_APP_PRIMARY_DOMAIN ? 'Welcome to the primary domain' : `Welcome to the custom domain ${domain}`}
<DomainForm />
</main>
)
};
interface DomainFormData {
incoming_address: string;
}
const DomainForm: React.FC = () => {
const [incoming_address, setDomain] = useState<string>('');
const [errors, setErrors] = useState<object| null>(null); // State to hold response errors
const [success, setSuccess] = useState<string | null>(null); // State to hold success message
const [dnsMessage, setDnsMessage] = useState<string | null>(null); // State to hold DNS message
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
setErrors(null); // Reset errors on new submission
setSuccess(null); // Reset message on new submission
setDnsMessage(null); // Reset DNS message on new submission
const formData: DomainFormData = { incoming_address };
const response = await fetch('/api/createVirtualHost', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (!response.ok) {
console.log(data);
// Assuming the error message is in the `message` field
if(data.error === 'Unauthorized'){
setErrors({'unauthorized': 'Unauthorized - incorrect or missing Approximated API key.', 'help': 'Check your env variables for APPROXIMATED_API_KEY.'});
}else{
setErrors(data.error.errors || data.error || {'unknown': 'An unknown error occurred'});
}
return;
}
setSuccess(data.data.incoming_address) // Handle the response as needed
setDnsMessage(data.data.user_message)
};
return (
<form className="text-center" onSubmit={handleSubmit}>
<div className="mb-2">
<label htmlFor="incoming_address">Connect a custom domain</label>
</div>
<input
type="text"
id="incoming_address"
value={incoming_address}
onChange={(e) => setDomain(e.target.value)}
required
className="text-black text-left px-2 py-1 text-xs"
/>
<button className="ml-2 border border-white rounded-md px-2 py-1 text-xs" type="submit">Submit</button>
{dnsMessage && <div className="mt-4 mx-auto max-w-xl text-sm mb-2">{dnsMessage}</div>}
{success &&
<div className="mt-2 mx-auto max-w-4xl">Once the DNS for the custom domain is set, <a href={'https://'+success} target="_blank" className="text-green-600 underline">Click here to view it.</a>
</div>
}
{errors && Object.entries(errors).map(([key, message]) => (
<p key={key} className="text-red-500 mt-2">{message}</p>
))}
</form>
);
}
This is all it takes to fully automate custom domains with Approximated, and have reliable SSL certificate management at any scale!
Between these two files, we're able to submit a custom domain to an API endpoint which then calls the Approximated API. It returns either a successful response with DNS instructions, or display errors such as duplicate custom domains.
You'll likely want to do more than this in a real world scenario, depending on your app. Things like validation, or adding custom domains to a database can be implemented however you see fit. Approximated is agnostic to the rest of your app - it will happily coexist with whatever stack or implementation details you choose now and in the future.