# Forge platform components

Status: active component architecture and database-decomposition runbook,
2026-08-08.

The requirement-to-evidence index and remaining connected release gates are in
[`platform-restructure-completion-audit.md`](platform-restructure-completion-audit.md).

## Decision

Forge remains one product and one monorepo, but it is released as twelve
authority-scoped Workers. `cloudforge-edge` is the only public origin for the
Forge application. It owns static web and documentation delivery and routes
requests to identity, content control, repository, and Deploy control over Service Bindings. It
has no database, object storage, email, AI, provider-control secret, Workflow,
or Durable Object binding.

The five responsibilities extracted from the former `cloudforge-api` are:

| Component | Product responsibility | Authority and state it owns |
| --- | --- | --- |
| `edge` | `forge.smol.ai`, web, docs, route selection | Assets and four HTTP Service Bindings only; no product state |
| `identity` | Login, sessions, credentials, users, accounts, organizations, teams, entitlements, identity broker, Slack installation records | Identity/account tables in D1; credential validation RPC; identity and OAuth secrets |
| `content-control` | Gists, character skins, product social cards, stateful HTML metadata | Content tables in D1, character-skin objects in Git R2, identity RPC |
| `repository` | Git, repository APIs, issues, PRs, Actions dispatch, agents, repository invitations, Wiki control, repository-owned Site configuration | Repository/Git/Actions/invitation tables, Git and Wiki R2 objects, repository DO and Workflows, repo-scoped authorization |
| `deploy-control` | Deploy projects, releases, provider targets, Sites projects/domains, desired state, activation, reconciliation | Deploy and Sites-control tables, Site asset control access, runner/runtime handoff and provider-control secrets |
| `notifications` | Transactional invitation delivery | Invitation delivery outbox state and the only `send_email` binding |

The shared D1 database is a temporary physical migration boundary, not a
shared-ownership license. `config/forge-database-domains.json` assigns every
canonical table to identity, repository, automation, Deploy, release
authority, notifications, content, Wiki, or AI control; `rate_limits` is
explicitly partitioned by service-prefixed key. `npm run test:database-domains`
reconstructs the canonical schema and fails on an unowned or multiply-owned
table.

The initial executable inventory contains 198 tables and 131 cross-domain
foreign keys. Those edges are migration blockers, not permission to retain a
permanent shared database. Each blocker must become an owner-local reference,
immutable external identifier, Service Binding query, or transactional outbox
before that domain binding changes. New domain D1 databases are created as
additive shadow targets. A cutover requires a source Time Travel bookmark,
deterministic replay, schema and row parity, stable content digests, dual-read
comparison where practical, and an independently verified rollback binding.
Production `cloudforge-next` is never recreated or destructively transformed
by this decomposition.

Identity, content control, repository, Deploy control, and notifications now have independent
npm workspace entrypoints and Wrangler files. Their first extraction delegates
to the existing `packages/api` implementation kernel so the provider bundle
and public behavior remain stable. Source directories move only after their
imports, tests, and database authority can follow the same owner; the kernel is
deleted when the last feature leaves it.

## Inventory before the split

The old `cloudforge-api` artifact served the SPA and docs and also carried D1,
Git/Site/Wiki R2, AI, email, two Workflows, the repository Durable Object,
runner/runtime/Wiki Service Bindings, OAuth and signing secrets, Git and
repository routes, account and identity routes, Deploy/Sites routes, invitation
delivery, operator endpoints, and a one-minute reconciliation cron. A content
change therefore rebuilt and reauthorized this complete authority set.

Responsibilities such as Wiki generation, CI runner, Sites delivery, Deploy
runtime, AI routing, and Slack execution were already source modules and
Workers, but the release workflow redeployed every Worker after every main
commit. The main API also remained their authority-rich control-plane hub, so
source modularity had not produced independent failure or release domains.

## Production authority and route map

