A self-hosted dashboard for managing multiple Cloudflare accounts from one place. Connect accounts with API Tokens or Cloudflare OAuth, then aggregate Zones, DNS, Workers, Pages, R2 storage, D1 databases, usage analytics, and email sending behind Cloudflare Access authentication.
Built with Astro 7 + React + DaisyUI and deployed to Cloudflare Workers with D1. The UI is bilingual (English / 简体中文) with a language switcher in the header — the choice is remembered via cookie and defaults to the browser language.
- Multi-account aggregation — add API Tokens or authorize a public OAuth client with PKCE, verify connection health, search/paginate accounts, and manage everything from a single dashboard. Credentials are encrypted at rest and de-duplicated per user; OAuth access tokens refresh automatically.
- Dashboard overview — quickly see account health, cached Zone totals, Workers scripts, Pages projects, and last sync status.
- Zones & DNS management — sync Zones across all accounts, filter by domain/status/account, inspect Zone metadata, and create/update/delete DNS records through the Cloudflare API.
- Workers management — list scripts across accounts, inspect bindings/settings/history/deployments, view and edit single-module source, manage cron triggers, secrets, workers.dev URLs, and custom domains when the token has edit scope.
- Pages management — browse Pages projects, inspect deployments/logs/domains, trigger deployments, retry/rollback deployments, purge build cache, and attach custom domains with optional DNS record creation.
- R2 storage management — sync buckets across accounts, create/delete buckets, and browse objects with folder navigation and pagination. Uploads and downloads go browser-direct through presigned S3 URLs (no server relay); downloads are forced as real attachments. Objects preview in-app — images, text/code, Markdown (sandboxed), PDF, video/audio — with text previews server-relayed under a 1 MB cap. Bucket settings cover public access (r2.dev and custom domains), CORS, and lifecycle rules, and each bucket has storage / Class A+B operations charts.
- D1 database management — sync D1 databases across accounts, create databases (with an optional primary-location hint), and delete them behind a type-the-name confirmation. Each database gets a detail page with a table browser (schema and row counts), a SQL query console (bind parameters, multi-statement batches, an extra confirmation before write statements, local query history), and a read-replication toggle.
- Usage analytics — view Workers and Pages Functions invocation counts with 24h hourly snapshots, 7d/30d daily snapshots, searchable tables, account filters, and trend charts. Completed ranges are cached in D1, including zero-traffic buckets, so normal range switching avoids repeated Cloudflare API fan-out; the refresh action explicitly rebuilds the selected range.
- Email sending — configure verified sending domains backed by either Resend (stored API key) or Cloudflare Email Sending (reuses an existing account credential), then compose and send mail from the dashboard. Cloudflare-backed domains can be onboarded through the API, including the required DNS records. Bodies support Markdown / HTML / plain text with a live preview, and every send is written to a searchable, auditable log (success or failure) you can review and re-render later.
- Cloudflare Access login — protect the dashboard with Cloudflare Access, verify the
Cf-Access-Jwt-AssertionJWT on every request, and use the authenticated email as the per-user data boundary. - Per-user data isolation — every user-owned query is scoped by authenticated email, so one Access user cannot see another user's accounts or caches.
- Resilient sync model — upstream Cloudflare data is cached in D1 using atomic per-account refreshes; one bad token is reported as an account failure without blocking other accounts.
- Bilingual, mobile-friendly UI — Simplified Chinese is the default locale, English routes are available under
/en, and the main views are designed for narrow screens without page-level horizontal overflow.
| Dashboard | Workers & Pages |
|---|---|
![]() |
![]() |
| Zones | Usage analytics |
|---|---|
![]() |
![]() |
- Runtime & framework — Astro 7 SSR with the Cloudflare adapter targeting Cloudflare Workers / workerd.
- Interactive UI — React 19 islands for feature panels, Tailwind CSS 4 and DaisyUI 5 for the design system, lucide-react icons, and Recharts for usage charts.
- Cloudflare platform — Cloudflare Workers for hosting, D1 for SQLite-backed storage, and Cloudflare Access for Zero Trust authentication.
- Authentication pipeline — Astro middleware verifies Access JWTs against the team JWKS, stores the authenticated email in
locals.userEmail, supports a guardedDEV_MODEbypass for local development, and uses/cdn-cgi/access/logoutfor Access session logout. - Cloudflare integration — official
cloudflareSDK wrapped by a localCfClient, with REST and GraphQL fallbacks for endpoints not covered cleanly by the SDK. - Storage & security — D1 cache tables mirror Cloudflare API resources; API tokens are encrypted with AES-GCM and matched by SHA-256 token hashes for duplicate detection.
- Routing & localization — Astro file-based routes, API routes under
src/pages/api, and a small i18n layer for Chinese / English routing and strings. - Quality tooling — Vitest unit tests, an in-memory
better-sqlite3D1 test double that runs real migrations,astro check+ TypeScript, and Biome for linting/formatting.
- Node.js >= 22.12.0
- A Cloudflare account with Workers, D1, and Zero Trust (Access) available
- Wrangler (installed as a dev dependency)
This project can run with npm or pnpm, but use only one package manager for a given
node_modulesinstall. The repository includes apnpm-lock.yaml; pnpm is recommended for day-to-day local development.
-
Install dependencies:
pnpm install
If pnpm reports ignored build scripts, approve the native/runtime packages and rebuild them:
pnpm approve-builds better-sqlite3 esbuild workerd sharp pnpm rebuild
better-sqlite3is required by the local D1 test helper and dev D1 binding;esbuild/workerd/sharpare used by the Astro and Cloudflare toolchain. If you use npm instead, runnpm installand do not mix the resulting install with pnpm in the samenode_modules. -
Create your Wrangler config (gitignored):
cp wrangler.toml.example wrangler.toml
The default
database_idplaceholder is fine for local D1; you only need a real id for remote deploys. -
Copy the example env file and generate a random
ENCRYPTION_KEY:cp .dev.vars.example .dev.vars node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Paste the generated value into
ENCRYPTION_KEYin.dev.vars. KeepDEV_MODE=trueand leaveCF_ACCESS_*unset for local development. -
Create the local D1 database and run migrations:
pnpm run db:migrate
Migration files live in
migrations/; add new changes asNNNN_description.sql. -
Start the dev server (Astro dev with HMR):
pnpm run dev
Runs on port 4321 with a live D1 binding (backed by
better-sqlite3) and.dev.varsloaded.DEV_MODE=truebypasses Cloudflare Access for local development.To verify a production build locally against the Workers runtime instead, use Wrangler:
pnpm run build pnpm run preview
-
Before submitting changes, run the standard checks:
pnpm run typecheck pnpm run test
See FAQ for common local setup issues, including better_sqlite3.node binding errors and Access-related 403s.
Note:
DEV_MODEbypass and Access config are mutually exclusive (a security guard rail). If both are set, the bypass is disabled and you'll get a local 403. KeepCF_ACCESS_*unset locally.
-
Create your
wrangler.toml(it is gitignored because it holds your own database id):cp wrangler.toml.example wrangler.toml
Then create the remote D1 database and paste the returned
database_idintowrangler.toml:wrangler d1 create cloudflareops-db
-
Apply migrations to the remote database:
npm run db:migrate:remote
-
In the Cloudflare Zero Trust dashboard, create a self-hosted Access application pointing at your Worker domain or attached custom domain. This gives you the two Access values the app needs:
Variable Where to find it CF_ACCESS_TEAM_DOMAINYour team domain, <your-team>.cloudflareaccess.com. Zero Trust → Settings → Custom Pages (or the URL you log in at). Value is the full host, e.g.acme.cloudflareaccess.com— nohttps://.CF_ACCESS_AUDZero Trust → Access → Applications → (your app) → Overview, field Application Audience (AUD) Tag — a long hex string. -
Set the three runtime values on the Worker (do not set
DEV_MODEin production).ENCRYPTION_KEYmust be a secret; the Access values may be secrets or plain Wrangler variables:- Dashboard: Compute (Workers) → your Worker → Settings → Variables and Secrets.
- CLI:
npx wrangler secret put ENCRYPTION_KEY npx wrangler secret put CF_ACCESS_TEAM_DOMAIN npx wrangler secret put CF_ACCESS_AUD
Variable Value ENCRYPTION_KEYThe 64-hex-char key you generated for local dev (or a fresh one) CF_ACCESS_TEAM_DOMAINe.g. acme.cloudflareaccess.comCF_ACCESS_AUDThe AUD tag from step 3 Important:
CF_ACCESS_TEAM_DOMAINandCF_ACCESS_AUDare what gate the app. If either is missing in production the app returns 500 "Cloudflare Access not configured"; if the request carries no valid Access JWT it returns 403. Also make sure the Access application's domain matches the Worker domain or custom domain, otherwise the JWT audience/issuer check fails. -
Optional: enable Connect with Cloudflare on the Accounts page by registering a self-managed OAuth client in Cloudflare (Manage Account → OAuth clients):
- Client type: public client, with no client secret.
- Token endpoint authentication method:
None. - PKCE: required, method
S256. - Grant types:
authorization_codeandrefresh_token; response type:code. - Redirect URI:
https://<your-worker-domain>/api/accounts/oauth/callback. - Allowed scopes must exactly include every value requested through
CF_OAUTH_SCOPES. Addoffline_accessfor refresh tokens. Do not add unsupported OIDC-style scopes such asopenidor the invalidaccount.readscope. - Email onboarding requires
email-sending.readandemail-sending.write; the application appends these two required scopes automatically.
Configure the runtime variables below. If
CF_OAUTH_CLIENT_IDis absent, OAuth is disabled and manual API Token connections remain available.Variable Value CF_OAUTH_CLIENT_IDOAuth client ID CF_OAUTH_REDIRECT_URIExact registered callback URI; if omitted, it is derived from the request origin CF_OAUTH_SCOPESSpace-separated Cloudflare scope IDs matching the OAuth client's allowed scopes See
.dev.vars.examplefor the complete scope example. Access tokens and refresh tokens are encrypted in D1; expired access tokens are refreshed automatically. -
Deploy:
npm run deploy
The command builds the Astro application and deploys it with
wrangler deploy. For repository-based deployment, configure Workers Builds with build commandnpm run buildand deploy commandnpx wrangler deploy.
You can connect accounts through a manual API Token or the optional OAuth client described above. In either case, the granted permissions determine which features are available. Recommended minimum scopes:
- Zones / DNS: Zone → Zone: Read; Zone → DNS: Edit
- Workers & Pages: Account → Account Settings: Read (to resolve which account the token belongs to); Account → Workers Scripts: Read; Account → Cloudflare Pages: Read
- Optional — Pages write (retry/rollback deployments, domain management, trigger deploys, purge cache): Account → Cloudflare Pages: Edit
- Optional — Workers write (in-browser edit/deploy, cron, secrets, custom domains): Account → Workers Scripts: Edit
- Optional — Usage page (Workers/Pages Functions invocation counts): Account → Account Analytics: Read
- Optional — R2 storage: Account → Workers R2 Storage: Read is enough to browse buckets/objects, preview, and download (presigned S3 credentials are derived from the token, so its R2 scope applies to transfers too); uploads, object deletion, bucket create/delete, and settings changes need Edit. The bucket usage tab additionally uses Account Analytics: Read.
- Optional — D1 databases: Account → D1: Read is enough to sync/list databases, inspect details, browse tables, and run read-only queries; creating/deleting databases, write statements in the query console, and the read-replication toggle need Edit.
- Optional — Email sending via Cloudflare (only if you configure a Cloudflare-backed sending domain; Resend domains use a separate API key and need no Cloudflare scope): Account → Email Sending: Edit. The domain must also be verified for sending in your Cloudflare account first.
Notes:
- Read scopes are enough for view-only features; missing Edit scopes surface a 403 error on the corresponding action button without affecting anything else.
- Accounts missing Workers/Pages scopes are recorded as failed per-account during sync (see the Accounts page) and do not block other accounts.
Commands are shown with npm run because they are package scripts; pnpm run <script> works the same when using pnpm.
| Script | Description |
|---|---|
npm run dev |
Astro dev server (Access bypassed via DEV_MODE) |
npm run build |
Build for production |
npm run preview |
Preview the production build with wrangler dev |
npm run deploy |
Build and deploy to Cloudflare Workers |
npm run typecheck |
astro check + tsc --noEmit |
npm run lint |
Run Biome lint |
npm run format |
Format files with Biome |
npm run check |
Run Biome check and write safe fixes |
npm run check:ci |
Run Biome in CI mode |
npm run test |
Run the Vitest suite |
npm run db:migrate |
Apply D1 migrations locally |
npm run db:migrate:remote |
Apply D1 migrations to remote |
src/
components/ React island panels (Accounts, Zones, DNS, Workers, Pages, R2, D1, Usage, Email, Dashboard)
pages/ Astro routes + API endpoints (src/pages/api)
server/ Server-side services (usage, sync, r2, d1, email, etc.)
lib/ Shared utilities (Cloudflare client, crypto, ...)
i18n/ Bilingual strings
middleware.ts Access authentication
migrations/ D1 SQL migrations (NNNN_description.sql)
tests/ Vitest unit tests
Issues and pull requests are welcome. Please run pnpm run typecheck and pnpm run test before submitting.
MIT © 2026 bearboy80



