{
  "$type": "site.standard.document",
  "description": "Cloudflare Tunnel changed local integration testing for me because it gave local services stable HTTPS names that behave like the real app boundary.",
  "path": "/cloudflare-tunnels-changed-how-i-test-local-integrations/",
  "publishedAt": "2026-06-25T08:13:00.000Z",
  "site": "at://did:plc:bryys25pc2fnagnyxqgsglhd/site.standard.publication/3mn26bjkkmh23",
  "tags": [
    "Tools",
    "Web"
  ],
  "textContent": "I used to treat local integration testing as a special mode. Run the app on localhost, add a forwarding URL when a provider needed to call back, update a dashboard, test the thing, then undo half of it later.\n\nCloudflare Tunnel changed that for me. My local apps now have stable HTTPS dev domains, so OAuth callbacks, webhooks, mobile callbacks, and browser testing go through the same public boundary every time.\n\nTHE MENTAL MODEL\n\nBefore tunnels, local testing looked like this:\n\nbrowser -> http://localhost:5173\nbackend -> http://localhost:4000\nprovider dashboard -> random forwarding URL, if I remembered to update it\nphone -> maybe my LAN IP, maybe nothing\n\nThat works for testing screens. It is awkward for integrations because the other service is not on my laptop.\n\nNow I try to make local development look like a small public deployment:\n\nbrowser\n  -> https://dev-example.com\n  -> Cloudflare Tunnel\n  -> http://localhost:5173\n\nStripe or GitHub\n  -> https://dev-example-backend.com/webhooks/provider\n  -> Cloudflare Tunnel\n  -> http://localhost:4000/webhooks/provider\n\nphone\n  -> https://dev-example.com\n  -> Cloudflare Tunnel\n  -> http://localhost:5173\n\nIt is still local. The processes are still on my machine. But the URLs look like real URLs, with HTTPS, stable hostnames, and provider dashboards that do not need to change every time I restart a terminal.\n\nSTABLE HOSTNAMES REMOVE A LOT OF DASHBOARD WORK\n\nCloudflare has Quick Tunnels for development. You can run:\n\ncloudflared tunnel --url http://localhost:8080\n\nIt prints a random trycloudflare.com hostname and proxies requests back to your local server. Cloudflare describes quick tunnels as useful for testing and development. I would not put a production site behind one.\n\nI like quick tunnels for one-off sharing. I do not like them for normal product development.\n\nProvider dashboards, OAuth clients, and mobile apps all remember URLs. If the hostname changes, the test setup changes.\n\nFor my own projects I use a named tunnel with hostnames I control. The Cloudflare setup is a little more work once, then the same URLs keep working:\n\ncloudflared tunnel create dev\ncloudflared tunnel route dns dev dev-example.com\ncloudflared tunnel route dns dev dev-example-backend.com\n\nCloudflare documents that a tunnel gets a UUID-backed cfargotunnel.com target, and DNS records point your hostname at that tunnel target. The CLI route command creates the CNAME for locally-managed tunnels.\n\nAfter that, my provider dashboard can keep using:\n\nhttps://dev-example-backend.com/webhooks/stripe\nhttps://dev-example-backend.com/oauth/google/callback\n\nNo random URL pasted into five places. No \"which tunnel URL is current?\" problem.\n\nTHE CONFIG BECOMES THE TEST MAP\n\nMy ~/.cloudflared/config.yml is the map:\n\ntunnel: <tunnel-id>\ncredentials-file: /Users/me/.cloudflared/<tunnel-id>.json\n\ningress:\n  - hostname: dev-example.com\n    service: http://localhost:5173\n  - hostname: dev-example-backend.com\n    service: http://localhost:4000\n  - service: http_status:404\n\nCloudflare's configuration docs say ingress rules are matched from top to bottom, and configs with ingress rules need a final catch-all rule. I use http_status:404 because unmatched hostnames should fail clearly.\n\nThis file answers the question I used to keep in my head:\n\npublic hostname -> local port\n\nWhen an integration breaks, I start with that mapping. I do not start in the provider dashboard.\n\nI PROVE THE ROUTE BEFORE CHANGING PROVIDER SETTINGS\n\nWhen a callback or webhook fails, I check the path from outside in.\n\nFirst, DNS:\n\ndig +short dev-example-backend.com\n\nThen HTTPS reachability:\n\ncurl -i https://dev-example-backend.com/health\n\nThen the actual route:\n\ncurl -i \\\n  -X POST \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"ping\":true}' \\\n  https://dev-example-backend.com/webhooks/provider\n\nFor webhook endpoints, I do not care if the fake request returns 400, 401, or signature verification failure. I care that the request reaches the local process and the log shows the route I expected.\n\nThen I ask Cloudflare which ingress rule it would use:\n\ncloudflared tunnel ingress rule https://dev-example-backend.com/webhooks/provider\n\nAnd I validate the config:\n\ncloudflared tunnel ingress validate\n\nThose two commands are more useful than staring at dashboard settings. One tells me which rule matches the URL. The other catches config mistakes before I blame Stripe, GitHub, Google, or my app.\n\nTHE APP SEES A REAL HOST\n\nThis also changed how I debug app config.\n\nMany local bugs only show up when the app uses a public origin:\n\n * OAuth redirect URLs must match the registered value. Google's OAuth docs say the redirect URI must exactly match an authorized redirect URI, or you get redirect_uri_mismatch.\n * Webhook providers send requests to an HTTPS endpoint you register. Stripe's webhook docs describe registering an HTTPS webhook endpoint that receives event data.\n * Cookies, CORS, callback URLs, generated absolute URLs, and backend allowlists often depend on the request host.\n\nIf I test everything on raw localhost, I can accidentally test a different app than the one I configured.\n\nWith tunneled dev domains, I can set local env vars to the same kind of values I use elsewhere:\n\nAPP_URL=https://dev-example.com\nAPI_URL=https://dev-example-backend.com\nGOOGLE_REDIRECT_URI=https://dev-example-backend.com/oauth/google/callback\nSTRIPE_WEBHOOK_URL=https://dev-example-backend.com/webhooks/stripe\n\nThe values are still development values. But they have the same scheme, hostname behavior, and callback path behavior as the deployed app.\n\nThat catches more useful bugs.\n\nONCE I PICK A DOMAIN, I USE IT\n\nI still use localhost as the tunnel target. I avoid using it as the app URL once I settle on a domain name.\n\nThe local process still runs on a port:\n\nhttp://localhost:5173\n\nBut I open the app through the tunnel subdomain:\n\nhttps://dev-example.com\n\nI usually put the project domain name into the tunnel subdomain. If the product is example.com, the local URLs become something like:\n\nhttps://dev-example.com\nhttps://dev-example-backend.com\n\nThat keeps the browser, backend config, OAuth callback URLs, webhook endpoints, cookies, and mobile testing on the same dev origin. I do not have one path through localhost and another path through the domain.\n\nMy rule is:\n\n * before the project has a domain: local ports are fine\n * after the tunnel subdomain exists: use the domain as the app URL\n * provider calls my machine: use the tunnel domain\n * phone or tablet testing: use the tunnel domain\n * one-off share link: quick tunnel is fine\n\nThe tunnel config still points to localhost. I just stop treating localhost as the URL I develop against.\n\nWHAT CHANGED IN PRACTICE\n\nBefore, an integration failure felt spread across too many places: local server, forwarding tool, provider dashboard, callback config, browser console, phone, and logs.\n\nNow I start with the public route:\n\n * Does the hostname resolve?\n * Does HTTPS work?\n * Does Cloudflare route the hostname to the local port I expect?\n * Does the app log the request?\n * Does the provider dashboard use the stable dev URL?\n * Is the app configured with the same dev origin?\n\nOnly after that do I debug provider-specific behavior: signatures, event types, OAuth scopes, redirect URI registration, retries, and handler code.\n\nI wrote the setup steps in Configuring Cloudflare Tunnel to Expose Servers for Local Development, Webhooks etc. Later I wrote about treating ~/.cloudflared/config.yml as my local dev directory.\n\nThis is the reason behind both posts: stable HTTPS dev domains make local integrations feel less like a pile of exceptions.",
  "title": "Cloudflare Tunnels Changed How I Test Local Integrations"
}