The full stack of an agent-era blog

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/admin with visual editing, media library, plugin manager

REST API at /_emdash/api/* for programmatic content management

MCP server at /_emdash/api/mcp for 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 MiniMax image-01 with style presets

/xmm/tts — generates narration audio via MiniMax speech-2.8-hd with 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 — explicit Allow: / for 16 known AI crawlers

/.well-known/oauth-protected-resource — MCP server discovery metadata

Plus RSS autodiscovery ( ) 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_markdown for agent download

1 About page

Primary nav menu (Posts, Skills, RSS, About)

What's not here

No CI/CD pipeline. pnpm run deploy from 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.