| Worker | Public route | Bound platform authority | Calls |
| --- | --- | --- | --- |
| `cloudforge-edge` | custom domain `forge.smol.ai` | Assets | identity, content-control, repository, deploy-control |
| `cloudforge-api` (identity) | none; reached through edge/RPC | shared D1; identity/OAuth secrets | no general downstream binding; exports `IdentityControl`, `ConnectedForgeIdentity`, and `SlackIntegrationControl` |
| `cloudforge-content-control` | none; reached through edge | shared D1, Git R2 | identity RPC |
| `cloudforge-repository` | none; reached through edge | shared D1, Git R2, Wiki R2, AI, `RepoLock`, last-commit and path-index Workflows, repository secrets | identity RPC, runner, Wiki, notifications |
| `cloudforge-deploy-control` | none; reached through edge | shared D1, Site R2, Deploy/identity signing secrets | identity RPC, runner, Deploy runtime |
| `cloudforge-notifications` | none; Service Binding only | shared D1, invitation-token key, email sender | none |
| `cloudforge-ci-runner` | none | shared D1, cache/Git/Site R2, Sandbox DO, CI/pack/full-clone Workflows, provider token, release policy | Deploy runtime and Cloudflare provider APIs permitted by per-repository policy |
| `cloudforge-sites` | `*.sites.smol.ai/*` | shared D1, Site R2, analytics, dispatch namespace, request-signing secrets | identity RPC and Deploy runtime |
| `cloudforge-deploy-runtime` | none | application shard and quota DOs, context and AI-router secrets | AI router; health includes the exact bound router version |
| `cloudforge-wiki` | none | shared D1, Git/Wiki R2, AI, Wiki Workflow and Wiki secrets | AI Gateway/providers; exact release health is exposed through repository's composed probe |
| `cloudforge-ai-router` | none | shared D1, Workers AI and provider credentials | configured AI providers |
| `cloudforge-slack-agent` | custom domain `slack.forge.smol.ai` | separate Slack-agent D1, Slack Workflow and signing secret | identity's narrow Slack RPC |

The Cloudflare zone also has an unrelated `api.smol.ai/*` route to
`smol-endpoint`; it is not a Forge release unit. Edge additionally exposes
`cloudforge-edge.swyxio.workers.dev` and immutable
`VERSION-cloudforge-edge.swyxio.workers.dev` preview origins because connected
Deploy must verify the exact uploaded version before activation. They are
noncanonical verification origins; `forge.smol.ai` is the product origin. The
private split components have `workers_dev` disabled.

Static paths and documentation terminate at edge assets. `/api/auth`, account,
organization membership/team, user-profile, identity and Slack-integration
paths route to identity. Organization repository operations, profile README and
R2-backed avatar paths, Repository, Git Smart HTTP, Actions, agent,
Wiki-control, gist and invitation paths route to repository. Deploy
project/release/provider and Sites-control paths route to deploy-control. Route
ownership is deterministic and contract tested; `/internal/*` is denied at the
public edge.

## State and authorization invariants

1. Identity is the sole credential validator and owner of credential
   revocation and account authorization-generation state. Repository and
   deploy-control call the `IdentityControl.authenticate` RPC once, fail closed
   if it is unavailable, and then apply resource-local authorization using the
   returned identity context. They do not copy token validation logic or build
   a synchronous call chain.
2. Repository owns repository membership, collaboration, invitation, and
   ref/object mutation decisions. Authentication does not imply repository
   authorization. A repository decision is made beside repository state.
3. Deploy-control owns desired deployment state, provider activation intent,
   active pointers, leases, and reconciliation. Runner and runtime perform
   bounded work but do not become the source of truth for activation.
4. Sites delivery validates signed identity/account context and the current
   account authorization generation before serving account-owned resources.
   Generation changes invalidate old signed context; Deploy and Sites converge
   from desired state and reconciliation rather than an unrepairable request
   chain.
5. Repository owns invitation intent and creates the invitation plus outbox
   row atomically. Notifications owns leased delivery transitions and the
   external email side effect. A best-effort nudge reduces latency; the
   notification cron repairs a lost nudge. Claims use leases and bounded
   attempts. A persisted side-effect fence distinguishes interruption before
   send (safe to retry) from interruption after send (ambiguous, fail for
   operator reconciliation rather than duplicate automatically).
