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: 1 — required. 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.json → engines). 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 Access → Applications
  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 Trust → Access → 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 Trust → Logs → Access

💰 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!