Cloudflare Pages + Access (private deployment of docs.eigenoid.services)
What this guide covers
How the internal docs site is deployed on Cloudflare Pages behind Cloudflare Access (Zero Trust), and how to operate it (add/remove users, rotate credentials, debug incidents).
Final URL (once the domain registration completes): docs.eigenoid.services
Provisional URL (in the meantime): <project>.pages.dev — Cloudflare assigns it automatically when the project is created. Access gates that URL the same as the custom domain.
Repo: eigenoid/internal-docs
Identity: GitHub OAuth, restricted to members of the eigenoid org.
Architecture
GitHub push (eigenoid/internal-docs, branch main)
│
▼
Cloudflare Pages (native build: npm run build → ./build)
│
▼
Edge: docs.eigenoid.services (CNAME to <project>.pages.dev)
│
▼
Cloudflare Access (Self-hosted application)
policy: GitHub org `eigenoid` → Allow
│ (OAuth login with GitHub)
▼
Authenticated user sees the site.
Preview deployments for PRs (URLs *.pages.dev) also go through the same Access policy — they are covered by the "Access policy for preview deployments" toggle in the Pages project.
Initial setup (one-time)
These tasks are done via the Cloudflare UI. No step requires editing the repo beyond the files that already exist (
.nvmrc,package.json,docs/).
Two-phase rollout. While
eigenoid.servicesfinishes registration and DNS propagation, we set up everything against the provisional<project>.pages.devURL (Phase 1). Once the domain is ready, we add it to the project and update Access (Phase 2). No re-deploy is needed.
Phase 1 — Setup against the default *.pages.dev domain
1. Create the project in Cloudflare Pages
- Cloudflare dashboard → Workers & Pages → Create application → Pages tab → Connect to Git.
- Authorize the Cloudflare GitHub App on the
eigenoidorg and grant access only to theinternal-docsrepo. - Select the
eigenoid/internal-docsrepo. - Production branch:
main. - Build configuration:
- Framework preset: Docusaurus
- Build command:
npm run build - Build output directory:
build - Root directory: (empty)
- Environment variables (production and preview):
NODE_VERSION=20(.nvmrcin the repo is also respected; this is belt-and-suspenders).
- Save and Deploy. The first deploy should finish green in ~2 min.
2. Custom domain (Phase 2 — when DNS is ready)
Skip this section until
eigenoid.serviceshas propagated and the zone is added in Cloudflare.
- In the Pages project → Custom domains → Set up a custom domain.
- Enter
docs.eigenoid.services. - Cloudflare creates the
CNAMEautomatically if theeigenoid.serviceszone is in the same account. Otherwise, the registrar's NS records must point to Cloudflare first. - SSL/TLS: automatic (Universal SSL).
3. Cloudflare Access — GitHub identity provider
Cloudflare Access uses OAuth Apps (not GitHub Apps) for GitHub identity.
- Create an OAuth App at
https://github.com/organizations/eigenoid/settings/applications→ New OAuth App:- Application name:
Cloudflare Access (eigenoid docs) - Homepage URL:
https://docs.eigenoid.services - Authorization callback URL: the one Cloudflare provides in the next step (format
https://<team>.cloudflareaccess.com/cdn-cgi/access/callback).
- Application name:
- Generate Client ID and Client Secret. Save the secret (Cloudflare masks it afterward).
- In Cloudflare → Zero Trust → Settings → Authentication → Login methods → Add new → GitHub.
- Paste Client ID and Client Secret.
- Test — it should prompt a GitHub login and return with a green check.
4. Cloudflare Access — Application
- Zero Trust → Access → Applications → Add an application → Self-hosted.
- Application name:
eigenoid-internal-docs - Session duration:
24 hours(convenient — does not require daily re-login). - Application domain:
<project>.pages.dev(Phase 1) — later changed todocs.eigenoid.servicesin Phase 2. - Identity providers: GitHub only (deselect the rest).
- Continue.
- Add policy:
- Policy name:
eigenoid-org-members - Action: Allow
- Configure rules → Include → GitHub organization =
eigenoid
- Policy name:
- Save.
5. Protect preview deployments
- Go back to the Pages project → Settings → General → Access policy.
- Enable the toggle. Cloudflare automatically creates a second Access app covering
*.<project>.pages.dev. - Verify that the applied policy is the same (
eigenoid-org-members) or create an equivalent one.
Day-to-day operation
Add a new user
No changes needed in Cloudflare. Add the user as a member of the eigenoid org on GitHub. The next time they visit docs.eigenoid.services, they will pass the policy.
Remove a user
Remove them from the eigenoid org on GitHub. The active session expires when the Session duration elapses (24h). If the user was compromised and you need to cut access immediately:
- Remove them from the GitHub org.
- Zero Trust → Access → Users → find the user → Revoke session.
Pulling from an external fork
By default, Cloudflare Pages does not build PRs from forks, so there is no risk of content leakage. If that option is ever enabled, previews are still gated by Access.
Change the domain
- Remove the old domain in Pages → Custom domains.
- Add the new one.
- Update the Access Application → Application domain to the new value.
- Update the Authorization callback URL in the GitHub OAuth App if the Cloudflare team changed.
Credential rotation
Rotate the GitHub OAuth App Client Secret
- GitHub org
eigenoid→ Settings → OAuth Apps →Cloudflare Access (eigenoid docs)→ Generate a new client secret. - Cloudflare → Zero Trust → Settings → Authentication → edit the GitHub provider → replace Client Secret → Save.
- Delete the old secret in GitHub.
Do this every 12 months or if a leak is suspected. The rotation requires no downtime: existing sessions remain valid; only new logins use the new secret.
Rotate the Cloudflare Pages GitHub App (the deploy one)
This is the app that clones the repo, not the Access one. To rotate it:
- GitHub org
eigenoid→ Settings → Installed GitHub Apps → Cloudflare → Suspend or Uninstall. - Re-connect the project in Cloudflare Pages — it will prompt for authorization again.
Troubleshooting
| Symptom | Likely cause | Action |
|---|---|---|
Build fails with unsupported engine or glibc | Wrong Node version | Verify that NODE_VERSION=20 is set in the project env vars and that .nvmrc contains 20 |
404 on docs.eigenoid.services after adding the domain | DNS has not propagated yet | Wait ~5 min. Verify the CNAME in the Cloudflare zone |
| GitHub login redirects to a Cloudflare error page | Callback URL misconfigured in the OAuth App | Compare the callback URL in the GitHub provider under Zero Trust with the one in the OAuth App |
| User who is an org member cannot access the site | The user has their org membership set to private | Ask them to make it public (Settings → Profile → Organizations) or change the policy to Email ends with @eigenoid.com |
| Preview deployments prompt for auth but do not validate the org | Policy not applied to the previews app | Pages → Settings → Access policy → re-enable the toggle |
| Build succeeds but the site is blank | Build output directory misconfigured | Must be build, not dist or public |
Design decisions (reference)
- Hosting: Cloudflare Pages (instead of GitHub Pages, Netlify, Vercel) for native integration with Access.
- Deploy: native Pages-to-GitHub connection (not GitHub Actions with
wrangler) — less code to maintain. - Auth: GitHub OAuth (not Google Workspace, not email OTP) — the team is already on GitHub, minimal friction.
- Policy: GitHub org members (not an email allowlist) — scales with the team's standard onboarding.
- Protected previews: yes — preview URLs
*.pages.devare publicly addressable if not gated. - Domain:
docs.eigenoid.services— short and self-descriptive. If public docs are needed later, they would go onpublic-docs.eigenoid.servicesor a dedicated subdomain. - Custom block page: redirect to
https://block.eigenoid.services(served from theeigenoid/access-block-pagerepo). Default for all Access apps in the org, not just this one. See ADR-0005 and the "Default block page" section below.
Default block page (block.eigenoid.services)
https://block.eigenoid.services is the org-wide generic block page: an on-brand static page with "403 Forbidden" copy that does not mention any specific service. The idea is to configure it as the block page redirect in all eigenoid Access apps, so blocked visitors get a consistent experience regardless of which surface they tried to reach.
The site lives in the dedicated repo eigenoid/access-block-page and is deployed with the native Cloudflare Pages integration.
That same origin also serves as a public CDN for branding assets (logos, wordmarks) at https://block.eigenoid.services/assets/<file>, accessible without authentication.
See ADR-0005 for the reasoning behind this decision.
Configure the redirect in an Access app
This setup must be done on each Access app in the org for a consistent experience. The destination page is always the same.
Prerequisite:
block.eigenoid.servicesmust be deployed and returning200 OKbefore configuring the redirect, or visitors will see an error instead of the page.
Why per-app and not global: on Pay-as-you-go / Enterprise plans there is a global "Access block page" setting under Reusable components → Custom pages, but on the Free plan that listing only exposes the Gateway block page (DNS/HTTP filter), not the Access one. The only way to change the Access block page on Free is to configure it application by application.
- Cloudflare → Zero Trust → Access → Applications → edit the app (e.g.
eigenoid-internal-docs). - Scroll to Block page (may appear as "Custom block page", "Forbidden page", or at the end of the Configuration wizard depending on UI version).
- Change from Cloudflare default to URL redirect.
- URL:
https://block.eigenoid.services. - Save.
Apply the same change to the Access app for preview deployments (the one covering *.<project>.pages.dev) to maintain consistency.
Checklist for new apps: whenever a new Access app is created in the org, add the block page redirect to
https://block.eigenoid.servicesat the end of the creation wizard. If forgotten, the app will fall back to the "Cloudflare default" until corrected. There is no way to inherit this setting on Free.
End-to-end verification
- Open an incognito window and go to
https://docs.eigenoid.services. - Log in with a GitHub account that is not a member of the
eigenoidorg. - Cloudflare should redirect to
https://block.eigenoid.servicesand show the page with the white wordmark, "no access" title, and the active red glitch effect. The browser address bar should showblock.eigenoid.services(it is a 307, not a proxy).
Operating the block page repo
- Where it lives:
eigenoid/access-block-page(private). - How it deploys: push to
main→ Cloudflare Pages detects and deploys to production automatically. No GitHub Actions. - How to add a branding asset: drop the file into
assets/in the repo, open a PR with a Conventional Commit title, merge. Available athttps://block.eigenoid.services/assets/<file>as soon as the deploy finishes. - How to change the page copy: edit
index.htmlin that repo. Do not touchinternal-docsfor this.
Block page troubleshooting
| Symptom | Likely cause | Action |
|---|---|---|
| Blocked visitor sees the default Cloudflare page | Block page is not set to "URL redirect" in the app, or it is a new app where the setting was not configured | Edit the Access app and change to URL redirect → https://block.eigenoid.services. Each app has its own config on Free; there is no global default. |
Redirect works but block.eigenoid.services returns 404 or cert error | The Pages project has not finished deploying or the custom domain has not propagated | Check the latest deploy in CF Pages, wait for SSL if the domain is new |
| Infinite loop when the policy fails | The block.eigenoid.services domain was accidentally placed behind Access | Remove the Access app covering that domain. The block page must remain public |
| Logo does not appear on the page | Wrong path in index.html or the asset was deleted | Verify the assets/<file> reference in the HTML and that the file exists on main |