6. No component may write another owner's tables merely because both have the
   same D1 binding. Shared SQL helpers may describe schemas and reads; they are
   not an authority bypass.
7. Operator routes live with the state owner. There is no universal operator
   Worker carrying all secrets.
8. Rate-limit rows are logically owned even while they share D1: keys begin
   with the component service name, so one API Worker cannot overwrite another
   component's enforcement window.

`config/forge-releases/*.json` is the exhaustive binding inventory. Each
binding name declares its provider type; configured or live bindings outside
that allowlist fail release verification. This is also the auditable inventory
for D1, R2, DO, Workflow, AI, email, Assets, secrets, variables, analytics,
dispatch namespaces, version metadata, and Service Bindings. The identity
contract's `prunableBindings` records obsolete secrets that may be removed by
the narrow release operation but can never be reintroduced as allowed
authority.

## Scheduled and durable repair inventory

| Component | Scheduled/durable behavior | Completion authority |
| --- | --- | --- |
| identity | one-minute account authorization and lifecycle repair | identity/account generation and operation rows |
| repository | repository metadata, Actions, agent, Git snapshot and invitation-intent handoffs; Workflows and `RepoLock` | repository refs/objects, run records, outbox receipts |
| notifications | one-minute invitation delivery lease scan | provider receipt or explicit ambiguous/terminal delivery state |
| deploy-control | one-minute provider activation, delivery and desired-state reconciliation | deploy release/operation row plus active provider pointer |
| runner | one-minute dispatch/cache/canary reconciliation; CI, pack and full-clone Workflows | immutable run/step evidence and external operation receipt |
| wiki-worker | Wiki Workflow and durable build stages | Wiki revision/build receipt |
| slack-agent | Slack Workflow with leased job polling | Slack job/transport receipt |
| Sites/runtime/AI/edge | request or DO-driven; no database repair cron | owner-signed context, DO state, invocation receipt, or active edge version |

## Options considered

### A. Edge plus one private application Worker

This is the smallest extraction and would have prevented web/docs changes from
requiring email approval. It leaves identity, Git, Deploy, provider control,
and notification authority in one failure and rollback domain, so it treats
the incident as an asset-routing issue instead of the broader coupling signal.

### B. One Worker per source feature

Separate Workers for accounts, organizations, credentials, repositories,
issues, Actions, agents, Sites, deployments, invitations, and Wiki control
would maximize nominal deploy units. In the current relational model it would
also create synchronous call chains and cross-service transaction problems,
duplicate repository authorization, and make local debugging materially
harder. Source directories are not trust boundaries.

### C. Authority-aligned platform components (selected)

Edge, identity, repository, Deploy control, and notifications are separate;
tightly coupled repository operations remain colocated; existing execution
Workers remain specialized. This removes the most dangerous bindings from
unrelated releases while retaining local decisions where correctness depends
on one transaction or one repository authorization check. It also permits a
gradual future database split without making that split a prerequisite.

## Handoffs and correctness model

Use synchronous Service Binding/RPC calls for bounded request/response facts:
credential validation, route forwarding, a current health observation, and a
single repository or deployment operation. Do not synchronously traverse
edge -> identity -> account -> repository -> runner.

Use a durable outbox, Workflow, or leased reconciliation record when the
caller has committed state and another component or provider must eventually
perform a side effect. The database record is authoritative; a Service Binding
nudge is only latency optimization. All consumers require a stable
idempotency key, bounded attempts, visible terminal/ambiguous state, and a
repair scan. Cloudflare Queues may replace polling where throughput warrants
it, but their at-least-once delivery still requires the same idempotency and
state model.

## Release impact and completion

