Skip to content

Cloudflare Web Setup

This is an operational runbook. A trusted operator executes it by hand, once, to bring the Cloudflare account, DNS zone, both Pages projects, and Email Routing into the state ADR 0015 requires and the GCP, WorkOS, and Supabase Exit plan assumes for app.feelproclub.com and app.dev.feelproclub.com. Nothing here is a Terraform apply: the Cloudflare account, zone, Pages projects, custom domains, and Email Routing rules are all configured through the Cloudflare Dashboard.

Prerequisites

  • A Cloudflare account with permission to add sites, create Pages projects, and configure Email Routing.
  • Access to the GoDaddy account that registers feelproclub.com, to change its nameservers.
  • Access to the current DNS records at Vercel, where the domain's DNS was hosted before this migration — not at GoDaddy, which is the registrar only.
  • Permission to add environment variables and a custom domain on each Cloudflare Pages project, and to add or change allowed_origins in infra/environments/dev/main.tf and the WorkOS development redirect allowlist if a preview deployment must call the development API (step 7).

1. Create the Cloudflare account and add the zone

A Cloudflare account is the billing and membership container; a domain added to it is a site with its own zone (the set of DNS records and settings for that domain). Moving a zone between accounts afterward is difficult, but changing who has membership on an account is easy. For that reason:

  1. Create or select the Cloudflare account intended to own the product long-term, not a personal or throwaway account.
  2. Add feelproclub.com as a site in that account. Cloudflare offers to scan for existing DNS records — let it, but do not trust the scan alone (step 2).
  3. When ownership consolidates (for example, moving the product into an organization-owned Cloudflare account), change account membership rather than attempting to move the zone.

2. Capture DNS records from Vercel before switching nameservers

The registrar is GoDaddy, but the domain's DNS was hosted at Vercel, not at GoDaddy. Cloudflare's automatic scan during site setup is best-effort and can miss records a passive scan does not observe. Before changing nameservers, read the authoritative record list directly from the Vercel project's Domains settings and confirm every record below is re-created in Cloudflare. Any record not imported disappears the moment the nameservers cut over, because GoDaddy stops being the effective DNS authority at that point.

The records that existed at Vercel:

TypeHostPoints to
A@ (apex)[VERCEL_APEX_IP]
Aapp[VERCEL_APP_IP]
Aapi[VERCEL_API_IP]
Astaging[VERCEL_STAGING_IP]
Amail[VERCEL_MAIL_IP]

There were no MX records and no TXT records. Do not add either from habit or from a generic template; add only what Email Routing creates in step 8 and what any future sending service requires.

Re-create each A record in the Cloudflare DNS editor with the same host and target before proceeding. Records that will be superseded later in this runbook (the app record, once the production Pages custom domain takes it over in step 5, and the mail-related records, once Email Routing writes its own MX and TXT records in step 8) still need to exist during the cutover window so nothing resolves to nothing while the migration is in progress. app.dev did not exist at Vercel; it is created fresh in step 5.

3. Switch nameservers at GoDaddy

  1. In the Cloudflare Dashboard, copy the two nameservers Cloudflare assigned to the zone.
  2. In the GoDaddy DNS management page for feelproclub.com, replace the existing nameservers with Cloudflare's two nameservers.
  3. Wait for Cloudflare to report the zone as active. Propagation can take up to 24 hours, though it is typically much faster.
  4. Confirm every record captured in step 2 resolves correctly once the zone is active, before relying on it for the Pages custom domains or Email Routing.

4. Create both Cloudflare Pages projects

A Cloudflare Pages project has exactly one *.pages.dev hostname and one Git production branch; branch previews live beneath that hostname (<branch>.<project>.pages.dev), not beside it. Serving two distinct hostnames with two distinct sets of environment variables — production values on one, development values on the other — therefore takes two projects, not one project with two branches:

ProjectProduction branchHostnameCustom domain
feelproclubmainfeelproclub.pages.devapp.feelproclub.com
feelproclub-devdevfeelproclub-dev.pages.devapp.dev.feelproclub.com

Project identity is permanent, so get the name right before creating anything. Two facts make this true:

  • Git integration cannot be added to an existing Pages application. A project created by wrangler pages project create or by a direct upload is permanently direct-upload; converting it means deleting it and starting again, which also releases its *.pages.dev subdomain. Reaching for the CLI to save a few clicks costs the project name.
  • Renaming an existing project means the same thing: deleting it and recreating it, releasing its *.pages.dev subdomain for anyone to claim in the interval. An earlier attempt at the project name fpc collided with an existing project elsewhere on Cloudflare and was silently assigned fpc-41x instead of fpc. If a project by that name exists in the account, delete it rather than reuse it — it is not a usable placeholder for either project above.

