Architecture

How Caelo is built.

A walk through the parts of the system that matter when you're evaluating it: the two-database split, the plugin sandbox, the snapshot & branching model, the provider abstraction layer, and the request pipeline from edit to publish.

01

Topology

Caelo runs as a small set of cooperating processes. Nothing exotic — Postgres, a Node app server, a static-file bucket, and an optional plugin sandbox. Each piece is replaceable without rewriting the others.

Edge / CDN

Serves the static production bucket. Caddy in self-hosted mode; any CDN in cloud mode. Terminates TLS, applies redirects, forwards /api/* + /admin to the app server.

App server (Node)

Handles /edit, /security, the chat agent, write tools, and plugin operation endpoints. Stateless — scale horizontally behind the edge.

Postgres × 2

One DB for content (pages, modules, snapshots, plugin tables). One DB for system state (users, roles, proposals, audit, deploys). See §02.

Plugin sandbox (Deno)

Tier 2 plugins execute here with --no-read --no-write --no-net. The app server is the only client. See §03.

Static bucket × 2

One staging bucket, one production bucket. Stage merges the chat branch and rebuilds staging; promote does an atomic pointer swap to production.

02

Two-database split

Caelo keeps content and system state in separate Postgres databases. The boundary is enforced — no foreign keys cross it, no joins span it. Operationally it means you can restore a content backup without disturbing user/role/audit history, and vice versa.

Content DB
per-site, branchable
  • pages, modules, module_placements
  • templates, layouts, template_blocks
  • snapshots + snapshot_payloads
  • structured_sets (nav, taxonomies, theme tokens)
  • Plugin-owned tables (prefixed with the plugin slug)
System DB
shared, append-mostly
  • users, roles, user_roles
  • proposals + proposal_events
  • audit_log (every write, with cost in microcents)
  • deploys, builds, domains
  • ai_providers, mcp_tokens, chat_sessions

Every row in the content DB carries a branch_id. Chat sessions write to their own branch; Stage merges into main. The system DB has no branching — proposals and audit are the source of truth.

03

Plugin tiers & sandbox

Plugins extend Caelo without forking it. Two tiers, with very different trust models. Pick the lowest tier that covers what you need.

Tier 1 — Core
trusted

Ships in the Caelo repo. Runs in the app-server process, can declare workers and AI tools, can request capabilities (file system, network, raw SQL). Used for first-party plugins: auth, forms, comments, ratings, search.

  • Imports anything the app can import
  • Can register write tools the AI agent calls
  • Owns migrations, workers, cron jobs
  • Changes go through code review + release
Tier 2 — Sandboxed
untrusted

Submitted via submit_plugin, validated, and Owner-approved. Runs in a Deno sandbox with --no-read --no-write --no-net. Imports limited to @caelo-cms/plugin-sdk. No fetch, no Deno, no dynamic imports, no raw SQL.

  • Declares schema, operations, optional Web Component
  • Tables with page_id must declare locale
  • Per-(plugin, operation) rate limits
  • Validation errors surface as structured {kind, hint} the agent can self-correct
04

Snapshots & branches

Every content row carries a branch_id. Every write emits a snapshot with the full diff payload. Together they give you per-chat isolation, one-click revert at three scopes, and a Stage step that's just a merge.

Branching

Each chat session writes to its own branch_id. The editor iframe reads that branch so you see your in-progress work. The published site only ever reads main.

Stage = merge

Clicking Stage merges every row from the chat branch into main, then rebuilds the staging bucket. No magic — just a transactional row copy.

Snapshot scopes

Three revert granularities: revert_module (one module), revert_page (one page + its layout), revert_site (everything to a point in time). Each is a proposal.

Chat-wide undo

revert_chat_changes files a single revert_site proposal targeting the snapshot right before your session began. One click rewinds the whole chat.

05

Provider abstraction

External services are reached through a thin Provider Abstraction Layer (PAL). Each capability has one interface and N adapters. Swap adapters by config — no plugin or page-level code changes.

AI
Anthropic OpenAI Google local-openai-compat
Email
SMTP Resend SES none
Static hosting
Caddy + local disk GCS S3
Media storage
local disk GCS S3
Image generation
DALL·E Imagen

Credentials are encrypted at rest in the system DB. Owners paste API keys inline at proposal-approve time — the AI agent never sees the plaintext. Per-provider cost caps in microcents kill runaway spend before it happens.

06

Edit → publish pipeline

Every editor action follows the same path. The pipeline is deliberately boring; the interesting parts are the gates between stages.

1 · Intent

Editor types in chat or clicks a chat-chip. The agent reads the system-prompt context block (layouts, templates, pages, locales, users) and plans tool calls.

2 · Tool call

Agent invokes a write tool (edit_module, create_page, compose_page_from_spec, …). Bulk variants collapse N round-trips into one.

3 · Gate (optional)

Hard-to-revert ops (locale add/delete, layout delete, plugin activate, site_defaults, AI provider config) route through the proposal queue. Owner clicks Approve at /security/<kind>/pending.

4 · Write & snapshot

Row mutated with branch_id. Snapshot emitted with diff payload. Audit row recorded with user, tool, args hash, cost in microcents.

5 · Preview

Live editor iframe rebinds. inspect_page_render and screenshot_page let the agent self-verify before reporting back.

6 · Stage

Operator clicks Stage. Branch rows merge into main; the staging bucket rebuilds. Stage URL is a 1:1 preview of what production would render.

7 · Publish

Owner approves a deploy.promote proposal — staging → production. Per-kind checkboxes (Pages, Templates, Layouts, Plugins, Redirects, SEO) allow partial promotion. Atomic bucket swap.