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
- Go to dash.cloudflare.com
- Sign up or log in
- Go to Workers & Pages in the sidebar
3. Create a New Pages Project
- Click "Create application" → "Pages" → "Connect to Git"
- Connect your GitHub account (jophillips90)
- Select the
sanctumrepository - 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 —.nvmrcand.node-versionat the repo root both pin Node 24 (v3 build image reads these; it ignorespackage.json→engines). SettingNODE_VERSIONin 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_VERSIONwas 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):
- Set the build variable
SKIP_DEPENDENCY_INSTALL = 1so Cloudflare does not auto-install the whole monorepo. - 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
- Go to Zero Trust in Cloudflare dashboard
- Navigate to Access → Applications
- 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
- Click "Add application"
- Visit your docs site
- You'll be prompted to verify your email
- Check your email for the login code
- 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!