Skip to main content

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.services finishes registration and DNS propagation, we set up everything against the provisional <project>.pages.dev URL (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

  1. Cloudflare dashboard → Workers & PagesCreate applicationPages tab → Connect to Git.
  2. Authorize the Cloudflare GitHub App on the eigenoid org and grant access only to the internal-docs repo.
  3. Select the eigenoid/internal-docs repo.
  4. Production branch: main.
  5. Build configuration:
    • Framework preset: Docusaurus
    • Build command: npm run build
    • Build output directory: build
    • Root directory: (empty)
  6. Environment variables (production and preview):
    • NODE_VERSION = 20 (.nvmrc in the repo is also respected; this is belt-and-suspenders).
  7. 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.services has propagated and the zone is added in Cloudflare.

  1. In the Pages project → Custom domainsSet up a custom domain.
  2. Enter docs.eigenoid.services.
  3. Cloudflare creates the CNAME automatically if the eigenoid.services zone is in the same account. Otherwise, the registrar's NS records must point to Cloudflare first.
  4. SSL/TLS: automatic (Universal SSL).

3. Cloudflare Access — GitHub identity provider

Cloudflare Access uses OAuth Apps (not GitHub Apps) for GitHub identity.

  1. Create an OAuth App at https://github.com/organizations/eigenoid/settings/applicationsNew 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).
  2. Generate Client ID and Client Secret. Save the secret (Cloudflare masks it afterward).
  3. In Cloudflare → Zero TrustSettingsAuthenticationLogin methodsAdd newGitHub.
  4. Paste Client ID and Client Secret.
  5. Test — it should prompt a GitHub login and return with a green check.

4. Cloudflare Access — Application

  1. Zero Trust → AccessApplicationsAdd an applicationSelf-hosted.
  2. Application name: eigenoid-internal-docs
  3. Session duration: 24 hours (convenient — does not require daily re-login).
  4. Application domain: <project>.pages.dev (Phase 1) — later changed to docs.eigenoid.services in Phase 2.
  5. Identity providers: GitHub only (deselect the rest).
  6. Continue.
  7. Add policy:
    • Policy name: eigenoid-org-members
    • Action: Allow
    • Configure rules → Include → GitHub organization = eigenoid
  8. Save.

5. Protect preview deployments

  1. Go back to the Pages project → SettingsGeneralAccess policy.
  2. Enable the toggle. Cloudflare automatically creates a second Access app covering *.<project>.pages.dev.
  3. 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:

  1. Remove them from the GitHub org.
  2. Zero Trust → AccessUsers → 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

  1. Remove the old domain in Pages → Custom domains.
  2. Add the new one.
  3. Update the Access Application → Application domain to the new value.
  4. Update the Authorization callback URL in the GitHub OAuth App if the Cloudflare team changed.

Credential rotation

Rotate the GitHub OAuth App Client Secret

  1. GitHub org eigenoid → Settings → OAuth Apps → Cloudflare Access (eigenoid docs)Generate a new client secret.
  2. Cloudflare → Zero Trust → Settings → Authentication → edit the GitHub provider → replace Client Secret → Save.
  3. 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:

  1. GitHub org eigenoid → Settings → Installed GitHub Apps → Cloudflare → Suspend or Uninstall.
  2. Re-connect the project in Cloudflare Pages — it will prompt for authorization again.

Troubleshooting

SymptomLikely causeAction
Build fails with unsupported engine or glibcWrong Node versionVerify that NODE_VERSION=20 is set in the project env vars and that .nvmrc contains 20
404 on docs.eigenoid.services after adding the domainDNS has not propagated yetWait ~5 min. Verify the CNAME in the Cloudflare zone
GitHub login redirects to a Cloudflare error pageCallback URL misconfigured in the OAuth AppCompare 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 siteThe user has their org membership set to privateAsk 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 orgPolicy not applied to the previews appPages → Settings → Access policy → re-enable the toggle
Build succeeds but the site is blankBuild output directory misconfiguredMust 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.dev are publicly addressable if not gated.
  • Domain: docs.eigenoid.services — short and self-descriptive. If public docs are needed later, they would go on public-docs.eigenoid.services or a dedicated subdomain.
  • Custom block page: redirect to https://block.eigenoid.services (served from the eigenoid/access-block-page repo). 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.services must be deployed and returning 200 OK before 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.

  1. Cloudflare → Zero TrustAccessApplications → edit the app (e.g. eigenoid-internal-docs).
  2. Scroll to Block page (may appear as "Custom block page", "Forbidden page", or at the end of the Configuration wizard depending on UI version).
  3. Change from Cloudflare default to URL redirect.
  4. URL: https://block.eigenoid.services.
  5. 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.services at 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

  1. Open an incognito window and go to https://docs.eigenoid.services.
  2. Log in with a GitHub account that is not a member of the eigenoid org.
  3. Cloudflare should redirect to https://block.eigenoid.services and show the page with the white wordmark, "no access" title, and the active red glitch effect. The browser address bar should show block.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 at https://block.eigenoid.services/assets/<file> as soon as the deploy finishes.
  • How to change the page copy: edit index.html in that repo. Do not touch internal-docs for this.

Block page troubleshooting

SymptomLikely causeAction
Blocked visitor sees the default Cloudflare pageBlock page is not set to "URL redirect" in the app, or it is a new app where the setting was not configuredEdit 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 errorThe Pages project has not finished deploying or the custom domain has not propagatedCheck the latest deploy in CF Pages, wait for SSL if the domain is new
Infinite loop when the policy failsThe block.eigenoid.services domain was accidentally placed behind AccessRemove the Access app covering that domain. The block page must remain public
Logo does not appear on the pageWrong path in index.html or the asset was deletedVerify the assets/<file> reference in the HTML and that the file exists on main