The empire is a single git repo containing 46 sites, 32 APIs, and 15 shared packages. Everything ships to Cloudflare — Pages for the sites, Workers for the APIs, D1 for the databases, R2 for the assets. One billing account, one deploy script, one wrangler config per node.
People keep asking how this stays manageable solo. It’s mostly about the platform layer. The folder structure does some of the work, but the real lock is a tiny set of central Workers that every site delegates to.
The shape
The folder naming is canonical: folder name equals deploy domain. apps/sites/maheshwaghmare.com/ ships to https://maheshwaghmare.com. apps/apis/private/platform/notify.surror.workers.dev/ ships to https://notify.surror.workers.dev. No translation, no guessing.
The three rules
After a year of mistakes that almost killed this setup:
The platform layer
Ten Workers do the heavy lifting for every site:
| Worker | What it does |
|---|---|
auth.surror.workers.dev | JWT issuance, OAuth, app passwords |
keys.surror.workers.dev | API key creation + verification |
credits.surror.workers.dev | Per-user credit balance + spend |
billing.surror.workers.dev | Razorpay webhooks, invoice generation |
email.surror.workers.dev | Resend wrapper with template registry |
notify.surror.workers.dev | Newsletter subscriptions + fan-out notifications |
media.surror.workers.dev | R2 uploads + image transforms |
usage.surror.workers.dev | Per-customer usage metering |
voting.surror.workers.dev | Up/down vote primitive for community sites |
attribution.surror.workers.dev | UTM + referral tracking |
A new site that needs auth + email + billing inherits all three by adding three secret tokens to its wrangler.jsonc and calling the central Workers via fetch(). Time to wire up auth for a new SaaS: under an hour.
The deploy script
There’s no fancy CI. Each node has a per-folder package.json with a deploy script. The repo-level helper just loops:
# Deploy a single site
cd apps/sites/maheshwaghmare.com
npm run deploy
# Or the entire site fleet (rarely needed)
npm run deploy:sites
Wrangler is the workhorse. Cloudflare’s edge handles the parallelism. The slow part is astro build per site (8-15 seconds), not the deploy itself. With 46 sites, a full rebuild is 6-12 minutes — but I almost never run a full rebuild. Each site is independent.
The empire isn’t 46 sites I maintain. It’s 10 platform Workers I maintain, with 46 thin clients on top.
The secrets dance
Every platform Worker has a SERVICE_AUTH_SECRET. Every consumer Worker has the same bytes stored as AUTH_VALIDATE_SERVICE_TOKEN. Rotating these is annoying — touch one, you have to touch every consumer.
The fix: a single rotation script at scripts/rotate-keys.js. Maps each “chain” of paired secrets and runs wrangler secret put against every node in the chain with the same generated hex. The whole rotation is one command:
node scripts/rotate-keys.js auth-chain --execute
Without that script, rotation was a 15-step manual process across 8 Workers, and I’d inevitably miss one. Most empire-scale problems aren’t about scale — they’re about repetition becoming a step you forget.
The mistakes that almost broke this
Three things I did wrong before I figured out the architecture:
What the architecture costs
Honest list of overheads:
- Cognitive load when bootstrapping a new site. You can’t just
npx create-astro— you need to know which platform Workers to bind, which package to extend, which secrets to set. Worth it after site #3, hard before that. - Cross-cutting changes need discipline. Changing the auth contract means touching every consuming Worker. The rotate-keys script + a shared TypeScript type help, but it’s still real work.
- Secret lifecycle. Ten platform Workers each have 3-5 secrets. That’s 30-50 entries to manage. The state lives in
wrangler secret put(encrypted server-side) and a gitignored.rotate-keys-state.json(local helper).
Monorepo architecture is exhausting at site three and joyful at site twenty. The middle is where most people quit.
What I’d tell past-me
If you’re standing up your first multi-site setup on Cloudflare:
- One Cloudflare account, one billing card. Non-negotiable.
apps/sites/<domain>/andapps/apis/<...>/<domain>/. Folder name = deploy domain. Skip “translation” layers.- One canonical metadata file per node. No
_PURPOSE.mdgraveyards. - Platform Workers before product Workers. Build auth + email + billing once. Every product after that inherits.
- A rotation script BEFORE you need to rotate. You’ll need it. Have it ready.
The architecture won’t feel like a win on site three. It’ll feel like a win on site thirteen.