Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions fern/products/dashboard/pages/domains.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ instances:
Log in to your domain registrar and add the DNS records shown in the Fern Dashboard. The specific records depend on your domain type (subdomain, subpath, or root domain).
</Step>

<Step title="Set up a reverse proxy (subpath only)">

Subpath hosting needs more than DNS — your infrastructure has to forward requests from the subpath to Fern's origin with the `x-fern-host` header set to your bare domain. Follow the [reverse proxy setup instructions](/learn/docs/preview-publish/reverse-proxy) for your provider (Cloudflare Workers, AWS CloudFront, Netlify, Vercel, Nginx, Akamai, or Caddy). Skip this step for subdomain or root domain hosting.
</Step>

<Step title="Verify the setup">

Once you've added the DNS records, return to the Fern Dashboard to verify your domain. SSL is automatically provisioned for your domain, but it may take a few minutes to propagate globally.
Expand Down
3 changes: 3 additions & 0 deletions fern/products/docs/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ navigation:
path: ./pages/preview-publish/publishing-your-docs.mdx
- page: Setting up your domain
path: ./pages/preview-publish/setting-up-your-domain.mdx
- page: Reverse proxy setup
path: ./pages/preview-publish/reverse-proxy.mdx
slug: reverse-proxy
- page: Multi-source docs
path: ./pages/preview-publish/multi-source.mdx
- section: Customization
Expand Down
302 changes: 302 additions & 0 deletions fern/products/docs/pages/preview-publish/reverse-proxy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
---
title: Reverse proxy setup
description: Configure a reverse proxy to serve Fern docs from a subpath on your domain, with provider-specific instructions for routing and caching.
---

When you host Fern docs on a [subpath](/learn/docs/preview-publish/setting-up-your-domain) like `mydomain.com/docs`, your infrastructure must proxy requests from that path to Fern's origin. Subdomain setups (`docs.mydomain.com`) use a CNAME record instead and don't require a reverse proxy.

## How it works

A working reverse proxy does two things:

1. **Routes requests** from your subpath to Fern's origin at `app.buildwithfern.com`, preserving the original path. The `x-fern-host` header tells Fern which docs site to serve.
2. **Disables caching** of HTML responses so visitors always receive the current deployment.

Every proxied request needs two headers:

| Header | Value | Purpose |
|---|---|---|
| `x-fern-host` | Your bare domain without the subpath (e.g. `mydomain.com`, not `mydomain.com/docs`) | Tells Fern which docs site to serve |
| `Host` | `app.buildwithfern.com` | Routes the request to Fern's origin |

Fern sets `Cache-Control: public, max-age=0, must-revalidate` on HTML responses, which most providers respect by default. If yours overrides origin cache headers or applies its own time-to-live, explicitly disable caching for the proxied path.

<Warning>
Don't cache HTML responses from Fern. Cached HTML references JavaScript and CSS from older deployments — those files no longer exist, so pages fail to load. Static assets (`/_next/static/*`) are served directly by Fern's CDN and don't pass through your proxy.
</Warning>

## Set up your provider


<AccordionGroup>
<Accordion title="Cloudflare Workers">

<Steps>
<Step title="Create the Worker">

Create a [Cloudflare Worker](https://developers.cloudflare.com/workers/) that intercepts requests to your docs subpath and proxies them to Fern:

```js
const SUBPATH = "/docs";
const FERN_HOST = "mydomain.com";

export default {
async fetch(request) {
const url = new URL(request.url);

if (
url.pathname !== SUBPATH &&
!url.pathname.startsWith(`${SUBPATH}/`)
) {
return new Response("Not found", { status: 404 });
}

const upstreamUrl = new URL(request.url);
upstreamUrl.protocol = "https:";
upstreamUrl.hostname = "app.buildwithfern.com";
upstreamUrl.port = "";

const proxiedRequest = new Request(upstreamUrl, request);
proxiedRequest.headers.set("x-fern-host", FERN_HOST);

return fetch(proxiedRequest);
},
};
```

Replace `SUBPATH` and `FERN_HOST` with your subpath and bare domain.
</Step>
<Step title="Attach the Worker to a route">

In the Cloudflare dashboard under **Workers Routes**, attach the Worker to a route pattern like `mydomain.com/docs/*`.
</Step>
<Step title="Check for conflicting cache rules">

The Worker forwards Fern's `Cache-Control` header, so caching works automatically. In the Cloudflare dashboard, confirm that no Page Rule or Cache Rule sets "Cache Level: Cache Everything" for `/docs*` — if one exists, remove it or override it with "Cache Level: Bypass."
</Step>
</Steps>
</Accordion>

<Accordion title="AWS CloudFront">

<Steps>
<Step title="Add a Fern origin">

Add an origin to your CloudFront distribution pointing to `app.buildwithfern.com` (HTTPS only, port 443). Add a custom origin header:

| Header name | Value |
|---|---|
| `x-fern-host` | `mydomain.com` |

CloudFront automatically sets the `Host` header to the origin domain.
</Step>
<Step title="Create a cache behavior for `/docs*`">

Set the behavior to:

- **Origin**: the Fern origin you just created
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] reported by reviewdog 🐶
[FernStyles.Hedges] Avoid hedge words and filler like 'just'. Prefer direct statements.

