Deploy Private Cloudflare Site
Deploy a site behind a fail-closed identity boundary, preserve framework asset delivery after authentication, and verify the public surface before handing it off.
Non-negotiable rules
- Treat private content, source data, build artifacts, and identity-provider credentials as sensitive.
- Prefer Cloudflare Access attached to the Worker. Use application-managed Google OAuth only when Access is unavailable, unsuitable, or explicitly requested.
- Keep the production route disabled or return a fail-closed maintenance response until the identity boundary and required secrets exist.
- Never place client secrets, signing keys, or API tokens in
vars, source files, command-line arguments, logs, or chat output. Use wrangler secret put through interactive or file-based input.
- Do not fabricate API tokens or ask for account passwords. Use Wrangler OAuth, the Cloudflare API MCP OAuth flow, or a narrowly scoped token the user creates.
- Require explicit authorization before transferring an identity-provider secret from one service to Cloudflare.
- Do not declare success from a local build alone. Test the deployed anonymous boundary and one authenticated browser session.
- Retrieve current official Cloudflare and identity-provider documentation before changing live configuration. Cloudflare's Workers and Access surfaces evolve quickly.
Load the focused references
- Read references/workflow.md before choosing the protection architecture or changing live resources.
- Read references/troubleshooting.md when deployment succeeds but requests fail, assets 404, or the page does not hydrate.
- Run
scripts/verify-private-boundary.mjs before handoff.
Workflow
1. Inspect before mutating
- Find repository instructions and inspect the working tree without overwriting unrelated changes.
- Identify the framework, build command, build output, Wrangler version, existing
wrangler.jsonc or generated config, routes, asset binding, and current public URLs.
- Check Cloudflare identity non-destructively with
wrangler whoami. If unavailable, use wrangler login; use --browser=false or device flow in a headless environment.
- Check whether the Cloudflare plugin or Cloudflare API MCP is installed. Use it when available for account configuration, but keep Wrangler for build/deploy/tail operations.
- Enumerate every public path: production hostname,
workers.dev, preview URLs, custom domains, static chunks, API endpoints, source maps, and old hosts.
- Record the intended allowlist and whether each identity is a Gmail, Workspace, or non-Google address. Do not assume a private-relay address can securely authenticate through Google.
2. Choose one protection architecture
Use this order:
- Worker-level Cloudflare Access: preferred because every domain and preview attached to the Worker can be protected together.
- Hostname/path Access: use when only a specific route or custom hostname should be private, or when WebSockets make Worker-level Access unsuitable.
- Application-managed OAuth: use only after documenting why Access is not being used. This adds callback, CSRF, token-validation, session, and asset-gating responsibilities to the application.
For exact people in Access, create an Allow policy whose Include selector is Emails with the complete addresses. Do not use Everyone, Login Methods: One-time PIN, or a broad email-domain rule when the requirement is an exact allowlist.
For a non-Google address, Cloudflare Access with email one-time PIN can authenticate ownership without pretending Google is authoritative for that address. If stronger assurance is required, configure its actual identity provider.
3. Build a fail-closed Worker boundary
For Cloudflare Access:
- Attach Access to the Worker or exact hostname before enabling the public route.
- Use
ctx.access only when application code needs the authenticated identity; Access itself should reject unauthorized requests before the Worker runs.
- Confirm the policy is deny-by-default and includes only intended addresses.
For application-managed Google OAuth:
- Use the authorization-code flow with an exact HTTPS redirect URI.
- Generate and verify
state; use nonce and PKCE where supported.
- Exchange the code server-side. Validate the ID token signature,
iss, aud, exp, nonce, and verified identity claims against current Google guidance.
- Normalize the email for allowlist comparison, but use the Google
sub claim as the stable account identifier in stored user records.
- For Gmail, Google is authoritative for the address. For Workspace, require
email_verified and the expected hd claim. For non-Gmail addresses without hd, flag that Google does not provide continuing authority over the underlying mailbox.
- Sign the application session with a dedicated random secret. Set cookies
Secure, HttpOnly, and SameSite=Lax or stricter; set explicit expiry; rotate deliberately.
- Return a generic 403 for a valid Google account not on the application allowlist.
4. Gate HTML and assets together
When Workers Static Assets are present:
- Configure an
ASSETS binding and assets.run_worker_first: true when application code performs authentication.
- Authenticate first, then route actual static files through
env.ASSETS.fetch(request).
- Do not send framework chunks such as
/_next/static/*, /assets/*, fonts, CSS, or client manifests through a framework request handler that may return a 404 instead of falling through to the asset binding.
- Keep only the minimum sign-in and OAuth callback surface public. Verify that public responses contain no private page text, serialized state, filenames, metadata, or user data.
- Prefer route-pattern
run_worker_first configuration when it can express the boundary without a wrapper.
5. Configure secrets and deploy safely
- Declare required secret names with
secrets.required when the installed Wrangler version supports it.
- Add secrets interactively with
wrangler secret put NAME. Do not pass values as shell arguments.
- Use a dry run or build before deployment. Inspect the generated Wrangler configuration when using the Cloudflare Vite plugin or another adapter.
- Deploy while
workers.dev remains disabled if the boundary is incomplete. Remember that Wrangler can re-enable it when config and dashboard disagree.
- Enable only the intended route after Access or the OAuth wrapper is ready.
- Preserve a rollback target or previous Worker version.
6. Verify the privacy boundary
Run the bundled anonymous verifier with at least one real deployed asset path and several private phrases:
node ~/.codex/skills/deploy-private-cloudflare-site/scripts/verify-private-boundary.mjs \
--url https://example.workers.dev \
--asset /assets/app.js \
--forbid "private dashboard" \
--forbid "customer name"
Then verify manually in a clean browser context:
- Anonymous root is redirected to sign-in or returns 401/403.
- The public sign-in response contains none of the private phrases.
- A real JavaScript/CSS/image path is also redirected or denied, not 200 or 404.
- An allowlisted identity completes sign-in and the app hydrates.
- A non-allowlisted identity is denied.
- Sign-out and expired/tampered sessions fail closed.
- Test every alternate hostname, preview URL, and old deployment.
- Confirm mobile layout and core navigation after hydration.
Do not use a guessed asset path: an anonymous 404 proves only that the path is absent.
7. Diagnose production failures with evidence
- Use
wrangler tail or Workers Logs immediately for 1101/exception responses.
- Inspect browser Network and Console panels when HTML loads but navigation or tabs do not work.
- Compare response status, content type, cache headers, and body for HTML and a known framework chunk.
- Test the smallest failing deployed request before changing architecture.
- Apply the error-specific checks in references/troubleshooting.md.
8. Handoff
Report:
- the canonical private URL and every other route's disposition;
- the protection architecture and exact allowlist, without secrets;
- build, tests, deploy version, anonymous verification, and authenticated verification;
- any OAuth testing-mode expiry, verification requirement, Access plan constraint, or non-Google identity caveat;
- rollback instructions and where future agents should update the allowlist.
Never state that a site is private merely because its root redirects. Privacy requires testing real assets and alternate routes too.
1---2name: deploy-private-cloudflare-site3description: Deploy, protect, debug, and verify a private Cloudflare Workers site for an exact user or email allowlist. Use for Workers or framework sites that must require Cloudflare Access, Google OAuth, or another SSO boundary; when protecting Workers Static Assets and framework chunks; when Wrangler/headless OAuth, secrets, workers.dev routes, errors 1042/1101, or unhydrated pages cause trouble; and when proving that anonymous users cannot retrieve sensitive HTML or assets.4---56# Deploy Private Cloudflare Site78Deploy a site behind a fail-closed identity boundary, preserve framework asset delivery after authentication, and verify the public surface before handing it off.910## Non-negotiable rules1112- Treat private content, source data, build artifacts, and identity-provider credentials as sensitive.13- Prefer Cloudflare Access attached to the Worker. Use application-managed Google OAuth only when Access is unavailable, unsuitable, or explicitly requested.14- Keep the production route disabled or return a fail-closed maintenance response until the identity boundary and required secrets exist.15- Never place client secrets, signing keys, or API tokens in `vars`, source files, command-line arguments, logs, or chat output. Use `wrangler secret put` through interactive or file-based input.16- Do not fabricate API tokens or ask for account passwords. Use Wrangler OAuth, the Cloudflare API MCP OAuth flow, or a narrowly scoped token the user creates.17- Require explicit authorization before transferring an identity-provider secret from one service to Cloudflare.18- Do not declare success from a local build alone. Test the deployed anonymous boundary and one authenticated browser session.19- Retrieve current official Cloudflare and identity-provider documentation before changing live configuration. Cloudflare's Workers and Access surfaces evolve quickly.2021## Load the focused references2223- Read [references/workflow.md](references/workflow.md) before choosing the protection architecture or changing live resources.24- Read [references/troubleshooting.md](references/troubleshooting.md) when deployment succeeds but requests fail, assets 404, or the page does not hydrate.25- Run `scripts/verify-private-boundary.mjs` before handoff.2627## Workflow2829### 1. Inspect before mutating30311. Find repository instructions and inspect the working tree without overwriting unrelated changes.322. Identify the framework, build command, build output, Wrangler version, existing `wrangler.jsonc` or generated config, routes, asset binding, and current public URLs.333. Check Cloudflare identity non-destructively with `wrangler whoami`. If unavailable, use `wrangler login`; use `--browser=false` or device flow in a headless environment.344. Check whether the Cloudflare plugin or Cloudflare API MCP is installed. Use it when available for account configuration, but keep Wrangler for build/deploy/tail operations.355. Enumerate every public path: production hostname, `workers.dev`, preview URLs, custom domains, static chunks, API endpoints, source maps, and old hosts.366. Record the intended allowlist and whether each identity is a Gmail, Workspace, or non-Google address. Do not assume a private-relay address can securely authenticate through Google.3738### 2. Choose one protection architecture3940Use this order:41421. **Worker-level Cloudflare Access**: preferred because every domain and preview attached to the Worker can be protected together.432. **Hostname/path Access**: use when only a specific route or custom hostname should be private, or when WebSockets make Worker-level Access unsuitable.443. **Application-managed OAuth**: use only after documenting why Access is not being used. This adds callback, CSRF, token-validation, session, and asset-gating responsibilities to the application.4546For exact people in Access, create an Allow policy whose Include selector is `Emails` with the complete addresses. Do not use `Everyone`, `Login Methods: One-time PIN`, or a broad email-domain rule when the requirement is an exact allowlist.4748For a non-Google address, Cloudflare Access with email one-time PIN can authenticate ownership without pretending Google is authoritative for that address. If stronger assurance is required, configure its actual identity provider.4950### 3. Build a fail-closed Worker boundary5152For Cloudflare Access:5354- Attach Access to the Worker or exact hostname before enabling the public route.55- Use `ctx.access` only when application code needs the authenticated identity; Access itself should reject unauthorized requests before the Worker runs.56- Confirm the policy is deny-by-default and includes only intended addresses.5758For application-managed Google OAuth:5960- Use the authorization-code flow with an exact HTTPS redirect URI.61- Generate and verify `state`; use `nonce` and PKCE where supported.62- Exchange the code server-side. Validate the ID token signature, `iss`, `aud`, `exp`, nonce, and verified identity claims against current Google guidance.63- Normalize the email for allowlist comparison, but use the Google `sub` claim as the stable account identifier in stored user records.64- For Gmail, Google is authoritative for the address. For Workspace, require `email_verified` and the expected `hd` claim. For non-Gmail addresses without `hd`, flag that Google does not provide continuing authority over the underlying mailbox.65- Sign the application session with a dedicated random secret. Set cookies `Secure`, `HttpOnly`, and `SameSite=Lax` or stricter; set explicit expiry; rotate deliberately.66- Return a generic 403 for a valid Google account not on the application allowlist.6768### 4. Gate HTML and assets together6970When Workers Static Assets are present:7172- Configure an `ASSETS` binding and `assets.run_worker_first: true` when application code performs authentication.73- Authenticate first, then route actual static files through `env.ASSETS.fetch(request)`.74- Do not send framework chunks such as `/_next/static/*`, `/assets/*`, fonts, CSS, or client manifests through a framework request handler that may return a 404 instead of falling through to the asset binding.75- Keep only the minimum sign-in and OAuth callback surface public. Verify that public responses contain no private page text, serialized state, filenames, metadata, or user data.76- Prefer route-pattern `run_worker_first` configuration when it can express the boundary without a wrapper.7778### 5. Configure secrets and deploy safely79801. Declare required secret names with `secrets.required` when the installed Wrangler version supports it.812. Add secrets interactively with `wrangler secret put NAME`. Do not pass values as shell arguments.823. Use a dry run or build before deployment. Inspect the generated Wrangler configuration when using the Cloudflare Vite plugin or another adapter.834. Deploy while `workers.dev` remains disabled if the boundary is incomplete. Remember that Wrangler can re-enable it when config and dashboard disagree.845. Enable only the intended route after Access or the OAuth wrapper is ready.856. Preserve a rollback target or previous Worker version.8687### 6. Verify the privacy boundary8889Run the bundled anonymous verifier with at least one real deployed asset path and several private phrases:9091```bash92node ~/.codex/skills/deploy-private-cloudflare-site/scripts/verify-private-boundary.mjs \93 --url https://example.workers.dev \94 --asset /assets/app.js \95 --forbid "private dashboard" \96 --forbid "customer name"97```9899Then verify manually in a clean browser context:1001011. Anonymous root is redirected to sign-in or returns 401/403.1022. The public sign-in response contains none of the private phrases.1033. A real JavaScript/CSS/image path is also redirected or denied, not 200 or 404.1044. An allowlisted identity completes sign-in and the app hydrates.1055. A non-allowlisted identity is denied.1066. Sign-out and expired/tampered sessions fail closed.1077. Test every alternate hostname, preview URL, and old deployment.1088. Confirm mobile layout and core navigation after hydration.109110Do not use a guessed asset path: an anonymous 404 proves only that the path is absent.111112### 7. Diagnose production failures with evidence113114- Use `wrangler tail` or Workers Logs immediately for 1101/exception responses.115- Inspect browser Network and Console panels when HTML loads but navigation or tabs do not work.116- Compare response status, content type, cache headers, and body for HTML and a known framework chunk.117- Test the smallest failing deployed request before changing architecture.118- Apply the error-specific checks in [references/troubleshooting.md](references/troubleshooting.md).119120### 8. Handoff121122Report:123124- the canonical private URL and every other route's disposition;125- the protection architecture and exact allowlist, without secrets;126- build, tests, deploy version, anonymous verification, and authenticated verification;127- any OAuth testing-mode expiry, verification requirement, Access plan constraint, or non-Google identity caveat;128- rollback instructions and where future agents should update the allowlist.129130Never state that a site is private merely because its root redirects. Privacy requires testing real assets and alternate routes too.