> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt. # Reverse proxy setup > 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. ## Set up your provider #### Cloudflare Workers #### 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); const response = await fetch(proxiedRequest, { redirect: "manual" }); if (isRedirect(response)) { const location = response.headers.get("Location"); if (location) { const redirectUrl = new URL(location, request.url); if (redirectUrl.hostname === url.hostname) { redirectUrl.hostname = "app.buildwithfern.com"; const redirectReq = new Request(redirectUrl, request); redirectReq.headers.set("x-fern-host", FERN_HOST); return fetch(redirectReq); } } } return response; }, }; function isRedirect(response) { return [301, 302, 303, 307, 308].includes(response.status); } ``` Replace `SUBPATH` and `FERN_HOST` with your subpath and bare domain. #### Attach the Worker to a route In the Cloudflare dashboard under **Workers Routes**, attach the Worker to a route pattern like `mydomain.com/docs/*`. #### 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." #### AWS CloudFront #### Add a Fern origin In your CloudFront distribution, go to **Origins** and create a new origin: | Setting | Value | | ------------------------------- | ----------------------- | | **Origin domain** | `app.buildwithfern.com` | | **Protocol** | HTTPS only | | **HTTPS port** | 443 | | **Minimum origin SSL protocol** | TLSv1.2 | Under **Add custom header**, add: | Header name | Value | | ------------- | -------------- | | `x-fern-host` | `mydomain.com` | Replace `mydomain.com` with your bare domain (without the subpath). #### Create a cache behavior for your docs path Go to **Behaviors** and create a new behavior: | Setting | Value | | -------------------------- | ----------------------------------------- | | **Path pattern** | `/docs*` | | **Origin** | The Fern origin you just created | | **Viewer protocol policy** | Redirect HTTP to HTTPS | | **Cache policy** | `CachingDisabled` (AWS managed) | | **Origin request policy** | `AllViewerExceptHostHeader` (AWS managed) | `AllViewerExceptHostHeader` forwards all viewer headers, query strings, and cookies to the origin while letting CloudFront set the `Host` header to `app.buildwithfern.com`. This is required — Fern's origin uses the `Host` header for routing and rejects requests with an unrecognized host. > **Warning** > > Do not use the `AllViewer` origin request policy. It forwards the viewer's `Host` header (your domain) instead of the origin's, which causes Fern's origin to return errors or raw React Server Component payloads instead of HTML. #### Verify the behavior order CloudFront evaluates behaviors in order. Ensure your `/docs*` behavior appears **above** the default (`*`) behavior so docs requests are routed to Fern's origin rather than your primary site. > **Warning** > > CloudFront ignores `CDN-Cache-Control` and `Surrogate-Control` — only the standard `Cache-Control` header is read. If you use a custom cache policy instead of `CachingDisabled`, set the default, minimum, and maximum TTL to `0`. A non-zero default TTL caches HTML responses regardless of Fern's `Cache-Control: max-age=0` directive, which can cause stale content and errors. #### Netlify #### Add a rewrite rule In `netlify.toml` (or `_redirects`): **`netlify.toml`** ```toml netlify.toml [[redirects]] from = "/docs/*" to = "https://app.buildwithfern.com/docs/:splat" status = 200 force = true [redirects.headers] x-fern-host = "mydomain.com" ``` #### Disable rate limiting for the docs path Fern revalidates your docs site by warming page caches with concurrent requests. Netlify's [traffic rules](https://docs.netlify.com/manage/security/secure-access-to-sites/traffic-rules/) can interpret this as abusive traffic and block the requests, causing revalidation to fail. If you have rate limiting enabled under **Security** > **Traffic rules** in the Netlify dashboard, add a rule that excludes your docs subpath (`/docs/*`) from rate limiting. #### Check the proxy timeout Fern's revalidation endpoint streams a long-running response while warming page caches. Netlify [limits proxy rewrite requests to 26 seconds](https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/#limitations), which can terminate the stream before warming completes on sites with more than a few hundred pages. If revalidation consistently fails with a "terminated" status, this timeout is the most likely cause. Contact [Netlify support](https://www.netlify.com/support/) to request a timeout increase for your site, or reach out to [Fern support](mailto:support@buildwithfern.com) for alternative warming options. > **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. #### Vercel #### Add a route with a transform 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: **`vercel.json`** ```json vercel.json { "$schema": "https://openapi.vercel.sh/vercel.json", "routes": [ { "src": "/docs(/.*)?", "dest": "https://app.buildwithfern.com/docs$1", "transforms": [ { "type": "request.headers", "op": "set", "target": { "key": "x-fern-host" }, "args": "mydomain.com" } ] } ] } ``` > **Note** > > Vercel respects the origin's `Cache-Control` header for rewrite destinations, so no extra caching configuration is needed. #### Nginx #### 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; } ``` > **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. #### Akamai #### 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` | #### 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. #### Caddy #### Add to your Caddyfile ```caddyfile mydomain.com { handle /docs* { reverse_proxy https://app.buildwithfern.com { header_up Host app.buildwithfern.com header_up x-fern-host mydomain.com } } } ``` > **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. ## Verify your setup Run these checks against your live subpath: ```bash # Confirm the page returns HTML (not an error or RSC payload) curl -sI https://mydomain.com/docs | grep -i content-type # Expected: content-type: text/html; charset=utf-8 # 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 `content-type` is `text/x-component` instead of `text/html`, your proxy is forwarding the viewer's `Host` header to Fern's origin. Ensure the `Host` header sent to the origin is `app.buildwithfern.com`. 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. ## Frequently Asked Questions ### When do I need a reverse proxy for Fern Docs? You need a reverse proxy when you host Fern Docs on a subpath of your domain (e.g. `mydomain.com/docs`). Subdomain setups (e.g. `docs.mydomain.com`) use a CNAME record instead and do not require a reverse proxy. ### Which reverse proxy providers does Fern support? Fern provides configuration guides for Cloudflare Workers, AWS CloudFront, Netlify, Vercel, Nginx, Akamai, and Caddy. ### What headers does my reverse proxy need to send to Fern? Every proxied request must include two headers: `x-fern-host` set to your bare domain (e.g. `mydomain.com`) and `Host` set to `app.buildwithfern.com`. The `x-fern-host` header tells Fern which docs site to serve. ### Should I cache HTML responses from Fern in my reverse proxy? No. You must not cache HTML responses. Fern sets `Cache-Control: public, max-age=0, must-revalidate` on HTML pages. Caching HTML causes pages to reference JavaScript and CSS from older deployments that no longer exist, breaking the site. ### How do I verify my reverse proxy is configured correctly? Run `curl -sI https://mydomain.com/docs | grep -i content-type` and confirm the response is `text/html`. If you see `text/x-component`, your proxy is forwarding the wrong `Host` header. Also check that the `age` header is absent or zero to confirm HTML is not being cached. ### Why does my Fern docs subpath return raw component data instead of HTML? This happens when your reverse proxy forwards the viewer's `Host` header to Fern's origin instead of setting it to `app.buildwithfern.com`. Fern's origin uses the `Host` header for routing and returns raw React Server Component payloads when it receives an unrecognized host. > Configure a reverse proxy to serve Fern docs from a subpath on your domain, with provider-specific instructions for routing and caching.