- **Cache policy**: `CachingDisabled` (AWS managed policy)
- **Origin request policy**: `AllViewer` (forwards all headers, query strings, and cookies)

To use a custom cache policy instead of `CachingDisabled`, set min, max, and default TTL to `0` and forward `Host` and `x-fern-host`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] reported by reviewdog 🐶
[FernStyles.Acronyms] 'TTL' has no definition.

</Step>
</Steps>

<Warning>
CloudFront ignores `CDN-Cache-Control` and `Surrogate-Control` — only the standard `Cache-Control` header is read. A custom cache policy with a non-zero default TTL caches responses regardless of Fern's `Cache-Control: max-age=0` directive.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] reported by reviewdog 🐶
[FernStyles.Acronyms] 'TTL' has no definition.

</Warning>
</Accordion>

<Accordion title="Netlify">

<Steps>
<Step title="Add a rewrite rule">

In `netlify.toml` (or `_redirects`):

```toml netlify.toml
[[redirects]]
from = "/docs/*"
to = "https://app.buildwithfern.com/docs/:splat"
status = 200
force = true

[redirects.headers]
x-fern-host = "mydomain.com"
```
</Step>
</Steps>

<Note>
Netlify rewrites with `status = 200` act as a reverse proxy — the visitor's browser sees your domain while the content comes from Fern. Netlify respects the origin's `Cache-Control` header, so no extra caching configuration is needed.
</Note>
</Accordion>

<Accordion title="Vercel">

<Steps>
<Step title="Configure rewrites">

In `vercel.json`:

```json vercel.json
{
"rewrites": [
{
"source": "/docs",
"destination": "https://app.buildwithfern.com/docs",
"has": [
{
"type": "header",
"key": "x-fern-host",
"value": "mydomain.com"
}
]
},
{
"source": "/docs/:path*",
"destination": "https://app.buildwithfern.com/docs/:path*",
"has": [
{
"type": "header",
"key": "x-fern-host",
"value": "mydomain.com"
}
]
}
]
}
```

For Next.js, use `next.config.js` rewrites instead:

```js next.config.js
module.exports = {
async rewrites() {
return [
{
source: "/docs",
destination: "https://app.buildwithfern.com/docs",
},
{
source: "/docs/:path*",
destination: "https://app.buildwithfern.com/docs/:path*",
},
];
},
};
```
</Step>
<Step title="Pass `x-fern-host` via middleware (Next.js only)">

Vercel forwards the `Host` header automatically, but Next.js rewrites don't propagate `x-fern-host` from the rewrite config. Add a [middleware function](https://vercel.com/docs/functions/middleware) that sets `x-fern-host` on requests under `/docs`.
</Step>
</Steps>

<Note>
Vercel respects the origin's `Cache-Control` header for rewrite destinations, so no extra caching configuration is needed.
</Note>
</Accordion>

<Accordion title="Nginx">

<Steps>
<Step title="Add a `location` block">

```nginx
location /docs {
proxy_pass https://app.buildwithfern.com;
proxy_set_header Host app.buildwithfern.com;
proxy_set_header x-fern-host mydomain.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;

# Prevent encoding issues with Brotli responses
proxy_set_header Accept-Encoding "gzip, deflate";

# Do not cache HTML
proxy_no_cache 1;
proxy_cache_bypass 1;
add_header Cache-Control "public, max-age=0, must-revalidate" always;
}
```
</Step>
</Steps>

