phattarachai/laravel-cloudflare

Purge the Cloudflare edge cache, toggle development mode, register tunnel DNS records, and sync WAF custom rules from artisan — built for apps behind a shared Cloudflare zone.

Maintainers

Package info

github.com/phattarachai/laravel-cloudflare

pkg:composer/phattarachai/laravel-cloudflare

Transparency log

Statistics

Installs: 128

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-08-25 08:37 UTC

This package is auto-updated.

Last update: 2026-08-25 08:38:14 UTC


README

Latest Version on Packagist Tests Code Style PHP Version Laravel Version Total Downloads

Purge the Cloudflare edge cache, toggle Development Mode, and register tunnel DNS records from artisan. Built for a fleet of apps sharing one Cloudflare zone behind a cloudflared tunnel, where purge_everything and Enterprise-only hosts/prefixes purges are both wrong.

Install

composer require phattarachai/laravel-cloudflare
php artisan vendor:publish --tag=cloudflare-config

Set the zone id (a public identifier, safe to commit) and, if you register DNS, the tunnel id:

CLOUDFLARE_ZONE_ID=...
CLOUDFLARE_TUNNEL_ID=...

The API token is resolved at runtime and falls back to the process env, so on a self-hosted runner you inject CLOUDFLARE_API_TOKEN once (in the runner's .env) and no repo needs a secret. A Zone → Cache Purge scope covers purge and dev-mode; add Zone → DNS Edit to use cloudflare:dns, and Zone → Zone WAF Edit to use cloudflare:waf.

Purge

php artisan cloudflare:purge                 # document root + every built asset from the Vite manifest
php artisan cloudflare:purge --url=https://…/sitemap.xml   # plus explicit URLs (repeatable)
php artisan cloudflare:purge --everything --force          # whole zone — nukes every app on a shared zone

Purges by exact files derived from public/build/manifest.json. Make it the last deploy step:

- run: php artisan cloudflare:purge || true

When there is no token or no manifest it exits 0 without calling the API, so dev, tests, and a not-yet-configured repo keep CI green.

Development Mode

php artisan cloudflare:dev-mode          # show current state
php artisan cloudflare:dev-mode on       # bypass the edge cache for ~3 hours
php artisan cloudflare:dev-mode off

DNS

Upsert-only — it never deletes a record it did not declare, so it is safe against the shared zone.

php artisan cloudflare:dns --name=new-app.phattarachai.app --tunnel   # proxied CNAME → <tunnel>.cfargotunnel.com
php artisan cloudflare:dns --name=txt.example.com --type=TXT --content='v=spf1 -all' --dns-only
php artisan cloudflare:dns --list
php artisan cloudflare:dns --name=old.example.com --delete --force

Declare an app's own records in config/cloudflare.php and a bare php artisan cloudflare:dns upserts them — handy as a one-off when spinning up a new subdomain:

'dns' => [
    'records' => [
        ['name' => 'new-app.phattarachai.app', 'tunnel' => true],
    ],
],

WAF custom rules

Upsert-only, keyed by a tag embedded in each rule's description — it never touches a rule it did not declare, so it is safe against the shared zone. The canonical use is a Managed Challenge on /admin: bots and brute-force hit the edge challenge, real browsers pass near-invisibly, and the API is left alone (a challenge needs a browser + cf_clearance cookie, so it would break XHR, mobile, and webhooks). Needs a Zone → Zone WAF Edit token.

php artisan cloudflare:waf                       # sync every rule declared in config
php artisan cloudflare:waf --list                # show the zone's WAF custom rules
php artisan cloudflare:waf --remove=admin-challenge --force

Declare the rules in config/cloudflare.php. Each carries a unique tag, an action (default managed_challenge), and EITHER declarative paths (the command builds the expression and, absent hosts, scopes it to this app's own host) OR a raw expression for full control:

'waf' => [
    'rules' => [
        // declarative — scoped to this app's host automatically
        ['tag' => 'admin-challenge', 'paths' => ['/admin']],

        // raw — pin every env host so all deploys upsert the SAME rule and converge
        // ['tag' => 'admin-challenge', 'expression' => '(starts_with(http.request.uri.path, "/admin")) and (http.host in {"backend-prod.example.com" "backend-qas.example.com"})'],
    ],
],

Verify: a challenged request returns 403 with a cf-mitigated: challenge header (curl always lands here — it can't solve the JS challenge); a passed request has no cf-mitigated header and holds a cf_clearance cookie (HttpOnly — visible in DevTools → Application → Cookies).

Testing

composer test