Docs site deploy (Cloudflare Pages + Access)
The docs site at apps/docs/ (Nextra v4) deploys to Cloudflare Pages and is gated behind Cloudflare Access. This runbook reflects the live setup as configured on 2026-05-29.
Live configuration
| Thing | Value |
|---|---|
| Cloudflare account | Taikan (74e1f4e430cf29df86bd66ff89e4d1c0) |
| Pages project | taikan-docs |
| Canonical URL | https://docs.usetaikan.com |
| Pages URL | https://taikan-docs.pages.dev |
| Preview URLs | https://<deploy-hash>.taikan-docs.pages.dev |
| Zero Trust team domain | desmotech.cloudflareaccess.com |
| Access application | docs (self-hosted) |
| Access policy | Allow — saarku@gmail.com only |
| Login method | One-time PIN (email) |
All three hostnames (docs.usetaikan.com, taikan-docs.pages.dev, *.taikan-docs.pages.dev) are covered by the single docs Access application, so every entry point — production, the raw Pages URL, and per-deploy previews — is gated.
What ships where
| Surface | URL | Auth |
|---|---|---|
| Local dev | http://localhost:3030 | none |
| Production | https://docs.usetaikan.com | Cloudflare Access |
| Source | /docs/ in this repo (canonical markdown) | repo permissions |
Pipeline
.github/workflows/deploy-docs.yml triggers on main pushes that touch docs/**, apps/docs/**, or the workflow itself.
push to main with docs change
↓
build apps/docs (pnpm install + pnpm build → out/)
↓
cloudflare/wrangler-action@v3 → `wrangler pages deploy out --project-name=taikan-docs --branch=main`
↓
Cloudflare Pages serves out/ from taikan-docs.pages.dev + docs.usetaikan.com
↓
Cloudflare Access gates the request (one-time PIN to policy-listed emails)Builds run on every matching push. Concurrency group docs-deploy prevents overlapping runs.
The build emits ~1,800 files / ~190 MB — Nextra v4 inlines the sidebar page-map into every page and ships per-page RSC .txt segments, so the output is heavier than a typical static site. This is well within Cloudflare Pages limits (25 MiB/file, 20,000 files) but is why the dashboard’s drag-and-drop uploader (1,000-file cap) can’t be used — deploys go through wrangler.
One-time setup (already done — kept for rebuild / DR)
1. Pages project
Created via wrangler rather than the dashboard, because the static export exceeds the dashboard uploader’s 1,000-file limit:
pnpm dlx wrangler login # interactive OAuth (local only)
pnpm -C apps/docs build # produces apps/docs/out/
pnpm dlx wrangler pages deploy apps/docs/out \
--project-name=taikan-docs --branch=main # creates the project if absentThe first pages deploy creates the taikan-docs project automatically.
2. CI credentials (GitHub repo secrets)
The local wrangler login only authenticates your machine. CI can’t do interactive OAuth, so the workflow uses an API token instead.
- Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token.
- Permission: Account → Cloudflare Pages → Edit, scoped to the Taikan account.
- Account ID: dashboard right sidebar (
74e1f4e430cf29df86bd66ff89e4d1c0). - Repo → Settings → Secrets and variables → Actions → add:
| Name | Value |
|---|---|
CLOUDFLARE_API_TOKEN | the custom token |
CLOUDFLARE_ACCOUNT_ID | 74e1f4e430cf29df86bd66ff89e4d1c0 |
3. Custom domain (docs.usetaikan.com)
Gotcha discovered during setup: adding a custom domain to a Pages project does not auto-create the DNS record, even when the zone is on the same account. You must do both:
- Attach the domain to the project:
POST /accounts/{account}/pages/projects/taikan-docs/domainswith{ "name": "docs.usetaikan.com" }. - Create the proxied CNAME in the
usetaikan.comzone:docs.usetaikan.com CNAME taikan-docs.pages.dev— proxied (orange cloud), TTL auto.
Without step 2 the domain sits in initializing/pending forever with a misleading “CNAME record not set” message. Once the proxied CNAME exists, Cloudflare validates over HTTP and provisions a Google-issued cert within a few minutes.
4. Cloudflare Access
Zero Trust dashboard → Access → Applications → Add → Self-hosted.
- Application name:
docs - Session duration: 24h
auto_redirect_to_identity: on (skips the email-entry screen when there’s a single login method)- Self-hosted domains — add all three so no entry point is left open:
docs.usetaikan.comtaikan-docs.pages.dev*.taikan-docs.pages.dev← covers preview deploys
Then add a policy:
| Field | Value |
|---|---|
| Action | Allow |
| Include | Emails → saarku@gmail.com (extend as needed) |
How Cloudflare Access OTP actually behaves
Understanding this avoids false alarms:
- The email-entry + “send code” screen never blocks anyone. That step is authentication (proving you control an inbox), not authorization. Anyone can type any email there.
- A one-time PIN is only emailed to addresses that match a policy. Random / test emails that aren’t in the allow list receive nothing — Cloudflare won’t relay codes to arbitrary inboxes. So “I entered a test email and no code arrived” is correct, expected behavior, not a bug.
- The policy is enforced after the code is verified. Even a non-listed email that somehow had a valid code would hit a hard “you don’t have access” wall.
Net: the only way in is an email explicitly listed in the policy. To let more people in, add their emails (or a whole email_domain) to the policy.
Day-to-day
Triggering a redeploy
Push to main with a change under docs/** or apps/docs/**, or run Actions → Deploy docs to Cloudflare Pages → Run workflow. Cloudflare retains recent deployments — roll back from the Pages project dashboard if a deploy goes bad.
Granting / revoking access
Zero Trust → Access → Applications → docs → Policies. Add or remove emails / domains. Changes apply within seconds; no redeploy needed.
To allow an entire domain, the policy include becomes:
include: [{ email_domain: { domain: "desmotech.com" } }]Adding a new doc
Edit any file under /docs/ (canonical source) and push to main. The workflow rebuilds and redeploys. Don’t edit apps/docs/content/ — it’s a regenerated mirror (see apps/docs/scripts/sync-content.mjs).
Manual deploy from your machine
If CI is down and you need to ship:
pnpm -C apps/docs build
pnpm dlx wrangler pages deploy apps/docs/out --project-name=taikan-docs --branch=mainRequires a prior wrangler login.
Why this setup, not GitHub Pages
- Auth. GitHub Pages on a regular plan is public-only. Cloudflare Access gates by email/SSO without a GitHub Enterprise Cloud subscription.
- Custom domain under the same gate.
docs.usetaikan.comand the.pages.devURLs all sit behind one Access app. - Faster invalidation. Cloudflare’s CDN updates within ~30s of a deploy.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
Login page shows an unexpected team domain (e.g. fragrant-rain-…cloudflareaccess.com) | Stale browser redirect / cached Access session from earlier setup | Retry in a fresh incognito window; the live team domain is desmotech.cloudflareaccess.com. |
| Test email “sends a code” but it never arrives | Working as designed — OTP only goes to policy-listed emails | Use a listed email, or add the address to the policy. |
Custom domain stuck in pending / “CNAME record not set” | Proxied CNAME never created (Pages doesn’t auto-create it) | Add docs.usetaikan.com CNAME taikan-docs.pages.dev, proxied, in the usetaikan.com zone. |
.pages.dev URL serves docs without a login prompt | Hostname not in the Access app’s self-hosted domains | Add taikan-docs.pages.dev + *.taikan-docs.pages.dev to the docs app. |
| Workflow fails at the deploy step with an auth error | CLOUDFLARE_API_TOKEN missing / expired / wrong scope | Re-mint with Pages → Edit; resave the repo secret. |
| Dashboard “Upload files” rejects the build | Export exceeds the 1,000-file dashboard cap | Deploy via wrangler (20,000-file limit), not the dashboard uploader. |
| Workflow succeeds but URL shows old content | CDN cache | Pages busts cache per deploy; if stuck, Purge Everything on the zone. |
| Nextra build hangs on Pagefind indexing | Runner memory cap | Temporarily set search: true (drop codeblocks) in apps/docs/next.config.mjs. |
Related
apps/docs/README.md— local dev recipe- Cloudflare Pages docs: https://developers.cloudflare.com/pages/
- Cloudflare Access: https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/
- Cloudflare Access one-time PIN: https://developers.cloudflare.com/cloudflare-one/identity/one-time-pin/