`config/forge-components.json` is the machine-readable component graph.
`scripts/forge-release-impact.mjs` evaluates changed paths, shared contracts,
migrations, provider configuration, and release-system inputs.
`forge-release-provider-impact.mjs` compares against every component's own
active exact-SHA baseline, and `verify-component-source-impact.mjs` checks the
graph against Wrangler's actual bundle metafiles. Local component code affects
only the components that compile it; web/docs affect only edge; and a migration
affects the D1 consumers. Root lockfile impact follows the signed runtime
workspace closure. Shared subpaths retain explicit consumer ownership; an
unresolved shared dependency fails closed rather than silently becoming a
permanent all-Workers rule.
Dependents are included only when their compiled/runtime contract changes, not
merely because another component has a new implementation. Each component
declares its own `releaseContract` path: a binding-policy edit selects only
that owner, while an unmapped contract file fails impact calculation closed.
Each component's `runtimeBindings` file lives in its own workspace and is
checked against the corresponding release contract. A new component therefore
does not change an aggregate runtime binding map. Release-system changes rerun
proof without rebuilding any runtime by default.

The merge queue first consumes already-signed evidence for the exact candidate
and base. If matching CI is still running, it joins that run instead of
dispatching a duplicate queue build. The controller promotes independent
managed Workers concurrently inside dependency-safe waves, persists each
provider receipt separately, and starts a dependent wave only after its
selected predecessors have succeeded. A cancelled workflow is terminal and
rolls the release back; it cannot leave the queue permanently running.

When signed evidence introduces a component unknown to the active controller,
the plan retains that component and bootstraps deploy-control first. The
exact-source controller then resumes the same fenced release with the new
manifest. This avoids both silent omission and an out-of-band feature-task
release handoff.

Every component release writes `forge-component-release/v1` evidence with:

- exact source SHA and local/GitHub Action result;
- release operation ID, immutable provider version ID, and version tag;
- binding-policy generation digest;
- active deployment/pointer and 100% traffic observation;
- component contract verification and, for edge, public dependency health;
- previous deployment/version as the rollback target, or explicit evidence
  that no prior active version exists for a first release.

An upload is not completion. Completion is an active immutable version whose
annotation or connected managed binding identifies the exact SHA, whose
provider bindings exactly match the checked-in typed allowlist, and whose
health or public route proves the intended version. A verification failure
deploys the recorded prior version back to 100% when one exists.

Private release health is composed without creating new public origins. Edge
observes identity, repository and Deploy control; repository observes
notifications, runner and Wiki; Deploy control observes runner and runtime;
runtime observes AI routing. Sites and Slack use their existing public health
hosts. The release coordinator selects the observation for the component being
released and requires its exact provider version, release contract, empty
missing-binding list and healthy state. An unrelated degraded sibling remains
visible in the aggregate graph but does not block that component's independent
release.

The connected generalized Deploy contract in `forgeBuild.ts` describes only
the public edge artifact. Its provider policy permits only the three Service
Bindings and no email, D1, R2, AI, Workflow, Durable Object, or provider
credential. The platform components behind that edge use the trusted,
dependency-aware release coordinator rather than smuggling their authority into the connected
application build.

## Staged migration and rollback

Each stage below is independently releasable even when executed in one planned
cutover for this pre-production system.

### 1. Notifications boundary

- Move: email send and outbox draining from the main API to notifications.
- Authority: repository remains invitation/outbox author; notifications becomes
  the only email-binding owner.
- State: no database move; add the side-effect fence migration.
- Compatibility: repository transaction commits the old outbox shape plus the
  new fence, and nudges the new Worker; cron repairs missed nudges.
- Order: migrate D1, deploy notifications, then repository.
- Rollback: stop nudges and restore the previous repository code. Rows remain
  readable; ambiguous sends are not automatically duplicated.
- Proof: interruption-before/after-send tests, idempotent lease tests, provider
  binding inspection, and a disposable invitation delivery.
- Remove after proof: email binding and delivery cron from the former API.

### 2. Identity and repository boundary

- Move: identity/account routes and credential validation to identity; Git,
  repositories, Actions, agents, Wiki control and invitations to repository.
- Authority: identity owns credential truth; repository owns repo scope.
- State: shared D1 remains; no dual writes and no data copy.
- Compatibility: edge supplies the stable public URLs. Repository calls the
  RPC entrypoint and fails closed; there is no local-auth fallback.
- Order: identity, notifications, repository, then edge route switch.
- Rollback: return edge routing to the prior active version; new private
  Workers may remain deployed with no public route.