<Warning>
Nginx doesn't natively support [Brotli](https://github.com/google/brotli) decompression. The `Accept-Encoding` override above prevents HTTP/2 transfer errors caused by Fern's default Brotli-compressed responses.
</Warning>
</Accordion>

<Accordion title="Akamai">

<Steps>
<Step title="Add a routing rule">

In a **Site Delivery** or **Ion** property, add a rule matching path `/docs*`:

- **Origin Server**: `app.buildwithfern.com`
- **Forward Host Header**: Origin Hostname

Add a **Modify Outgoing Request Header** behavior:

| Action | Header name | Value |
|---|---|---|
| Add | `x-fern-host` | `mydomain.com` |
</Step>
<Step title="Disable caching on the rule">

Add a **Caching** behavior to the same rule:

| Setting | Value |
|---|---|
| Caching Option | No Store |

Alternatively, set **Honor Origin Cache-Control** to **Yes** so Akamai respects Fern's `Cache-Control: public, max-age=0, must-revalidate` header.
</Step>
</Steps>
</Accordion>

<Accordion title="Caddy">

<Steps>
<Step title="Add to your Caddyfile">

```caddyfile
mydomain.com {
handle_path /docs* {
reverse_proxy https://app.buildwithfern.com {
header_up Host app.buildwithfern.com
header_up x-fern-host mydomain.com
}
}
}
```
</Step>
</Steps>

<Note>
Caddy doesn't cache responses by default, so no extra caching configuration is needed unless you've explicitly enabled caching with a `cache` directive or external module.
</Note>
</Accordion>
</AccordionGroup>

## Verify your setup

Run these checks against your live subpath:

```bash
# Check that the page loads and x-fern-host is recognized
curl -sI https://mydomain.com/docs | head -20

# Verify Cache-Control on the HTML response
curl -sI https://mydomain.com/docs | grep -i cache-control
# Expected: cache-control: public, max-age=0, must-revalidate

# Verify the page is not cached by your proxy (age should be 0 or absent)
curl -sI https://mydomain.com/docs | grep -i "^age:"
```

If the `age` header is present and non-zero, your proxy is serving a cached response. Revisit your provider's configuration to ensure HTML caching is disabled.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Once Fern has completed your setup, you'll be able to access your documentation

<Markdown src="/snippets/team-plan.mdx"/>

To host your documentation on a subpath like `mydomain.com/docs`, you need to edit your `docs.yml` configuration and then get provider-specific instructions for setting up the subpath. Common providers include Cloudflare, AWS Route53 and Cloudfront, Netlify, and Vercel.
To host your documentation on a subpath like `mydomain.com/docs`, you need to edit your `docs.yml` configuration and set up a reverse proxy on your infrastructure.

<Steps>
<Step title="Configure the `url` in `docs.yml`">
Expand Down Expand Up @@ -108,6 +108,11 @@ instances:
[Here's an example.](https://github.com/fern-api/fern/blob/7d8631c6119787a8aaccb4ba49837e73c985db28/fern/docs.yml#L1-L3)
</Step>

<Step title="Set up a reverse proxy">

A subpath can't be routed with DNS alone — your infrastructure has to forward requests from `mydomain.com/docs` to Fern's origin with the `x-fern-host` header set to your bare domain. Follow the [reverse proxy setup instructions](/learn/docs/preview-publish/reverse-proxy) for your provider (Cloudflare Workers, AWS CloudFront, Netlify, Vercel, Nginx, Akamai, or Caddy).
</Step>

<Step title="Contact Fern">

Contact Fern via your dedicated Slack channel or [email](mailto:support@buildwithfern.com) to set up your custom subpath.
Expand All @@ -133,7 +138,7 @@ proxy_set_header Accept-Encoding "gzip,deflate";
</Accordion>
<Accordion title="Root domain">

To host your documentation on a root domain like `mydomain.com`, you need to edit your `docs.yml` configuration and then get provider-specific instructions for setting up the domain. Common providers include Cloudflare, AWS Route53 and Cloudfront, Netlify, and Vercel.
To host your documentation on a root domain like `mydomain.com`, you need to edit your `docs.yml` configuration and configure DNS records.

<Steps>
<Step title="Configure the `url` in `docs.yml`">
Expand Down
Loading