Modules and defaults
Modules
Section titled “Modules”Opt-in capabilities, each pulling real infrastructure:
commerce—a catalog mirror in D1 with a pulled/owned split, a webhook receiver, a queue consumer, and a cron safety net. Providers fill roles (storefront/invoicing) rather than being “the” provider, becausecommerce/stripehas no catalog API andcommerce/fourthwallhas no invoicing. A Square storefront also gets the server-authoritative payment seam:src/pages/api/checkout.tsre-prices every line from the D1 mirror (the client’s price is a staleness check, never an input to the charge), derives an idempotency key from the cart and a cart id, and charges only once commerce is really provisioned—otherwise it simulates rather than calling Square with a placeholder credential.<SquareCard>mounts the Web Payments card field, which is an iframe from Square’s CDN, so the raw card number never reaches the Worker. The cart itself is yours—where it lives and what it holds is a project decision, and a half-opinionated cart is worse than none.portal—a second, fully isolated Better Auth instance for customers/members: its own mount, cookie prefix, andportal_*tables, so a portal account can’t sign into the studio.map—a self-hosted PMTiles basemap served from R2, brand-recoloured. No API key, no external tile host.pwa—a scoped service worker that never caches/api/*or the editor, plus a derived manifest.realtime—live multi-editor editing on a page: a per-page Durable Object holding presence, field sync, and a rich-text soft-lock. See below.
What’s on by default
Section titled “What’s on by default”Three things are wired into every scaffold rather than hidden behind a flag, because each has a client half that already ships in the editor drawer—leaving them unmounted meant rendering UI for a subsystem that could never have data.
- The Home dashboard (
overviewRoute)—draft counts, unpublished changes, last edit. It’s the drawer’s initial panel, so it’s the first screen an owner sees. - AI assists (
aiRoute,seoFixRoute, alt-text on upload)—rewrite a selection, suggest SEO, describe an image. All editor-gated, and all degrade to a hidden button when theAIbinding is absent, so they cost nothing unused. - The typed Actions surface (
src/actions/index.ts)—save,saveDraft, andsettingsas Astro Actions beside the raw routes. Not a second implementation: each shares its route’s store path, so a field is validated once and written in one place however it was called. Add your own beside them. - Real-visitor Core Web Vitals—a beacon in
public/vitals.jsposts LCP, CLS, and INP to an Analytics Engine dataset, and the daily health scan reads the p75 back. Collection is free and needs nothing; the read-back needsCF_ACCOUNT_ID+CF_API_TOKEN(the SQL API is account-scoped and has no binding), and until those are real the badge reads “not measured yet”. - Site health (
healthRoute+ a daily cron)—broken links, images missing alt text, published pages with SEO gaps. The summary is stored in the existingRLnamespace under its own key, so there’s no extra binding to provision. Until the first scan runs the panel says “not checked yet”. The inbox card counts unhandled inquiries—the whole table, because the Inquiries tab reviews and clears submissions, so a surviving row is one still waiting.
Edge caching (off by default)
Section titled “Edge caching (off by default)”The generated worker wraps Astro’s SSR fallback in withEdgeCache, Louise’s
cookie-aware Worker Cache API layer. It ships wrapped but inert: the var
ASTROID_EDGE_CACHE is "false", so every render emits no-store and the layer
stores nothing.
Why cookie-aware matters: Cloudflare’s automatic edge cache is keyed by URL and
runs before your Worker, so it cannot see the edit cookie—it will serve a
cached public page to a signed-in editor, drafts and all. withEdgeCache runs
inside the Worker, inspects the request first, and strips the CDN directive from
every response so the automatic cache never engages. This feature was reverted
twice before that distinction was understood.
Do not enable it straight on production. caches.default is not cleared by
Cloudflare Dev Mode or “Purge Everything,” so a bad flip is hard to walk back.
Turn it on for a preview deploy and walk the activation runbook in
docs/adr/0004-edge-caching.md first: verify an anonymous second request is
served from cache, an editor always renders fresh, and a publish shows up within
the 60 s TTL.
That TTL is short on purpose—caches.default is per-colo with no global purge,
so maxAge is the real worldwide freshness floor after a publish.
wrangler.jsonc gets a triggers.crons list, and the generated worker’s one
scheduled handler dispatches on controller.cron—Cloudflare fires a single
handler for every trigger and that string is the only way to tell them apart.
| Cron | What runs |
|---|---|
17 4 * * * |
The daily site-health scan. Always. |
0 * * * * |
The catalog re-sync safety net. Commerce only; queues.cron: false disables it. |
Realtime editing
Section titled “Realtime editing”modules: ["realtime"] turns editing into a live session. A per-page Durable
Object holds presence and authoritative field state, broadcasts changes to the
other editors on that page, and coalesces writes to D1 on an alarm.
Two properties are worth knowing:
- It augments, it does not replace. With the module off, the socket unopened, or the connection dropped, the client falls back to the existing debounced auto-save. Realtime is an accelerator, never a dependency.
- There is one write path. The session’s flush goes through
applySaveDraft—the same merge-over-pending-draft the fetch auto-save uses—so drafts, version history, publish semantics, and read-your-writes are all unchanged. The DO is a new front end to that path, not a parallel store.
The rich-text body takes a soft-lock (one editor at a time) rather than being last-writer-wins clobbered, and locked values are never fanned out to peers—so raw rich text doesn’t cross sockets.
Astroid scaffolds src/edit-session.ts (the DO subclass—it must import
cloudflare:workers, so it can’t live in the toolkit), the durable_objects
binding, and the migration block. That last one is the part nobody gets right
from memory: a DO class needs a migration tag, it must be new_sqlite_classes
rather than new_classes, and the storage backend cannot be changed after the
class is first deployed.
Dormant until provisioned
Section titled “Dormant until provisioned”Astroid’s modules are opt-in at the config level but not at the account
level: switching commerce on must not require a Square account before pnpm dev
will boot.
So every module follows one rule—a module whose secrets are unprovisioned is
dormant. It renders, it serves, it says out loud that it is simulated, and it
never calls upstream with a dummy credential. create-astroid seeds every secret
with a loud DUMMY_REPLACE_ME sentinel, so a fresh clone has a complete, valid
binding set and zero real credentials.
astroid doctor reports which modules are dormant and exactly which secrets are
still missing.
Next steps
Section titled “Next steps”- Astroid API reference—the exported surface.
- Sections—the underlying section/block model Astroid’s catalog is built on.
- Inline editing—how the edit markers work.