Skip to content

Commit 189e7bd

Browse files
thesandlorddevin-ai-integration[bot]kafkasdevalog
authored
feat(docs): add reverse proxy setup guide with provider-specific instructions (#5601)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Anar Kafkas <anarkafkas@gmail.com> Co-authored-by: Devin Logan <devinannlogan@gmail.com>
1 parent be0e253 commit 189e7bd

4 files changed

Lines changed: 285 additions & 2 deletions

File tree

fern/products/dashboard/pages/domains.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ instances:
4646
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).
4747
</Step>
4848
49+
<Step title="Set up a reverse proxy (subpath only)">
50+
51+
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.
52+
</Step>
53+
4954
<Step title="Verify the setup">
5055

5156
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.

fern/products/docs/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ navigation:
248248
path: ./pages/preview-publish/publishing-your-docs.mdx
249249
- page: Setting up your domain
250250
path: ./pages/preview-publish/setting-up-your-domain.mdx
251+
- page: Reverse proxy setup
252+
path: ./pages/preview-publish/reverse-proxy.mdx
253+
slug: reverse-proxy
251254
- page: Multi-source docs
252255
path: ./pages/preview-publish/multi-source.mdx
253256
- section: Customization
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
---
2+
title: Reverse proxy setup
3+
description: Configure a reverse proxy to serve Fern docs from a subpath on your domain, with provider-specific instructions for routing and caching.
4+
---
5+
6+
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.
7+
8+
## How it works
9+
10+
A working reverse proxy does two things:
11+
12+
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.
13+
2. **Disables caching** of HTML responses so visitors always receive the current deployment.
14+
15+
Every proxied request needs two headers:
16+
17+
| Header | Value | Purpose |
18+
|---|---|---|
19+
| `x-fern-host` | Your bare domain without the subpath (e.g. `mydomain.com`, not `mydomain.com/docs`) | Tells Fern which docs site to serve |
20+
| `Host` | `app.buildwithfern.com` | Routes the request to Fern's origin |
21+
22+
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.
23+
24+
<Warning>
25+
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.
26+
</Warning>
27+
28+
## Set up your provider
29+
30+
31+
<AccordionGroup>
32+
<Accordion title="Cloudflare Workers">
33+
34+
<Steps>
35+
<Step title="Create the Worker">
36+
37+
Create a [Cloudflare Worker](https://developers.cloudflare.com/workers/) that intercepts requests to your docs subpath and proxies them to Fern:
38+
39+
```js
40+
const SUBPATH = "/docs";
41+
const FERN_HOST = "mydomain.com";
42+
43+
export default {
44+
async fetch(request) {
45+
const url = new URL(request.url);
46+
47+
if (
48+
url.pathname !== SUBPATH &&
49+
!url.pathname.startsWith(`${SUBPATH}/`)
50+
) {
51+
return new Response("Not found", { status: 404 });
52+
}
53+
54+
const upstreamUrl = new URL(request.url);
55+
upstreamUrl.protocol = "https:";
56+
upstreamUrl.hostname = "app.buildwithfern.com";
57+
upstreamUrl.port = "";
58+
59+
const proxiedRequest = new Request(upstreamUrl, request);
60+
proxiedRequest.headers.set("x-fern-host", FERN_HOST);
61+
62+
return fetch(proxiedRequest);
63+
},
64+
};
65+
```
66+
67+
Replace `SUBPATH` and `FERN_HOST` with your subpath and bare domain.
68+
</Step>
69+
<Step title="Attach the Worker to a route">
70+
71+
In the Cloudflare dashboard under **Workers Routes**, attach the Worker to a route pattern like `mydomain.com/docs/*`.
72+
</Step>
73+
<Step title="Check for conflicting cache rules">
74+
75+
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."
76+
</Step>
77+
</Steps>
78+
</Accordion>
79+
80+
<Accordion title="AWS CloudFront">
81+
82+
<Steps>
83+
<Step title="Add a Fern origin">
84+
85+
Add an origin to your CloudFront distribution pointing to `app.buildwithfern.com` (HTTPS only, port 443). Add a custom origin header:
86+
87+
| Header name | Value |
88+
|---|---|
89+
| `x-fern-host` | `mydomain.com` |
90+
91+
CloudFront automatically sets the `Host` header to the origin domain.
92+
</Step>
93+
<Step title="Create a cache behavior for `/docs*`">
94+
95+
Set the behavior to:
96+
97+
- **Origin**: the Fern origin you just created
98+
- **Cache policy**: `CachingDisabled` (AWS managed policy)
99+
- **Origin request policy**: `AllViewer` (forwards all headers, query strings, and cookies)
100+
101+
To use a custom cache policy instead of `CachingDisabled`, set min, max, and default TTL to `0` and forward `Host` and `x-fern-host`.
102+
</Step>
103+
</Steps>
104+
105+
<Warning>
106+
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.
107+
</Warning>
108+
</Accordion>
109+
110+
<Accordion title="Netlify">
111+
112+
<Steps>
113+
<Step title="Add a rewrite rule">
114+
115+
In `netlify.toml` (or `_redirects`):
116+
117+
```toml netlify.toml
118+
[[redirects]]
119+
from = "/docs/*"
120+
to = "https://app.buildwithfern.com/docs/:splat"
121+
status = 200
122+
force = true
123+
124+
[redirects.headers]
125+
x-fern-host = "mydomain.com"
126+
```
127+
</Step>
128+
</Steps>
129+
130+
<Note>
131+
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.
132+
</Note>
133+
</Accordion>
134+
135+
<Accordion title="Vercel">
136+
137+
<Steps>
138+
<Step title="Add a route with a transform">
139+
140+
Use a [route with a transform rule](https://vercel.com/changelog/transform-rules-are-now-available-in-vercel-json) to rewrite the path and set `x-fern-host` in one step:
141+
142+
```json vercel.json
143+
{
144+
"$schema": "https://openapi.vercel.sh/vercel.json",
145+
"routes": [
146+
{
147+
"src": "/docs(/.*)?",
148+
"dest": "https://app.buildwithfern.com/docs$1",
149+
"transforms": [
150+
{
151+
"type": "request.headers",
152+
"op": "set",
153+
"target": { "key": "x-fern-host" },
154+
"args": "mydomain.com"
155+
}
156+
]
157+
}
158+
]
159+
}
160+
```
161+
</Step>
162+
</Steps>
163+
164+
<Note>
165+
Vercel respects the origin's `Cache-Control` header for rewrite destinations, so no extra caching configuration is needed.
166+
</Note>
167+
</Accordion>
168+
169+
<Accordion title="Nginx">
170+
171+
<Steps>
172+
<Step title="Add a `location` block">
173+
174+
```nginx
175+
location /docs {
176+
proxy_pass https://app.buildwithfern.com;
177+
proxy_set_header Host app.buildwithfern.com;
178+
proxy_set_header x-fern-host mydomain.com;
179+
proxy_set_header X-Real-IP $remote_addr;
180+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
181+
proxy_set_header X-Forwarded-Proto $scheme;
182+
proxy_ssl_server_name on;
183+
184+
# Prevent encoding issues with Brotli responses
185+
proxy_set_header Accept-Encoding "gzip, deflate";
186+
187+
# Do not cache HTML
188+
proxy_no_cache 1;
189+
proxy_cache_bypass 1;
190+
add_header Cache-Control "public, max-age=0, must-revalidate" always;
191+
}
192+
```
193+
</Step>
194+
</Steps>
195+
196+
<Warning>
197+
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.
198+
</Warning>
199+
</Accordion>
200+
201+
<Accordion title="Akamai">
202+
203+
<Steps>
204+
<Step title="Add a routing rule">
205+
206+
In a **Site Delivery** or **Ion** property, add a rule matching path `/docs*`:
207+
208+
- **Origin Server**: `app.buildwithfern.com`
209+
- **Forward Host Header**: Origin Hostname
210+
211+
Add a **Modify Outgoing Request Header** behavior:
212+
213+
| Action | Header name | Value |
214+
|---|---|---|
215+
| Add | `x-fern-host` | `mydomain.com` |
216+
</Step>
217+
<Step title="Disable caching on the rule">
218+
219+
Add a **Caching** behavior to the same rule:
220+
221+
| Setting | Value |
222+
|---|---|
223+
| Caching Option | No Store |
224+
225+
Alternatively, set **Honor Origin Cache-Control** to **Yes** so Akamai respects Fern's `Cache-Control: public, max-age=0, must-revalidate` header.
226+
</Step>
227+
</Steps>
228+
</Accordion>
229+
230+
<Accordion title="Caddy">
231+
232+
<Steps>
233+
<Step title="Add to your Caddyfile">
234+
235+
```caddyfile
236+
mydomain.com {
237+
handle /docs* {
238+
reverse_proxy https://app.buildwithfern.com {
239+
header_up Host app.buildwithfern.com
240+
header_up x-fern-host mydomain.com
241+
}
242+
}
243+
}
244+
```
245+
</Step>
246+
</Steps>
247+
248+
<Note>
249+
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.
250+
</Note>
251+
</Accordion>
252+
</AccordionGroup>
253+
254+
## Verify your setup
255+
256+
Run these checks against your live subpath:
257+
258+
```bash
259+
# Check that the page loads and x-fern-host is recognized
260+
curl -sI https://mydomain.com/docs | head -20
261+
262+
# Verify Cache-Control on the HTML response
263+
curl -sI https://mydomain.com/docs | grep -i cache-control
264+
# Expected: cache-control: public, max-age=0, must-revalidate
265+
266+
# Verify the page is not cached by your proxy (age should be 0 or absent)
267+
curl -sI https://mydomain.com/docs | grep -i "^age:"
268+
```
269+
270+
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.

fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Once Fern has completed your setup, you'll be able to access your documentation
7878

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

81-
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.
81+
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.
8282

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

111+
<Step title="Set up a reverse proxy">
112+
113+
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).
114+
</Step>
115+
111116
<Step title="Contact Fern">
112117

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

136-
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.
141+
To host your documentation on a root domain like `mydomain.com`, you need to edit your `docs.yml` configuration and configure DNS records.
137142

138143
<Steps>
139144
<Step title="Configure the `url` in `docs.yml`">

0 commit comments

Comments
 (0)