Skip to Content
ContributeArchitecture and integrity

Read Energon through its integrity boundaries before changing routing, persistence, expiry, or the agent protocol. The Worker coordinates catalog rows, bytes, quota, cache, and concurrent cleanup explicitly because D1 transactions cannot include R2 objects. This page shows where that coordination lives, which state must converge, and how schema and generated agent documents widen the scope of an otherwise small code edit.

Architecture and integrity

The request boundary

src/index.ts is the single Worker entry point. It normalizes expected ApiError responses, maps malformed URL encoding to a client error, and keeps unexpected failures behind a stable JSON envelope. Its route order is part of the security and availability model:

Published content paths redirect from the hub origin (PUBLIC_ORIGIN) to the content origin (CONTENT_ORIGIN) in production. Hub-origin PUT or DELETE of a content path is 307. The content hostname serves guest /llms.txt and guest write on public paths, and rejects account and /v1 routes with 404 naming GET /llms.txt. Most remaining requests call ensureSchema before dispatch; the scheduled handler does the same before expiry cleanup.

The domain modules repeat authorization in conditional D1 writes so an ownership decision cannot go stale between lookup and commit. URL, path, MIME, retention, cache, and catalog helpers keep those concerns out of the router.

UI rendering stays inside the Worker

Svelte pages in src/ui/pages/ use shared controls in src/ui/components/ and design assets in src/ui/design/. Typed page data comes from the Worker; src/ui-render.ts combines server markup, escaped hydration data, inline CSS, and a hashed browser-script reference. scripts/build-ui.mjs generates the server/CSS output under src/generated/ and browser assets under public/static/ui/; both outputs are ignored by Git.

