Cloudflare Edge (Routing + WAF)¶
Cloudflare sits in front of the whole platform for security (WAF, TLS, bot/DDoS
mitigation, cache) and as the request router. The coexistence of the Website and
WordPress on the same hostname swapps.com is resolved with Workers Routes
in the swapps.com zone.
Full zone reference
The detailed zone configuration — DNS (66 records), SSL/TLS and certificates,
all security rules (rate limiting, custom and managed WAF rulesets, config
rules), cache, page rules, the 48 Workers Routes and recommendations — is
consolidated in the repo's CLOUDFLARE.md. This page summarizes the
essentials; to audit or change something, go to that document.
Zone at a glance¶
| Aspect | Current value |
|---|---|
| Zone / plan | swapps.com · Pro · 11f7934f8dcfecb73fa95691f02b1dd1 |
| Account | Swapps · b7edd98532935f2d2477d46a266cd1bc |
| DNS | 66 records (8 A, 10 AAAA, 19 CNAME, 10 MX, 18 TXT, 1 CAA); email on Google Workspace + Mailgun + SendGrid |
| TLS / SSL | Min TLS 1.0 · mode Full · HSTS injected by the Worker (not at zone level) |
| Certificates | Universal Google Trust Services (active) + Sectigo (backup) |
| Rate limiting | Login 15/60s and General 120/60s (excludes static assets) — Challenge action |
| WAF managed | Cloudflare Managed Ruleset + OWASP Core Ruleset deployed |
| IP Access Rules | None |
Recommended pending items
See CLOUDFLARE.md §7 for detail/priority: raise Min TLS to 1.2, evaluate SSL
Full (Strict), enable HSTS at the zone level, and review the "Allow scraping"
config rule that leaves a fixed IP with security nearly disabled.
Routing mechanism¶
There is no "WordPress worker". The swapps-client Worker captures all of
swapps.com via a wildcard route (swapps.com/*) plus explicit per-section routes.
The routes WordPress must serve are marked "Workers disabled": on those the
Worker does not run and Cloudflare serves the request directly from the
origin (WordPress on Pantheon). In other words, WordPress is the origin of the
domain and the Worker only intercepts the React site's routes.
Routes that run the swapps-client Worker (Website, React SSR)¶
| Route | Notes |
|---|---|
swapps.com and swapps.com/* |
Wildcard: captures the whole site |
swapps.com/assets/* |
React site assets |
swapps.com/services* |
Services section |
swapps.com/case-studies*, .../case-studies/sector* |
Case studies |
swapps.com/get-swapps* |
Get Swapps landing |
swapps.com/about-us* |
About us |
swapps.com/blog* |
Blog (SSR render; content comes from the worker → WP REST API) |
swapps.com/es* |
Spanish site |
swapps.com/style-guide* |
Style guide |
Routes that run the swapps-ai Worker (AI)¶
| Route | Notes |
|---|---|
swapps.com/api/ai/* |
Plan recommendation and refinement endpoint, served by Cloudflare Workers AI. |
Routes with "Workers disabled" → served by the origin (WordPress / Pantheon)¶
| Route | Notes |
|---|---|
swapps.com/wp-content/* |
CMS media and assets |
swapps.com/wp-includes/* |
WordPress core |
swapps.com/wp-admin/* |
CMS backoffice |
swapps.com/wp-login.php* |
WordPress login |
swapps.com/wp-json/* |
WP REST API (consumed by the Platform worker) |
swapps.com/wp-cron.php |
WordPress cron |
swapps.com/health |
Origin health check |
swapps.com/robots.txt |
robots.txt served by WordPress |
Mental rule
On swapps.com, WordPress routes (/wp-*, plus /health and /robots.txt)
are "Workers disabled" and fall to the origin; the Worker swapps-client
serves the rest. When adding a new site section, the wildcard usually suffices;
when exposing something new from WordPress, you must add its route with
"Workers disabled".
Other hostnames don't use these routes:
app.swapps.comresolves to the App (Django, K8s/Argo CD ingress) andswapps-worker.swapps.workers.devis the Platform worker's ownworkers.devdomain.
WAF exceptions for server-to-server traffic¶
Cloudflare applies a Managed Challenge (Super Bot Fight Mode) to traffic that
looks automated. A browser solves the JS challenge and passes; a
server-to-server client (an inbound webhook, another server, a Worker)
cannot solve it and gets HTTP 403 with the cf-mitigated: challenge header.
So each server-to-server ingress needs a WAF exception (Skip action) scoped by
route.
Gotcha: Super Bot Fight Mode runs in its own phase
Super Bot Fight Mode runs in the http_request_sbfm phase, distinct from WAF
Managed Rules (http_request_firewall_managed). Marking Skip → "All managed
rules" does not disable the bot challenge; you must mark the
Super Bot Fight Mode component.
Current rules (WAF → Custom rules):
| Zone | Inbound traffic | Match (summary) | Skip |
|---|---|---|---|
app.swapps.com |
ClickUp / Stripe / Zapier webhooks | POST and path starts with /services/webhooks/clickUp, /subscriptions/webhooks/stripe or /zapier/webhooks |
Super Bot Fight Mode |
swapps.com |
Platform worker → WP REST API | path starts with /wp-json/wp/v2/ and cf.worker.upstream_zone ne "" |
bot challenge |
swapps.com |
API requests to AI | path starts with /api/ai/ |
Super Bot Fight Mode |
Reference expression for the app.swapps.com rule:
(http.request.method eq "POST" and (
starts_with(http.request.uri.path, "/services/webhooks/clickUp") or
starts_with(http.request.uri.path, "/subscriptions/webhooks/stripe") or
starts_with(http.request.uri.path, "/zapier/webhooks")
))
Match criteria and scope:
- External webhooks → by path +
POSTmethod. They are not Workers, socf.worker.upstream_zonedoesn't apply; each endpoint validates its own signature/token (ClickUpX-Signature, StripeSTRIPE_WEBHOOK_SECRET, Zapier token), so the skip only removes the bot challenge, not authentication. The path is case-sensitive (e.g.clickUpwith a capital U): it must match what each provider has registered. - Worker → WordPress → by path +
cf.worker.upstream_zone(only Worker subrequests from the account); it does not open/wp-jsonto external traffic. - The skip is deliberately narrow: broad routes like
app.swapps.com/api/*or the rest of the site remain challenged. Only specific server-to-server ingress routes are excepted.
When adding a new inbound integration
When adding a new inbound integration (a webhook from another provider, or
another app that consumes the API server-to-server), you must add its route to
the corresponding Skip rule; otherwise it will fail with a silent 403 the
app never sees.