From 2f793a207b874d0a79e785761b0a6d4e60ae8435 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:36:20 +0000 Subject: [PATCH 1/6] feat(docs): add reverse proxy setup guide with provider-specific instructions Co-Authored-By: Sandeep Dinesh --- fern/products/docs/docs.yml | 2 + .../pages/preview-publish/reverse-proxy.mdx | 322 ++++++++++++++++++ .../setting-up-your-domain.mdx | 4 +- 3 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 fern/products/docs/pages/preview-publish/reverse-proxy.mdx diff --git a/fern/products/docs/docs.yml b/fern/products/docs/docs.yml index 41cd6ddac..b08cd8e26 100644 --- a/fern/products/docs/docs.yml +++ b/fern/products/docs/docs.yml @@ -248,6 +248,8 @@ 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 - page: Multi-source docs path: ./pages/preview-publish/multi-source.mdx - section: Customization diff --git a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx new file mode 100644 index 000000000..f8e4ad49a --- /dev/null +++ b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx @@ -0,0 +1,322 @@ +--- +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. + +A working reverse proxy does two things: + +1. **Routes requests** from your subpath to Fern's origin, with the `x-fern-host` header set so Fern identifies your site. +2. **Disables caching** of HTML responses so visitors always receive the latest deployment. + +## Routing + +Your proxy must forward all requests under your docs subpath to Fern's origin at `app.buildwithfern.com`, preserving the original path. Set two headers on every proxied request: + +| Header | Value | Purpose | +|--------|-------|---------| +| `x-fern-host` | Your bare domain (e.g. `mydomain.com`) | Tells Fern which docs site to serve | +| `Host` | `app.buildwithfern.com` | Routes the request to Fern's origin | + + +The `x-fern-host` value is the bare domain without the subpath. For `mydomain.com/docs`, set `x-fern-host: mydomain.com` — not `mydomain.com/docs`. + + +### Provider-specific routing + + + + +Create a [Cloudflare Worker](https://developers.cloudflare.com/workers/) that intercepts requests to your docs subpath and proxies them to Fern. + +```js +const FERN_ORIGIN = "https://app.buildwithfern.com"; + +export default { + async fetch(request) { + const url = new URL(request.url); + + // Only proxy requests under /docs + if (!url.pathname.startsWith("/docs")) { + return fetch(request); + } + + const targetUrl = new URL(url.pathname + url.search, FERN_ORIGIN); + + const headers = new Headers(request.headers); + headers.set("x-fern-host", url.hostname); + headers.set("Host", "app.buildwithfern.com"); + + return fetch(targetUrl.toString(), { + method: request.method, + headers, + body: request.body, + redirect: "manual", + }); + }, +}; +``` + +Attach the Worker to a route pattern like `mydomain.com/docs/*` in the Cloudflare dashboard under **Workers Routes**. + + + + +Add a second origin and behavior to your CloudFront distribution. + +**1. Create an origin** pointing to `app.buildwithfern.com` (HTTPS only, port 443). + +**2. Add a custom origin header:** + +| Header name | Value | +|---|---| +| `x-fern-host` | `mydomain.com` | + +CloudFront automatically sets the `Host` header to the origin domain. + +**3. Create a cache behavior** for the path pattern `/docs*`: + +- Origin: the Fern origin created above +- Cache policy: `CachingDisabled` (AWS managed policy) +- Origin request policy: `AllViewer` (forwards all headers, query strings, and cookies) + +If you need fine-grained control instead of `CachingDisabled`, create a custom cache policy with TTL values set to `0` and forward the `Host` and `x-fern-host` headers. + + + + +Add a rewrite rule in your `netlify.toml` or `_redirects` file. Netlify rewrites proxy the request server-side and return the upstream response directly. + +```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. + + + + + +Configure rewrites in your `vercel.json` (or `next.config.js` if using Next.js). Vercel rewrites proxy the request and serve the response from your domain. + +```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*", + }, + ]; + }, +}; +``` + + +Vercel rewrites automatically forward the `Host` header. To pass `x-fern-host`, you may need a middleware function. See Vercel's [middleware documentation](https://vercel.com/docs/functions/middleware) for details. + + + + + +Add a `location` block that proxies your docs subpath to Fern. + +```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"; +} +``` + + +Nginx does not 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. + + + + + +Configure a **Site Delivery** or **Ion** property to proxy your docs subpath. + +**1. Add a rule** matching the path `/docs*`: + +- **Origin Server**: `app.buildwithfern.com` +- **Forward Host Header**: Origin Hostname + +**2. Add a Modify Outgoing Request Header behavior** to set: + +| Action | Header name | Value | +|---|---|---| +| Add | `x-fern-host` | `mydomain.com` | + +**3. Set the caching behavior** (see [Caching](#caching) below). + + + + +```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 + } + } +} +``` + + + + +## Caching + +Fern sets `Cache-Control: public, max-age=0, must-revalidate` on HTML responses, which tells your proxy not to cache page content. Most providers respect this header by default. However, if your provider overrides origin cache headers or applies a default TTL, you must explicitly disable caching for the proxied path. + +Caching stale HTML causes visitors to load a page that references JavaScript and CSS files from an older deployment. Those files no longer exist, so the page fails to hydrate and may display an error. + + +Do not cache HTML responses from Fern. Static assets (`/_next/static/*`) are served directly by Fern's CDN with long-lived cache headers and do not pass through your proxy. + + +### Provider-specific caching + + + + +Cloudflare respects the origin's `Cache-Control` header by default for proxied (orange-cloud) records. No additional configuration is needed unless you have **Page Rules** or **Cache Rules** that override caching for your domain. + +To verify, check that no Page Rule sets "Cache Level: Cache Everything" for the `/docs*` path. If one exists, either remove it or add a more specific rule for `/docs*` with "Cache Level: Bypass." + +For Workers, the example in the [Routing](#routing) section already passes through the origin's headers without modification. + + + + +Use the AWS managed `CachingDisabled` cache policy on the `/docs*` behavior. This policy sets `min-TTL`, `max-TTL`, and `default-TTL` to `0`, so CloudFront always fetches a fresh response from Fern. + +If you use a custom cache policy instead, set all TTL values to `0`: + +| Setting | Value | +|---|---| +| Minimum TTL | `0` | +| Maximum TTL | `0` | +| Default TTL | `0` | + + +CloudFront ignores `CDN-Cache-Control` and `Surrogate-Control` headers. It only reads the standard `Cache-Control` header. A custom cache policy with a non-zero `default-TTL` will cache responses regardless of the origin's `Cache-Control: max-age=0` directive. + + + + + +Netlify respects the origin's `Cache-Control` header for rewrite-proxied responses. No additional configuration is needed. + + + + +Vercel respects the origin's `Cache-Control` header for rewrite destinations. No additional configuration is needed. + + + + +Add cache-control directives to the `/docs` location block to prevent Nginx from caching responses (or from being cached by downstream proxies that ignore the origin header): + +```nginx +location /docs { + proxy_pass https://app.buildwithfern.com; + # ... other proxy_set_header directives ... + + # Do not cache HTML + proxy_no_cache 1; + proxy_cache_bypass 1; + add_header Cache-Control "public, max-age=0, must-revalidate" always; +} +``` + + + + +In the rule matching `/docs*`, add a **Caching** behavior: + +| 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 does not cache responses by default. No additional configuration is needed unless you have explicitly enabled caching with a `cache` directive or external module. + + + + +## Verify your setup + +After configuring your proxy, confirm that requests are routed correctly and responses are not cached. + +```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. Review the [Caching](#caching) section for your provider. diff --git a/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx b/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx index fca3d3fe1..24cb1b478 100644 --- a/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx +++ b/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx @@ -78,7 +78,7 @@ Once Fern has completed your setup, you'll be able to access your documentation -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](/learn/docs/preview-publish/reverse-proxy) that routes requests to Fern. @@ -133,7 +133,7 @@ proxy_set_header Accept-Encoding "gzip,deflate"; -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. From ff6b8a9c5df3defbf8a1b01ab1c4b91a29c1382d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:43:07 +0000 Subject: [PATCH 2/6] fix(docs): address vale linting feedback on reverse proxy page Co-Authored-By: Sandeep Dinesh --- .../pages/preview-publish/reverse-proxy.mdx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx index f8e4ad49a..53d40207c 100644 --- a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx +++ b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx @@ -8,7 +8,7 @@ When you host Fern docs on a [subpath](/learn/docs/preview-publish/setting-up-yo A working reverse proxy does two things: 1. **Routes requests** from your subpath to Fern's origin, with the `x-fern-host` header set so Fern identifies your site. -2. **Disables caching** of HTML responses so visitors always receive the latest deployment. +2. **Disables caching** of HTML responses so visitors always receive the current deployment. ## Routing @@ -81,7 +81,7 @@ CloudFront automatically sets the `Host` header to the origin domain. - Cache policy: `CachingDisabled` (AWS managed policy) - Origin request policy: `AllViewer` (forwards all headers, query strings, and cookies) -If you need fine-grained control instead of `CachingDisabled`, create a custom cache policy with TTL values set to `0` and forward the `Host` and `x-fern-host` headers. +If you need fine-grained control instead of `CachingDisabled`, create a custom cache policy with time-to-live (TTL) values set to `0` and forward the `Host` and `x-fern-host` headers. @@ -181,7 +181,7 @@ location /docs { ``` -Nginx does not 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. +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. @@ -221,12 +221,12 @@ mydomain.com { ## Caching -Fern sets `Cache-Control: public, max-age=0, must-revalidate` on HTML responses, which tells your proxy not to cache page content. Most providers respect this header by default. However, if your provider overrides origin cache headers or applies a default TTL, you must explicitly disable caching for the proxied path. +Fern sets `Cache-Control: public, max-age=0, must-revalidate` on HTML responses, which tells your proxy not to cache page content. Most providers respect this header by default. However, if your provider overrides origin cache headers or applies its own time-to-live, you must explicitly disable caching for the proxied path. Caching stale HTML causes visitors to load a page that references JavaScript and CSS files from an older deployment. Those files no longer exist, so the page fails to hydrate and may display an error. -Do not cache HTML responses from Fern. Static assets (`/_next/static/*`) are served directly by Fern's CDN with long-lived cache headers and do not pass through your proxy. +Don't cache HTML responses from Fern. Static assets (`/_next/static/*`) are served directly by Fern's CDN with long-lived cache headers and don't pass through your proxy. ### Provider-specific caching @@ -245,16 +245,16 @@ For Workers, the example in the [Routing](#routing) section already passes throu Use the AWS managed `CachingDisabled` cache policy on the `/docs*` behavior. This policy sets `min-TTL`, `max-TTL`, and `default-TTL` to `0`, so CloudFront always fetches a fresh response from Fern. -If you use a custom cache policy instead, set all TTL values to `0`: +If you use a custom cache policy instead, set all time-to-live (TTL) values to `0`: | Setting | Value | |---|---| -| Minimum TTL | `0` | -| Maximum TTL | `0` | -| Default TTL | `0` | +| Minimum time-to-live | `0` | +| Maximum time-to-live | `0` | +| Default time-to-live | `0` | -CloudFront ignores `CDN-Cache-Control` and `Surrogate-Control` headers. It only reads the standard `Cache-Control` header. A custom cache policy with a non-zero `default-TTL` will cache responses regardless of the origin's `Cache-Control: max-age=0` directive. +CloudFront ignores `CDN-Cache-Control` and `Surrogate-Control` headers. It only reads the standard `Cache-Control` header. A custom cache policy with a non-zero default time-to-live caches responses regardless of the origin's `Cache-Control: max-age=0` directive. @@ -298,14 +298,14 @@ Alternatively, set "Honor Origin Cache-Control" to **Yes** so Akamai respects Fe -Caddy does not cache responses by default. No additional configuration is needed unless you have explicitly enabled caching with a `cache` directive or external module. +Caddy doesn't cache responses by default. No additional configuration is needed unless you have explicitly enabled caching with a `cache` directive or external module. ## Verify your setup -After configuring your proxy, confirm that requests are routed correctly and responses are not cached. +After configuring your proxy, confirm that requests are routed correctly and responses aren't cached. ```bash # Check that the page loads and x-fern-host is recognized From f4229102f080a531cce21f469d465d54400154c9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 17:40:44 +0000 Subject: [PATCH 3/6] fix(docs): align Cloudflare Workers example with verified worker code Co-Authored-By: Anar Kafkas --- .../pages/preview-publish/reverse-proxy.mdx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx index 53d40207c..7927fc240 100644 --- a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx +++ b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx @@ -31,34 +31,34 @@ The `x-fern-host` value is the bare domain without the subpath. For `mydomain.co Create a [Cloudflare Worker](https://developers.cloudflare.com/workers/) that intercepts requests to your docs subpath and proxies them to Fern. ```js -const FERN_ORIGIN = "https://app.buildwithfern.com"; +const SUBPATH = "/docs"; +const FERN_HOST = "mydomain.com"; export default { async fetch(request) { const url = new URL(request.url); - // Only proxy requests under /docs - if (!url.pathname.startsWith("/docs")) { - return fetch(request); + if ( + url.pathname !== SUBPATH && + !url.pathname.startsWith(`${SUBPATH}/`) + ) { + return new Response("Not found", { status: 404 }); } - const targetUrl = new URL(url.pathname + url.search, FERN_ORIGIN); + const upstreamUrl = new URL(request.url); + upstreamUrl.protocol = "https:"; + upstreamUrl.hostname = "app.buildwithfern.com"; + upstreamUrl.port = ""; - const headers = new Headers(request.headers); - headers.set("x-fern-host", url.hostname); - headers.set("Host", "app.buildwithfern.com"); + const proxiedRequest = new Request(upstreamUrl, request); + proxiedRequest.headers.set("x-fern-host", FERN_HOST); - return fetch(targetUrl.toString(), { - method: request.method, - headers, - body: request.body, - redirect: "manual", - }); + return fetch(proxiedRequest); }, }; ``` -Attach the Worker to a route pattern like `mydomain.com/docs/*` in the Cloudflare dashboard under **Workers Routes**. +Replace `SUBPATH` and `FERN_HOST` with your subpath and bare domain. Attach the Worker to a route pattern like `mydomain.com/docs/*` in the Cloudflare dashboard under **Workers Routes**. From 09f0f4833d011270f46bbf78a597af59ffb806da Mon Sep 17 00:00:00 2001 From: Devin Logan Date: Fri, 22 May 2026 13:50:25 -0400 Subject: [PATCH 4/6] restructure --- fern/products/dashboard/pages/domains.mdx | 5 + fern/products/docs/docs.yml | 1 + .../pages/preview-publish/reverse-proxy.mdx | 242 ++++++++---------- .../setting-up-your-domain.mdx | 7 +- 4 files changed, 123 insertions(+), 132 deletions(-) diff --git a/fern/products/dashboard/pages/domains.mdx b/fern/products/dashboard/pages/domains.mdx index 2fb777b52..c07112ae0 100644 --- a/fern/products/dashboard/pages/domains.mdx +++ b/fern/products/dashboard/pages/domains.mdx @@ -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). + + +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. + + 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. diff --git a/fern/products/docs/docs.yml b/fern/products/docs/docs.yml index b08cd8e26..34b173b6a 100644 --- a/fern/products/docs/docs.yml +++ b/fern/products/docs/docs.yml @@ -250,6 +250,7 @@ navigation: 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 diff --git a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx index 7927fc240..978eeb8ac 100644 --- a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx +++ b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx @@ -5,30 +5,36 @@ description: Configure a reverse proxy to serve Fern docs from a subpath on your 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, with the `x-fern-host` header set so Fern identifies your site. +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. -## Routing - -Your proxy must forward all requests under your docs subpath to Fern's origin at `app.buildwithfern.com`, preserving the original path. Set two headers on every proxied request: +Every proxied request needs two headers: | Header | Value | Purpose | -|--------|-------|---------| -| `x-fern-host` | Your bare domain (e.g. `mydomain.com`) | Tells Fern which docs site to serve | +|---|---|---| +| `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 | - -The `x-fern-host` value is the bare domain without the subpath. For `mydomain.com/docs`, set `x-fern-host: mydomain.com` — not `mydomain.com/docs`. - +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 + -### Provider-specific routing + + - - + + -Create a [Cloudflare Worker](https://developers.cloudflare.com/workers/) that intercepts requests to your docs subpath and proxies them to Fern. +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"; @@ -58,35 +64,55 @@ export default { }; ``` -Replace `SUBPATH` and `FERN_HOST` with your subpath and bare domain. Attach the Worker to a route pattern like `mydomain.com/docs/*` in the Cloudflare dashboard under **Workers Routes**. +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/*`. + + -Add a second origin and behavior to your CloudFront distribution. +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." + + + -**1. Create an origin** pointing to `app.buildwithfern.com` (HTTPS only, port 443). + -**2. Add a custom origin header:** + + + +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. + + + +Set the behavior to: + +- **Origin**: the Fern origin you just created +- **Cache policy**: `CachingDisabled` (AWS managed policy) +- **Origin request policy**: `AllViewer` (forwards all headers, query strings, and cookies) -**3. Create a cache behavior** for the path pattern `/docs*`: +To use a custom cache policy instead of `CachingDisabled`, set min, max, and default TTL to `0` and forward `Host` and `x-fern-host`. + + -- Origin: the Fern origin created above -- Cache policy: `CachingDisabled` (AWS managed policy) -- Origin request policy: `AllViewer` (forwards all headers, query strings, and cookies) + +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. + + -If you need fine-grained control instead of `CachingDisabled`, create a custom cache policy with time-to-live (TTL) values set to `0` and forward the `Host` and `x-fern-host` headers. + - - + + -Add a rewrite rule in your `netlify.toml` or `_redirects` file. Netlify rewrites proxy the request server-side and return the upstream response directly. +In `netlify.toml` (or `_redirects`): ```toml netlify.toml [[redirects]] @@ -98,15 +124,20 @@ Add a rewrite rule in your `netlify.toml` or `_redirects` file. Netlify rewrites [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 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. + - - + -Configure rewrites in your `vercel.json` (or `next.config.js` if using Next.js). Vercel rewrites proxy the request and serve the response from your domain. + + + +In `vercel.json`: ```json vercel.json { @@ -155,15 +186,22 @@ module.exports = { }, }; ``` + + + +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`. + + -Vercel rewrites automatically forward the `Host` header. To pass `x-fern-host`, you may need a middleware function. See Vercel's [middleware documentation](https://vercel.com/docs/functions/middleware) for details. +Vercel respects the origin's `Cache-Control` header for rewrite destinations, so no extra caching configuration is needed. + - - + -Add a `location` block that proxies your docs subpath to Fern. + + ```nginx location /docs { @@ -177,33 +215,54 @@ location /docs { # 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. + - - + -Configure a **Site Delivery** or **Ion** property to proxy your docs subpath. + + -**1. Add a rule** matching the path `/docs*`: +In a **Site Delivery** or **Ion** property, add a rule matching path `/docs*`: - **Origin Server**: `app.buildwithfern.com` - **Forward Host Header**: Origin Hostname -**2. Add a Modify Outgoing Request Header behavior** to set: +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. + + + -**3. Set the caching behavior** (see [Caching](#caching) below). + - - + + ```caddyfile mydomain.com { @@ -215,97 +274,18 @@ mydomain.com { } } ``` + + - - - -## Caching - -Fern sets `Cache-Control: public, max-age=0, must-revalidate` on HTML responses, which tells your proxy not to cache page content. Most providers respect this header by default. However, if your provider overrides origin cache headers or applies its own time-to-live, you must explicitly disable caching for the proxied path. - -Caching stale HTML causes visitors to load a page that references JavaScript and CSS files from an older deployment. Those files no longer exist, so the page fails to hydrate and may display an error. - - -Don't cache HTML responses from Fern. Static assets (`/_next/static/*`) are served directly by Fern's CDN with long-lived cache headers and don't pass through your proxy. - - -### Provider-specific caching - - - - -Cloudflare respects the origin's `Cache-Control` header by default for proxied (orange-cloud) records. No additional configuration is needed unless you have **Page Rules** or **Cache Rules** that override caching for your domain. - -To verify, check that no Page Rule sets "Cache Level: Cache Everything" for the `/docs*` path. If one exists, either remove it or add a more specific rule for `/docs*` with "Cache Level: Bypass." - -For Workers, the example in the [Routing](#routing) section already passes through the origin's headers without modification. - - - - -Use the AWS managed `CachingDisabled` cache policy on the `/docs*` behavior. This policy sets `min-TTL`, `max-TTL`, and `default-TTL` to `0`, so CloudFront always fetches a fresh response from Fern. - -If you use a custom cache policy instead, set all time-to-live (TTL) values to `0`: - -| Setting | Value | -|---|---| -| Minimum time-to-live | `0` | -| Maximum time-to-live | `0` | -| Default time-to-live | `0` | - - -CloudFront ignores `CDN-Cache-Control` and `Surrogate-Control` headers. It only reads the standard `Cache-Control` header. A custom cache policy with a non-zero default time-to-live caches responses regardless of the origin's `Cache-Control: max-age=0` directive. - - - - - -Netlify respects the origin's `Cache-Control` header for rewrite-proxied responses. No additional configuration is needed. - - - - -Vercel respects the origin's `Cache-Control` header for rewrite destinations. No additional configuration is needed. - - - - -Add cache-control directives to the `/docs` location block to prevent Nginx from caching responses (or from being cached by downstream proxies that ignore the origin header): - -```nginx -location /docs { - proxy_pass https://app.buildwithfern.com; - # ... other proxy_set_header directives ... - - # Do not cache HTML - proxy_no_cache 1; - proxy_cache_bypass 1; - add_header Cache-Control "public, max-age=0, must-revalidate" always; -} -``` - - - - -In the rule matching `/docs*`, add a **Caching** behavior: - -| 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 doesn't cache responses by default. No additional configuration is needed unless you have explicitly enabled caching with a `cache` directive or external module. - - - + +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 -After configuring your proxy, confirm that requests are routed correctly and responses aren't cached. +Run these checks against your live subpath: ```bash # Check that the page loads and x-fern-host is recognized @@ -319,4 +299,4 @@ curl -sI https://mydomain.com/docs | grep -i cache-control 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. Review the [Caching](#caching) section for your provider. +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. diff --git a/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx b/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx index 24cb1b478..933947a19 100644 --- a/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx +++ b/fern/products/docs/pages/preview-publish/setting-up-your-domain.mdx @@ -78,7 +78,7 @@ Once Fern has completed your setup, you'll be able to access your documentation -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](/learn/docs/preview-publish/reverse-proxy) that routes requests to Fern. +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. @@ -108,6 +108,11 @@ instances: [Here's an example.](https://github.com/fern-api/fern/blob/7d8631c6119787a8aaccb4ba49837e73c985db28/fern/docs.yml#L1-L3) + + +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). + + Contact Fern via your dedicated Slack channel or [email](mailto:support@buildwithfern.com) to set up your custom subpath. From 116194c3bb172537b7ed3eb61377698139290913 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 17:59:29 +0000 Subject: [PATCH 5/6] fix(docs): fix Vercel has condition and Caddy handle_path in reverse proxy guide - Vercel: Remove incorrect 'has' conditions from rewrites (has is a match condition, not a header-setter). Add middleware example for x-fern-host. - Caddy: Change handle_path to handle to preserve the /docs prefix in the upstream request. Co-Authored-By: Anar Kafkas --- .../pages/preview-publish/reverse-proxy.mdx | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx index 978eeb8ac..bceffba57 100644 --- a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx +++ b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx @@ -144,25 +144,11 @@ In `vercel.json`: "rewrites": [ { "source": "/docs", - "destination": "https://app.buildwithfern.com/docs", - "has": [ - { - "type": "header", - "key": "x-fern-host", - "value": "mydomain.com" - } - ] + "destination": "https://app.buildwithfern.com/docs" }, { "source": "/docs/:path*", - "destination": "https://app.buildwithfern.com/docs/:path*", - "has": [ - { - "type": "header", - "key": "x-fern-host", - "value": "mydomain.com" - } - ] + "destination": "https://app.buildwithfern.com/docs/:path*" } ] } @@ -187,9 +173,27 @@ module.exports = { }; ``` - + + +Vercel rewrites don't provide a way to add custom request headers to the upstream. Add a [middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) file to set `x-fern-host` on proxied requests: + +```ts middleware.ts +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-fern-host", "mydomain.com"); -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`. + return NextResponse.next({ + request: { headers: requestHeaders }, + }); +} + +export const config = { + matcher: ["/docs", "/docs/:path*"], +}; +``` @@ -266,7 +270,7 @@ Alternatively, set **Honor Origin Cache-Control** to **Yes** so Akamai respects ```caddyfile mydomain.com { - handle_path /docs* { + handle /docs* { reverse_proxy https://app.buildwithfern.com { header_up Host app.buildwithfern.com header_up x-fern-host mydomain.com From ee4cd750381462fa739c211d2e26b6e31f925d8f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 18:28:40 +0000 Subject: [PATCH 6/6] fix(docs): use Vercel routes with transforms for x-fern-host Replace rewrites + middleware approach with routes + transforms, which handles both the rewrite and the header in one vercel.json config. Co-Authored-By: Anar Kafkas --- .../pages/preview-publish/reverse-proxy.mdx | 64 ++++--------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx index bceffba57..3ebc444ea 100644 --- a/fern/products/docs/pages/preview-publish/reverse-proxy.mdx +++ b/fern/products/docs/pages/preview-publish/reverse-proxy.mdx @@ -135,65 +135,29 @@ Netlify rewrites with `status = 200` act as a reverse proxy — the visitor's br - + -In `vercel.json`: +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 { - "rewrites": [ + "$schema": "https://openapi.vercel.sh/vercel.json", + "routes": [ { - "source": "/docs", - "destination": "https://app.buildwithfern.com/docs" - }, - { - "source": "/docs/:path*", - "destination": "https://app.buildwithfern.com/docs/:path*" + "src": "/docs(/.*)?", + "dest": "https://app.buildwithfern.com/docs$1", + "transforms": [ + { + "type": "request.headers", + "op": "set", + "target": { "key": "x-fern-host" }, + "args": "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*", - }, - ]; - }, -}; -``` - - - -Vercel rewrites don't provide a way to add custom request headers to the upstream. Add a [middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) file to set `x-fern-host` on proxied requests: - -```ts middleware.ts -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export function middleware(request: NextRequest) { - const requestHeaders = new Headers(request.headers); - requestHeaders.set("x-fern-host", "mydomain.com"); - - return NextResponse.next({ - request: { headers: requestHeaders }, - }); -} - -export const config = { - matcher: ["/docs", "/docs/:path*"], -}; -```