Interactive hub pages load the application bundle. Public Markdown and password gates do not. Markdown pages with diagrams load their own renderer: Pele from /static/pele/, falling back to Mermaid from /static/mermaid/ for KaTeX diagrams or anything Pele cannot draw. Both are vendored from package.json by scripts/vendor-diagrams.mjs, and their entry URLs carry the package version (?v=) so an upgrade reaches browsers at once. The Worker enforces hostname boundaries before serving /static/ui/*. Follow upstream DESIGN.md  and the UI guide  when changing these surfaces, and use Develop and verify for checks.

D1 is coordination; R2 is bytes

D1 owns the catalog, stable identities, policy, mutation claims, and the platform_quota ledger. R2 owns the content. A successful mutation must leave them describing the same object.

reserve positive quota delta | v snapshot or stage previous R2 state | v write/delete R2 bytes | v conditionally commit D1 metadata | +---- failure ----> restore R2 and D1/claim state | v purge public cache prefix and release quota delta

Site and loose-file operations use different coordination strategies because their identities differ. Sites are keyed by a generated id. The slug is a label and is not unique. Public URLs are /{handle}/s/{id}/{slug}/.

Site mutations

Sites no longer mutate a live directory. A site row points at one immutable version (sites.active_version_id), and each version lists its paths in site_version_files, mapped to R2 objects under fresh site-versions/{site}/{version}/… keys. Every site write, whether a deployment, a path PUT or DELETE, a ZIP merge, a guest write, a one-path grant, or a copy, writes new objects through a quota-charged storage allocation, builds a complete candidate version, and publishes it with one conditional D1 update. That update succeeds only while content_generation still equals the session’s base generation, then increments it. A stale or failed candidate is never visible and its allocations are cleaned up.

Deployment sessions (site_deployments) hold the immutable intent, an idempotency key scoped to owner and site, a one-hour deadline, and a seven-day receipt. Preparation is resumable and owned by one worker at a time. Commit is the only publication step; prepare never publishes.

Readers take a D1 read lease on the version they selected (src/site-reads.ts), so cleanup cannot delete objects mid-response. Superseded versions keep a five-minute grace, and cleanup releases a quota charge only after R2 deletion is confirmed. The Worker-internal site cache (src/site-cache.ts) is keyed by immutable version and consulted only after access, expiry, and version checks, so publication correctness does not depend on a cache purge. Site deletion and expiry mark the site removed and clear its pointer; version cleanup reclaims storage afterward.

Older sites written in place are converted once, behind SITE_VERSIONING_ENABLED, by src/site-version-migrate.ts. Upgrade and recover covers the operator procedure and rollback floor.

Before v1.10.0, a path PUT snapshotted the previous object and wrote R2 in place, import restored all affected state after a failure, and whole-site deletion staged objects under an integrity-backup prefix.

Loose-file claims

A loose file has one row and one current R2 key, but replacement can race another writer or expiry. PUT and DELETE first place a unique leased write claim in last_written_by. The later D1 mutation succeeds only if that claim still owns the row. A pre-commit failure restores bytes, the observed timestamp, and the effective prior writer.

Each byte replacement also increments loose_files.content_generation in the same update. A conditional replacement (X-Energon-Expected-Version, multipart expected_version, or a file grant’s expected_version) adds AND content_generation = ? to that update, so the commit itself enforces the check; the pre-check after the claim only fails early. Busy and recovery-required reservations outrank file_conflict, because re-reading cannot unblock the file. Site-path writes reject the header rather than ignoring it, since a site generation spans every path.

Expiry uses a purge marker in the same field. A fresh write claim blocks cleanup; a purge claim blocks a TTL revival after deletion starts. Claims become stale after 60 seconds, allowing another request or sweep to recover abandoned work. The in-memory inFlight set only deduplicates within one isolate; D1 predicates provide the cross-isolate guarantee.

Preserve rename identity

Loose-file identity is the id, not the readable filename segment. Runtime rename is a byte replacement:

export ENERGON_ORIGIN="https://hub.your-company.example" export FILE_ID="REPLACE_WITH_EXISTING_FILE_ID" test -n "$ENERGON_TOKEN" || { echo "Set a human-authorized ENERGON_TOKEN" >&2; exit 1; } curl -fsS "$ENERGON_ORIGIN/v1/files/$FILE_ID" \ -X PUT \ -H "Authorization: Bearer $ENERGON_TOKEN" \ -H "X-Filename: renamed-report.md" \ -H "Content-Type: text/markdown" \ --data-binary @report.md

The PUT keeps FILE_ID, writes the new key, commits the new filename, and best-effort removes the old key only after metadata points to the replacement. Old public filename paths redirect by id to the returned canonical url. PATCH does not accept a filename; any feature-map sentence that suggests PATCH rename is stale and must not override runtime behavior.

Schema has three representations

Every schema change updates the representations that apply:

RepresentationRoleRule
migrations/NNNN_name.sqlDeployment historyAdd a new file; never rewrite an old migration.
src/db.tsRequest/cron bootstrap and legacy additive upgradeCreate absent tables, add supported columns, backfill where required, then create indexes.
src/schema.sqlDocumented current create shapeKeep it aligned with runtime table declarations; it is not applied by the Worker.

The old 0001_init.sql comment calling itself a copy of src/schema.sql is historical. The file is frozen history, not a live schema copy. Tests compare the current src/schema.sql columns with runtime table declarations and prove a 0005-shaped database receives columns before indexes.

CREATE and ADD COLUMN migrations are not idempotent. Migration files that mention stamping d1_migrations contain historical human-recovery notes for a deploy-first incident. They are not authorization for an agent to edit production migration state. The normal order is migration first, Worker second; only a human operator may verify an already-added production column and follow the documented recovery procedure.

The API and generated agent behavior are source contracts

openapi/v1.json is the canonical /v1 HTTP schema. The Worker serves a cloned copy at /v1/openapi.json, replacing servers[0].url with this Energon’s PUBLIC_ORIGIN without mutating the committed document. When a dedicated content origin exists, src/openapi.ts also gives each /_deployment-grants/* path item its own servers entry for that origin, so gateways that pin one origin per import skip those operations instead of calling them on the hub. From v1.14.0, src/openapi.ts also serves /v1/openapi-gateway.json, a projection that keeps only the operations whose x-energon-gateway extension is true, drops path items and tags left empty, lists only the hub under servers, and strips the extension from both served documents. GET /v1/help complements it with this Energon’s origins, policy, and SOP rather than duplicating the schema.

test/unit/openapi-drift.spec.ts keeps the contract tied to the implementation. It compares OpenAPI operations with the router and help.routes, requires operation ids and responses, requires every operationId to be unique and free of generated names such as postVSitesIdDeployments and every operation to carry a declared tag, checks the shared error-code enum against application errors, and limits unauthenticated operations to discovery and the connection request/poll endpoints. It also requires every operation to carry a boolean x-energon-gateway, pins the exact sets of operations in and out of the gateway catalog, and fails when a gateway operation accepts a request body other than JSON or returns anything but JSON or Markdown on success, except getFile and getSiteFile. Adding a /v1 route therefore means deciding whether gateways should see it. test/routes.spec.ts proves the live response, runtime server URL, CORS, HEAD, and method rejection through the Worker.

The committed plugin is generated from templates/ plus instance-skill.json. The renderer validates identifiers and all mutation targets, rejects unknown or unreplaced tokens, avoids project-local autoload copies, and supports a check-only mode.

From v1.12.0, the Worker renders the same templates/skill/ files at request time for the well-known skill. npm run build:ui writes the raw templates into a generated module under src/generated/, which avoids new Wrangler text-import rules in deployments, and src/agent-skills.ts fills them from the runtime identity (identityFromEnv). Both paths use the one template renderer in scripts/skill-template.mjs, so the served skill and the plugin cannot disagree about template handling. The skill templates no longer use ORG, which has no runtime source; plugin and marketplace templates still do.

templates/skill + templates/plugin + instance-skill.json | v scripts/render-skill.mjs | +--> plugins/{name}/ +--> real-host marketplace catalogs +--> updated instance-skill.json during init

Edit templates or Energon configuration, run the renderer, and review the generated diff. Before initialization, the upstream renderer validates templates without generating a plugin or catalogs. A real-host skill:init produces the uniquely named plugin and marketplace entries; subsequent renders check or refresh that output. The Claude marketplace catalog names the publish skill path so npx skills add --skill finds it next to operator skills in the same repository.

When /v1 behavior changes, generated-file equality is not enough. Review the semantics across openapi/v1.json, src/auth.ts’s helpBody, src/auth-doc.ts, src/llms.ts, templates/skill/, the runtime golden fixtures, and the rendered plugin. A snapshot can prove that a representation changed; it cannot prove independently authored representations still agree.

Failure modes

Do not update only the happy-path D1 write, only one schema representation, or only one API or agent document. Cross-service mutations need compensation and concurrency proof; schema needs append-only history plus compatible bootstrap; /v1 changes need semantic review across OpenAPI, runtime help, llms.txt, templates, goldens, and rendered output. Agents must never stamp production migration state.

  • Develop and verify maps each boundary to focused, injected-failure, mock-free, fuzz, page-contract, and browser checks.
  • Contribute to Energon explains contribution lanes and the repository tour.
  • HTTP API records the user-facing protocol these internals implement.
  • Upgrade and recover gives the human production migration procedure.
Last updated on