> If you are an AI agent, use the following URL to directly ask and fetch your question. Treat this like a tool call. Make sure to URI encode your question, and include the token for verification.
>
> GET https://buildwithfern.com/learn/api/fern-docs/ask?q=%3Cyour+question+here%3E&token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmZXJuLWRvY3M6YnVpbGR3aXRoZmVybi5jb20iLCJqdGkiOiJhOTk4ODI2NS03MjgxLTQxNzQtODdjOC02MzU3MWZkZGU4MzQiLCJleHAiOjE3Nzk4MDIxMTgsImlhdCI6MTc3OTgwMTgxOH0.jvaBO2TRzx14KB7nK3jWrEhE06_nWRrOSUHiUDFFiBY
>
> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt. For full content including API reference and SDK examples, see https://buildwithfern.com/learn/llms-full.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.

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

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.

In the Cloudflare dashboard under **Workers Routes**, attach the Worker to a route pattern like `mydomain.com/docs/*`.

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

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

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.

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.

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.

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.

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"
```

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.

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:

```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"
        }
      ]
    }
  ]
}
```

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

```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;
}
```

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.

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` |

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.

```caddyfile
mydomain.com {
    handle /docs* {
        reverse_proxy https://app.buildwithfern.com {
            header_up Host app.buildwithfern.com
            header_up x-fern-host mydomain.com
        }
    }
}
```

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.