- Proof: revoked-token/RPC failure tests, repository authorization tests, Git
  clone/push canary, exact-version health.
- Remove after proof: public and repository handlers from the monolithic
  entrypoint and its Git/R2/AI/Workflow/DO bindings.

### 3. Deploy-control boundary

- Move: Deploy/Sites desired state, provider operations, leases, activation and
  reconciliation to deploy-control; keep request delivery in Sites/runtime.
- Authority: deploy-control owns state; runner/provider adapters execute
  declared operations.
- State: shared D1 remains so activation pointer changes retain their existing
  transactions; no cross-service writes are introduced.
- Compatibility: edge routes existing URLs; Sites continues validating identity
  through the identity RPC contract.
- Order: deploy-runtime and runner contracts, deploy-control, then edge.
- Rollback: edge to the previous version and deploy-control to its recorded
  provider version; reconciliation resumes from durable desired state.
- Proof: activation interruption/retry suites, exact-version provider receipt,
  Sites canary and rollback target.
- Remove after proof: Deploy/Sites-control handlers and provider secrets from
  the monolithic entrypoint.

### 4. Edge cutover and release isolation

- Move: `forge.smol.ai`, assets and docs to edge; enable path-derived releases.
- Authority: edge owns only routing/assets. Private Workers have no public
  route and `workers_dev` is disabled.
- State: none. Edge's immutable `workers.dev` preview is verification-only;
  private Workers keep `workers_dev` disabled.
- Compatibility: browser-visible URLs and cookies remain same-origin.
- Order: all private dependencies healthy, edge last.
- Rollback: restore the prior edge version; private versions are independently
  rolled back only if their own proof failed.
- Proof: deep health, public assets/login/repository/Deploy checks, route and
  version observations, plus changed-path tests proving docs do not select
  email/Git/Deploy/operator components.
- Remove after proof: the old combined Wrangler configuration and deploy-all
  workflow.

## Local development, disposable integration, and debugging

Run `npm run dev:platform` after providing non-production values in each
runtime workspace's `.dev.vars`; the checked-in scoped `.dev.vars.example`
files are safe for disposable development. Wrangler starts the six split configs in one local
multi-Worker session with one persisted D1/R2 directory and connected Service
Bindings. Add `-- --migrate` to apply D1 migrations to that local persistence
directory before startup. Each workspace file contains only that runtime's
secrets, so identity cannot receive repository, notification, or Deploy
secrets and vice versa. Add `-- --with-runner` (and the runner's local
example secrets) for the container-backed production Git-ingest path. The
integration harness creates disposable dev-var files from the examples only
when no developer-owned file exists and deletes only the files it created.
Edge is the listening Worker at `http://localhost:8787`.
Trigger a component cron with `/cdn-cgi/handler/scheduled`. Do not use real
provider secrets locally; AI bindings are remote and should be avoided unless
the test explicitly needs them.

The former combined API entrypoint and its all-authority integration Wrangler
configuration are gone. Local integration tests traverse edge and the real
Service Bindings, so a passing smoke cannot validate a topology that production
does not use.

Unit and contract tests own the fast path. Tests that depend on Cloudflare
Service Binding/RPC behavior, version overrides, routes, email-binding policy,
or provider activation use disposable real Workers and isolated names. They
must record their exact versions and delete only the resources they created.

For production debugging, begin at edge `/health/dependencies`, follow the
reported component version IDs, then inspect the owning component's structured
operation/outbox/reconciliation record. Do not repair product state with raw D1
commands. Re-drive the owning narrow API or reconciliation job. Provider state
is compared to durable desired state, and the release evidence supplies the
exact rollback target.

The default-branch workflow definitions are likewise Git-owned. A push reads
`.github/workflows` from the exact Git object SHA, validates Forge's small
workflow contract, and synchronizes source metadata before dispatch. Local
tests use disposable D1/R2 substitutes for object and transaction behavior;
Service Binding/RPC, version routing, email policy, and provider activation
tests use disposable real Workers when Miniflare cannot reproduce the
Cloudflare contract.
