# Repository configuration

Status: implemented in the current source branch. Importing configuration does
not publish a Worker, attach a route, change DNS, or switch production traffic.

Forge deliberately separates repository-wide migration intent from build
configuration:

- `.forge/config.json` is static JSON. It lists Deploy projects, their roots,
  repository defaults, and proposed Cloudflare domain or path bindings. Forge
  can inspect it at an exact Git commit without running repository code.
- Each project root still contains its own `forgeBuild.ts`. That typed,
  statically evaluated file describes how Forge builds one application. It is
  read only when that project builds.

This keeps one discoverable repository map without turning a TypeScript file
into DNS authority. A repository build never receives Cloudflare credentials,
secret values, provider target identifiers, or permission to change domains or
routes.

## File format

The machine-readable schema is
[`https://forge.smol.ai/spec/deploy/v1/repository-config.schema.json`](https://forge.smol.ai/spec/deploy/v1/repository-config.schema.json).
The optional `$schema` field gives editors the same contract Forge validates.

```json
{
  "$schema": "https://forge.smol.ai/spec/deploy/v1/repository-config.schema.json",
  "version": 1,
  "defaults": {
    "production_branch": "main",
    "publication_mode": "manual"
  },
  "projects": [
    {
      "root": "apps/main",
      "slug": "main",
      "cloudflare": {
        "zone_name": "example.com",
        "hostname": "www.example.com",
        "bindings": [{ "kind": "custom_domain" }]
      }
    },
    {
      "root": "apps/archive",
      "slug": "archive",
      "cloudflare": {
        "zone_name": "example.com",
        "hostname": "www.example.com",
        "bindings": [
          {
            "kind": "path_route",
            "route_path": "/archive/2025",
            "match_mode": "reserved_prefix"
          }
        ]
      }
    }
  ]
}
```

`version`, `defaults`, and `projects` are required. `defaults` requires
`production_branch` and `publication_mode`. Every project requires a unique
repository-relative `root` and permanent `slug`; it may override either
default. Forge expects `<root>/forgeBuild.ts` at the exact inspected commit.

Cloudflare declarations name a zone and hostname but never provider account
IDs, zone IDs, Worker names, credentials, receipts, or current state. Those
remain administrator-approved Forge control-plane records.

## Path matching

A `path_route` uses a wildcard-free `route_path`. `match_mode` chooses how
Forge translates that logical path into Cloudflare Worker Routes:

| Mode | Provider behavior | Use when |
| --- | --- | --- |
| `exact_and_descendants` | Matches the exact path and its `/` descendants. This is the default when `match_mode` is omitted. | Lexical siblings must remain available to another application. |
| `reserved_prefix` | Reserves every URL whose path text starts with the prefix. | The archive owns the whole namespace, including a query-bearing root, and lexical siblings can be intentionally unavailable. |

For `/archive/2025`, `exact_and_descendants` covers `/archive/2025` and
`/archive/2025/...` but does not reserve `/archive/20250`. At Cloudflare's
route layer, a query-bearing exact root needs different treatment.
`reserved_prefix` maps to the broader provider pattern `/archive/2025*`, so it
also captures `/archive/2025?utm=x`, `/archive/2025/...`, and lexical siblings
such as `/archive/20250` and `/archive/2025-old`.

Use `reserved_prefix` only when that lexical namespace is intentionally owned
by one project. Forge validates overlaps across projects and fails closed when
provider state contains ambiguous or unexpected overlapping patterns.

## Inspect, import, then apply

Repository configuration is a reviewed proposal, not a deployment primitive:

1. An authenticated repository administrator inspects an exact SHA. Forge
   parses `.forge/config.json`, confirms each `forgeBuild.ts`, resolves existing
   projects and approved targets, and returns a dry-run plan and digests.
2. The administrator reviews that plan and explicitly imports the same SHA and
   digests. New projects are created disabled. Existing projects keep their
   enabled or disabled state while the reviewed root, production branch, and
   publication mode are updated.
3. A missing or disabled provider target remains visible as unresolved work.
   If an already-approved target does not allow the configuration's declared
   zone, the plan is blocking and cannot be imported. Import does not invent or
   broaden provider authority.
4. An authenticated project administrator separately reviews desired
   Cloudflare state and uses the explicit **Apply** action. Only this step may
   reconcile provider routes or domains.

The exact-SHA API flow is:

```text
GET  /api/repos/:owner/:repo/sites/repository-config?sha=<40-character-sha>
POST /api/repos/:owner/:repo/sites/repository-config/import
```

The import request carries the inspected source/head SHA and expected
configuration and plan digests. A changed branch head or plan is stale and
must be inspected again.

```json
{
  "action": "import",
  "source_sha": "<inspected 40-character SHA>",
  "expected_head_sha": "<inspected branch-head SHA>",
  "expected_config_digest": "<inspection config_digest>",
  "expected_plan_digest": "<inspection plan_digest>"
}
```

Import never enables a project, publishes a release, attaches or detaches a
domain, applies a Worker Route, changes DNS, or switches production traffic.
Removing a project or binding from `.forge/config.json` never means detach;
detach is a separate confirmed control-plane action.

Changing an approved provider target, zone set, or binding policy is also a
separate operator action. Forge allows an identical approval to be repeated.
While domains or routes remain attached, Forge also permits a narrowly fenced
monotonic binding-policy expansion when the Cloudflare account, Worker, zone
set, and approval state are unchanged. Existing resource IDs and D1 ID-to-name
mappings must remain byte-for-byte present; additions advance the provider
target generation and the attached domain/route fences atomically. Existing
artifacts and releases retain their old frozen generation, so only a new build
can consume the added authority. Resource removals or renames, zone changes,
and provider identity changes remain blocked until every binding is detached,
applied, and retired.

Imports are not atomic across independent projects: one project may complete
before a later project fails. The recorded outcome is retryable. Resolve the
failing contract, inspect the current exact SHA again, review its new plan
digest, and import again. Do not reuse an earlier approval after project,
target, zone, or binding state changes.

See [Migrate an existing repository](migrating-an-existing-repository.md) for
the author workflow and the [operator runbook](operator-runbook.md) for the
control-plane and live-verification gates.
