Skip to main content

Deploying Docs to Cloudflare Pages

Why Cloudflare Pages?

  • ✅ Free deployment
  • ✅ Free authentication (Cloudflare Access)
  • ✅ Fast global CDN
  • ✅ Auto-deploy on git push

Setup Instructions

1. Push Your Code to GitHub

git push origin master

2. Sign Up / Log In to Cloudflare

3. Create a New Pages Project

  1. Click "Create application""Pages""Connect to Git"
  2. Connect your GitHub account (jophillips90)
  3. Select the sanctum repository
  4. Click "Begin setup"

4. Configure Build Settings

Production branch: master

Build settings:

  • Framework preset: None (we configure manually)
  • Build command: bash scripts/cf-pages-build.sh
  • Build output directory: docs/build
  • Root directory: / (leave empty or set to root)

Environment variables (Build variables):

  • SKIP_DEPENDENCY_INSTALL: 1required. See "Why the custom build script" below.
  • NODE_VERSION: leave unset.nvmrc and .node-version at the repo root both pin Node 24 (v3 build image reads these; it ignores package.jsonengines). Setting NODE_VERSION in the dashboard overrides the version files and silently drifts; don't do it.

⚠️ If the build fails with engine warnings or pnpm-install errors, first check whether NODE_VERSION was set to an older value in the dashboard. The fix is to delete it and re-deploy.

Why the custom build script (pnpm monorepo pitfall)

This repo is a pnpm monorepo. Cloudflare's default behaviour runs pnpm install --frozen-lockfile at the repo root before the build command, which installs every workspace project (Scope: all 11 workspace projects) — api, web, admin, gdl and their native modules (better-sqlite3, @prisma/engines, @swc/core, esbuild, unrs-resolver, …). The static docs site needs none of that.

Whenever a dependency bump invalidates Cloudflare's dependency cache, that whole native-heavy graph has to be cold-installed inside the build container. It is slow and fragile and surfaces as generic "Build failed" / "Building" errors even though the docs themselves compile fine. This was the root cause of the July 2026 run of failed builds that followed the batch of dependency-upgrade PRs.

The fix (Cloudflare's own recommendation for pnpm monorepos — Skip dependency install):

  1. Set the build variable SKIP_DEPENDENCY_INSTALL = 1 so Cloudflare does not auto-install the whole monorepo.
  2. Point the build command at scripts/cf-pages-build.sh, which runs a scoped install (pnpm install --filter docs... --frozen-lockfile) and then builds only the docs workspace.

Keeping the logic in a committed script (instead of a one-line dashboard field) means it is version-controlled, reviewable, and testable locally:

bash scripts/cf-pages-build.sh

Known Issues

OpenAPI Reference Pages

docusaurus-plugin-openapi-docs + docusaurus-theme-openapi-docs (5.1.x) generate the /api/reference/* pages from static/openapi.json on Docusaurus 3.10. Getting that combination to survive SSG needs three workarounds, all of them load-bearing — removing any one breaks pnpm build.

1. Duplicate DocProvider React context. pnpm gives the OpenAPI theme its own instance of @docusaurus/plugin-content-docs (pnpm why @docusaurus/plugin-content-docs reports "1 version, 2 instances" — one version, two peer-resolved copies). @theme/ApiItem then renders <DocProvider> from one copy while theme-classic's DocItem internals call useDoc() against the other, and every reference page dies with:

Hook useDoc is called outside the <DocProvider>.

Neither node-linker=hoisted, public-hoist-pattern=@docusaurus/*, nor dedupe-peer-dependents collapses the two copies. The fix is the small dedupe-docs-client plugin in docusaurus.config.ts, which aliases @docusaurus/plugin-content-docs/client to the copy theme-classic uses so both share one module and therefore one React context.

2. js-yaml 5.x for Redocly. @redocly/openapi-core (pulled in by the plugin) calls CORE_SCHEMA.withTags(), which only exists in js-yaml 5.x. The repo-wide js-yaml: ^4.1.1 override forces it down to 4.x and page generation fails. pnpm-workspace.yaml scopes an exception: '@redocly/openapi-core>js-yaml': ^5.2.1.

3. webpackbar 7.x. webpackbar@6.0.1 overwrites the ProgressPlugin options it inherited from super() with its own ({name, color, reporters, ...}). Webpack validates those against the ProgressPlugin schema on compiler.hooks.validate and rejects them:

Progress Plugin has been initialized using an options object that does not match the API schema.

webpackbar 7.0.0 composes rather than mutates, and its . export is still the webpack plugin class @docusaurus/bundler expects, so pnpm-workspace.yaml overrides webpackbar: ^7.0.0.

Regenerating the pages. scripts/cf-pages-build.sh only runs pnpm --filter docs build; it does not generate the reference pages. So docs/docs/api/reference/** is committed and must be regenerated by hand whenever the spec changes:

cd apps/api && pnpm gen:api-spec # refresh docs/static/openapi.json
cd ../../docs && pnpm docusaurus gen-api-docs all

The generated output is excluded from Prettier via .prettierignore. The spec itself also stays served as a static asset at /openapi.json for Postman / Insomnia / Scalar consumers.

5. Deploy

Click "Save and Deploy"

Cloudflare will:

  • Install dependencies
  • Build the docs
  • Deploy to sanctum-docs.pages.dev

⏱️ Takes about 2-3 minutes

🔐 Add Authentication (Free!)

Enable Cloudflare Access

  1. Go to Zero Trust in Cloudflare dashboard
  2. Navigate to AccessApplications
  3. Click "Add an application""Self-hosted"

Configuration:

  • Application name: Sanctum Docs
  • Session duration: 24 hours (or your preference)
  • Application domain: sanctum-docs.pages.dev

Add Access Policy

Policy name: Email verification

Action: Allow

Configure rules:

  • Selector: Emails
  • Value: Your email addresses (comma-separated)

Example: you@example.com, collaborator@example.com

Or use email domain:

  • Selector: Emails ending in
  • Value: @yourdomain.com

Save and Test

  1. Click "Add application"
  2. Visit your docs site
  3. You'll be prompted to verify your email
  4. Check your email for the login code
  5. Enter code → Access granted!

🔄 Auto-Deploy

Every push to master automatically deploys:

git add docs/
git commit -m "docs: update API reference"
git push
# → Cloudflare auto-deploys in ~2 minutes

📧 Managing Access

Add More Users

Go to Zero TrustAccess → Your application → Edit

Add more emails to the policy.

Revoke Access

Remove emails from the policy or disable the application.

Access Logs

View who accessed your docs: Zero TrustLogsAccess

💰 Cost

100% FREE for:

  • Up to 50 users
  • Unlimited deployments
  • Email authentication
  • Global CDN

🎯 Result

Your documentation will be:

  • ✅ Deployed globally
  • ✅ Protected by email authentication
  • ✅ Auto-updated on every push
  • ✅ Completely free

Only people with verified emails can access it!