Create each project through the Cloudflare Dashboard, and only through the Dashboard: Workers & Pages → Create application → Pages → Import an existing Git repository. A Git-connected project requires installing the "Cloudflare Workers and Pages" GitHub App on the feelproclub organization, which is an app-installation flow that only a GitHub organization owner can complete interactively. No API, connector, or wrangler command can create a Git-connected project. The GitHub App installation is a one-time, account-level step; both projects can authorize against it without reinstalling.

For each project (feelproclub first, then feelproclub-dev):

  1. Connect the Cloudflare Pages project to the feelproclub/fpc GitHub repository, authorizing the GitHub App against the organization when prompted.

  2. Configure the build:

    • Production branch: main for feelproclub, dev for feelproclub-dev.
    • Build command: pnpm --filter @feelproclub/shared build && pnpm --filter fpc-app build
    • Build output directory: apps/app/dist
    • Root directory: the repository root
    • Environment variable NODE_VERSION: 24

    Both projects publish the same build output. apps/app/public/_redirects ships inside apps/app/dist as part of that build, so the single-page-application catch-all rewrite applies identically to both projects with no project-specific configuration.

  3. Add the environment variables the built client reads at build time, matching apps/app/.env.example:

    • VITE_API_URL
    • VITE_WORKOS_CLIENT_ID
    • VITE_SENTRY_DSN
    • VITE_PUBLIC_POSTHOG_KEY
    • VITE_PUBLIC_POSTHOG_HOST

    These are per-project, not shared: feelproclub's Production environment variables hold production values (the production run.app API URL, the production WorkOS client id); feelproclub-dev's hold development values (the development run.app API URL, the development WorkOS client id). A single project cannot cleanly serve both value sets at once, which is as much the reason for splitting the project as the two hostnames are. Set feelproclub-dev's Preview environment variables to the same development values, since step 6 confines that project's non-production branches to previews that should exercise the development API. feelproclub's Preview environment does not need values: step 6 restricts that project to building only its production branch, so no Preview build ever runs there.

    VITE_WORKOS_REDIRECT_URI is optional and normally left unset on both projects. Unset, the client derives its callback from the origin it is served from, which is correct for app.feelproclub.com, app.dev.feelproclub.com, and any preview URL. Set it only when the callback must differ from the serving origin — a deployment reached through a proxy, for instance. Step 7 explains why setting it does not remove the need to allowlist preview origins.

    Do not commit any of these values to the repository; apps/app/.env.example documents only placeholders.

  4. Trigger a build and confirm it succeeds before moving on to the custom domain.

5. Add the custom domains

Multi-level subdomains are fine for Pages custom domains, and not for everything else. A Pages custom domain provisions its own certificate per domain, so app.dev.feelproclub.com is issued a valid certificate and works on the free plan; this was verified against the live deployment. The zone's free Universal SSL certificate is a different mechanism and does cover only the root domain and its first-level subdomains, so a hostname served by a proxied DNS record rather than by a Pages custom domain — a future api.dev.feelproclub.com pointing at Cloud Run, for instance — would rely on that zone certificate and may not be covered. Verify certificate issuance for any such hostname before depending on it, rather than assuming the Pages behaviour generalises.

  1. On the feelproclub project's Custom domains tab, add app.feelproclub.com.
  2. On the feelproclub-dev project's Custom domains tab, add app.dev.feelproclub.com.
  3. Cloudflare manages the required DNS record for each domain automatically once it is verified active on the zone. app.feelproclub.com supersedes the app A record captured in step 2; app.dev.feelproclub.com is a new record with no prior entry to supersede.
  4. Confirm https://app.feelproclub.com serves the feelproclub project and https://app.dev.feelproclub.com serves the feelproclub-dev project before treating the cutover as complete.

6. Restrict preview deployments to the development project

Both projects watch the same feelproclub/fpc repository, so without a restriction a feature branch would build in both projects and produce two preview URLs — one per project, for the same commit. That is redundant rather than useful, since only one of them (the development project, wired to the development API and WorkOS environment) is a preview anyone should actually use.

