This post is the technical map of everything running behind mondello.dev right now. Not a tutorial — a reference. If you're building something similar, this is the stack trace.
The layers
The blog runs as a single Cloudflare Worker. One binary, one deployment, one wrangler deploy command. Everything underneath is bindings, integrations, and plugins — no separate services, no containers, no multi-deploy pipeline.
Compute: Cloudflare Workers
The Worker is an Astro 6 server-rendered application using @astrojs/cloudflare as the adapter. Every page is rendered on-demand at the edge. There is no build step that produces static HTML — Astro's output: "server" mode means the entire site is dynamic, which is what makes EmDash's admin, preview mode, and Live Collections work.
The Worker costs $0/month on the free tier. The only thing I'm missing is Dynamic Worker Loaders (sandboxed plugin execution), which requires the $5/month Workers Paid plan.
Database: Cloudflare D1
All content, media metadata, users, settings, menus, widgets, taxonomies, bylines, and plugin state live in a single D1 database. D1 is SQLite-over-HTTP with read replicas. EmDash manages the schema entirely — 51 tables as of this writing, including FTS5 virtual tables for full-text search.
I don't touch the schema directly. EmDash's seed system creates the initial tables on first boot, and the ORM handles migrations. The only raw SQL I've written is for bulk operations (seeding skills, creating tags, populating byline assignments) where the REST API was slower or missing features.
Storage: Cloudflare R2
Media files (hero images, narration audio) live in an R2 bucket. EmDash serves them through /_emdash/api/media/file/{storageKey}. R2 is S3-compatible with zero egress fees. The backup cron also writes JSON table dumps to R2 under a backups/ prefix.
Sessions: Cloudflare KV
The @astrojs/cloudflare adapter auto-provisions a KV namespace for session storage. EmDash's passkey auth uses it to persist login sessions across requests. I don't configure it manually — it's injected at deploy time.
CMS: EmDash 0.1.1
EmDash is the entire content layer. It provides:
- Admin panel at
/_emdash/adminwith visual editing, media library, plugin manager - REST API at
/_emdash/api/*for programmatic content management - MCP server at
/_emdash/api/mcpfor agent-to-site interaction - Plugin system with
definePlugin(), KV storage, admin widgets, lifecycle hooks, API routes - Passkey-first authentication (WebAuthn)
- Portable Text content model (structured JSON, not serialized HTML)
- WordPress importer
- Full-text search via FTS5
- Sitemap, robots.txt, and SEO metadata generation
I'm using 5 registered plugins, all running in-process (trusted mode, no sandboxing):
1. emdash-forms — first-party form builder + submissions 2. posthog-analytics — analytics snippet with admin settings 3. agent-seo — robots.txt + llms.txt generation with bot catalog 4. minimax-image — hero image generation settings 5. minimax-tts — narration audio settings
Payments: x402
The @emdash-cms/x402 integration provides per-page payment enforcement. When an agent fetches a gated page, the middleware returns a 402 response with payment instructions (wallet, network, price, facilitator URL). The agent's x402 client settles USDC on Base and re-fetches with proof.
Configuration is 6 lines in astro.config.mjs. The wallet is my hardware wallet (0x0E4d...1028). Revenue goes directly to me — no intermediary, no marketplace fee, no account system.
Currently enforcing on /skills/[slug] and /skills/[slug]/raw with botOnly: true so humans browse free.
Media generation: MiniMax
Two Worker bridge routes call the MiniMax platform API using a secret stored in the Worker environment:
/xmm/image— generates hero images via MiniMaximage-01with style presets/xmm/tts— generates narration audio via MiniMaxspeech-2.8-hdwith automatic Portable Text to narration script conversion
Both routes are admin-gated (require authenticated session + x-emdash-request CSRF header). The API key never leaves the Worker.
Agent discovery
Four files make the site discoverable to LLM agents:
/llms.txt— discovery manifest per llmstxt.org listing all posts and pages/llms-full.txt— full post content inlined for single-fetch ingestion/robots.txt— explicitAllow: /for 16 known AI crawlers/.well-known/oauth-protected-resource— MCP server discovery metadata
Plus RSS autodiscovery (<link rel="alternate" type="application/rss+xml">) and the sitemap at /sitemap.xml with 9 URLs.
Automated backups
Four Cloudflare Cron Triggers fire the Worker's scheduled() handler, which self-invokes /api/backup to dump 19 D1 tables as JSON into R2:
- Hourly at :17 past
- Daily at 03:43 UTC
- Weekly on Sundays at 04:23 UTC
- Monthly on the 1st at 05:53 UTC
Each backup writes a _manifest.json with table counts, byte totals, and error log.
Content inventory
- 4 published blog posts with hero images, bylines, and 13 tags across 16 assignments
- 4 published skills in a custom "skills" collection with
raw_markdownfor agent download - 1 About page
- Primary nav menu (Posts, Skills, RSS, About)
What's not here
- No CI/CD pipeline.
pnpm run deployfrom the laptop is the deploy mechanism. - No custom theme. Using the default EmDash blog-cloudflare template with minor fixes.
- No email/newsletter. The forms plugin is registered but no forms are configured.
- No session recording or advanced analytics. PostHog plugin is registered but not keyed.
- No Stripe. x402 is the only payment path.
Cost
$0/month. Everything runs on Cloudflare's free tier. The only paid upgrade that would change anything is Workers Paid ($5/month) to unlock Dynamic Worker Loaders for sandboxed plugin execution and the EmDash marketplace.
The MiniMax API is on a dev-tier key with usage limits. The hero images and narration audio were generated within those limits at zero dollar cost.
Total infrastructure spend for a production blog with micropayment revenue, agent discovery, automated backups, and a skills marketplace: zero.