In the feelproclub project's build settings, restrict automatic deployments to its production branch (main) only, so pushes to other branches do not also build there. Leave feelproclub-dev building every branch, which is what makes it the previews project. If this setting cannot be located in the current Dashboard, treat it as a known gap rather than blocking the runbook on it: the consequence is duplicate builds on non-main branches — noisy build minutes and an extra unused preview URL per push — not a broken or insecure deployment.

7. The preview-deployment CORS and redirect consequence

Every non-production branch pushed to feelproclub-dev gets two native preview URLs: a per-commit URL (<hash>.feelproclub-dev.pages.dev) and a stable branch alias (<branch>.feelproclub-dev.pages.dev) that always points at that branch's latest commit. Branch names are lowercased and non-alphanumeric characters are replaced with hyphens to form the alias.

  • If a preview build must call the development API, its exact origin must be added to allowed_origins in infra/environments/dev/main.tf, and the same origin's /auth/callback redirect URI must be added to the WorkOS development environment's allowlist (see WorkOS development setup). Without both, the preview build's requests fail CORS and its sign-in redirect is rejected.
  • Per-commit URLs are unbounded, so allowlisting each one is not practical. The stable feelproclub-dev.pages.dev origin (the branch alias for dev itself) is the one allowlisted by default; treat individual per-commit preview URLs as visual-only unless a specific one is deliberately allowlisted for a short-lived reason and removed afterward.
  • Production needs neither change. app.feelproclub.com is the unchanged production origin; it is not a *.pages.dev address, and infra/environments/prod/main.tf's allowed_origins already reads https://app.feelproclub.com. Do not add a pages.dev origin to the production root.

Pinning VITE_WORKOS_REDIRECT_URI to a fixed address does not avoid this. It sends the user back to that deployment after sign-in rather than to the preview they started from, so the preview never receives a session. A preview that must authenticate needs its own origin allowlisted; no configuration removes that requirement.

8. Enable Email Routing

  1. On the zone's Email Routing page, enable Email Routing. Cloudflare adds its own MX records and an SPF TXT record to the zone automatically; do not hand-write these.
  2. Add and verify a destination address — the existing mailbox that will actually receive forwarded mail. Verification requires clicking a confirmation link sent to that address.
  3. Create routing rules:
    • manu@feelproclub.com → the verified destination address.
    • support@feelproclub.com → the verified destination address.
    • A catch-all rule → the verified destination address, so an address not explicitly listed still reaches someone instead of bouncing.

Two facts matter operationally and are easy to assume away:

  • Email Routing forwards but cannot send. It receives mail addressed to the domain and forwards it to the destination address; it has no mechanism for sending mail from an address on the domain. Replying as support@feelproclub.com or similar requires a separate outbound sending service — Email Routing does not provide one.
  • The support@feelproclub.com Google Group is now vestigial. Because the zone's MX records point at Cloudflare, Google can no longer receive mail for the domain, so any Google Group configured against support@feelproclub.com receives nothing. It is not deleted by this runbook, but it no longer does anything; do not rely on it for support intake.

Verification

  • https://app.feelproclub.com loads the application, served by the feelproclub Cloudflare Pages project rather than Vercel, and https://app.dev.feelproclub.com loads the application served by the feelproclub-dev project.
  • A direct request to a client-side route on either domain — for example https://app.feelproclub.com/invite/[SOME_CODE] — returns the application (a 200 serving index.html through the single-page-application catch-all rewrite), not a 404. This is the specific failure the rewrite exists to prevent: without it, only the root path resolves and every deep link into the invite flow breaks.
  • dig feelproclub.com A, dig app.feelproclub.com, dig app.dev.feelproclub.com, dig api.feelproclub.com, dig staging.feelproclub.com, and dig mail.feelproclub.com all resolve, matching what step 2 captured or what later steps in this runbook superseded.
  • dig feelproclub.com MX returns Cloudflare's Email Routing MX records, and dig feelproclub.com TXT includes the SPF record Cloudflare added.
  • Sending a test message to manu@feelproclub.com and to support@feelproclub.com each arrives at the verified destination address. A message to an address with no explicit rule (for example random@feelproclub.com) also arrives there, through the catch-all.
  • Pushing a non-main branch produces a build in feelproclub-dev only, confirming step 6's build-branch restriction is in effect (or, if the restriction could not be configured, confirms the accepted duplicate-build gap and nothing worse).
  • The stable feelproclub-dev.pages.dev branch-alias origin loads without a CORS error once it has been added to the development allowed_origins and WorkOS development redirect allowlist per step 7. Production sign-in and API calls continue to work with no pages.dev origin added, confirming the non-consequence for production is real and not just documented.