# `forgeBuild.ts` v1 specification

`forgeBuild.ts` is the project-authored contract for one Forge application.
It is typed TypeScript for editors, but Forge does not execute it. Forge
statically reads one data object from the exact Git commit and converts it into
its provider-neutral deployment plan.

Committing this file does **not** activate Deploy by itself. Create a Deploy
project from the repository's **Sites** page, accept the current Deploy Terms,
and enable that project. `smolforge deploy check` validates only the local
manifest; it does not check project creation, account eligibility, or enabled
state.

An optional repository-root `.forge/config.json` has a different purpose: it
statically lists projects, shared defaults, and reviewed provider-binding
proposals. It does not replace any project-root `forgeBuild.ts`, and importing
it never applies Cloudflare state or grants builds DNS authority. See
[Repository configuration](repository-config.md).

Every new Forge deployment requires this file at its configured project root.
A repository may contain multiple independently enabled projects; each has its
own root, releases, previews, provider target, active pointer, and rollback
history. A production-branch push schedules every enabled project
independently. Forge does not infer changed paths or create atomic release
groups.
`forge.yml` and `forge.yaml` are unsupported; a commit containing either is rejected even when
`forgeBuild.ts` is also present. An already-active deployment remains available
until it is replaced, rolled back, disabled, or suspended.

## Example

```ts
import { defineForge, importWrangler } from '@smolai/forge/config';

export default defineForge({
  version: 1,
  name: 'notes',
  build: {
    command: 'npm run build',
    workingDirectory: '.',
    context: {
      exclude: ['docs/ai/**'],
      include: [
        'docs/ai/program-summary.json',
        'docs/ai/shared/**',
      ],
    },
  },
  app: {
    entrypoint: 'src/server.ts',
    assets: {
      directory: 'dist',
      fallback: 'index.html',
    },
    limits: {
      state: { maxScopes: 200 },
      realtime: {
        maxProjectConnections: 50,
        maxEventBytes: '4KiB',
      },
    },
    identity: {
      scopes: ['user:basic', 'accounts:read'],
    },
  },
  routes: [
    { pattern: '/api/*', to: 'app.http' },
    { pattern: '/*', to: 'app.assets' },
  ],
  provider: {
    cloudflare: {
      wrangler: importWrangler('wrangler.toml', {
        shareProductionResources: true,
      }),
    },
  },
});
```

### Importing Wrangler configuration

`importWrangler('wrangler.toml', { shareProductionResources: true })` is a static reference, not a JavaScript
import. Forge reads the referenced TOML file from the same exact Git commit,
validates its syntax, and retains its path in the normalized deployment
configuration. Repository code never runs while Forge evaluates either file.

The path must be a string literal naming a repository-relative `.toml` file.
Absolute paths, `..`, dynamic expressions, other file formats, and missing or
invalid TOML are rejected. This reference describes provider requirements; it
does not provision or migrate Cloudflare bindings. The required acknowledgement
is explicit because a connected preview inherits the existing Worker's
bindings and can read or mutate the same KV, D1, R2, Durable Object, and other
resources. Forge never treats that preview as an isolated test environment.

Install `@smolai/forge` as a development dependency for autocomplete and local
checking. Forge recognizes the import directly and never installs repository
dependencies merely to read configuration.

```sh
npm install --save-dev @smolai/forge
npx smolforge deploy check
```

## Fields

- `version` is required and currently `1`.
- `name` is an optional lowercase DNS label. It does not claim or change the
  stable production hostname.
- `build` optionally declares one sandboxed command, repository-relative
  working directory, and a bounded source context. Omitting it executes no
  repository build command.
- `app` must declare an `entrypoint`, `assets`, or both.
- Alternatively, `app.runtime: 'opennext-cloudflare'` selects Forge's
  OpenNext build preset. It forbids authored `build`, `entrypoint`, `assets`,
  and application limits because Forge derives `.open-next/worker.js` and
  `.open-next/assets`.
- `app.entrypoint` may be authored JavaScript/TypeScript source or an
  already-bundled ESM framework output. Forge still passes that module graph
  through its pinned compiler without repository plugins or compiler
  configuration. The resulting module must be Workers-compatible and export a
  default object with an asynchronous `fetch` method; a Node HTTP server entry
  is not compatible.
- `app.assets.directory` is the static root; `index` and `fallback` are
  optional artifact-relative files.
- `app.limits` may lower state and realtime ceilings but cannot increase
  entitlement or budget.
- `app.identity` opts into Forge-hosted, app-scoped visitor identity. Its
  non-empty, duplicate-free `scopes` array accepts `user:basic`,
  `accounts:read`, and `teams:read`. It is also valid for assets-only apps.
- `routes` map exact or suffix-wildcard paths to `app.http` or `app.assets`.
- `provider.cloudflare.wrangler` may reference one checked-in Wrangler TOML
  file using `importWrangler()` and must explicitly acknowledge shared
  production resources.

### Framework build output

Frameworks may point `app.entrypoint` at their generated Worker module. For
SolidStart/Nitro, select the Cloudflare module preset so the output is a Worker
module instead of Nitro's default Node HTTP server:

```ts
// vite.config.ts (inside the existing plugins array)
nitro({ preset: 'cloudflare_module' })
```

```ts
// forgeBuild.ts
export default defineForge({
  version: 1,
  build: { command: 'pnpm build' },
  app: { entrypoint: '.output/server/index.mjs' },
  routes: [{ pattern: '/*', to: 'app.http' }],
});
```

Framework configuration APIs can change between releases; the stable Forge
requirement is that the configured entrypoint is an ESM Worker module, not a
process-listening Node server.

### Project-scoped build context

Large repositories may omit committed inputs that the project build does not
read:

```ts
build: {
  command: 'npm run build',
  context: {
    exclude: ['docs/ai/**'],
    include: [
      'docs/ai/program-summary.json',
      'docs/ai/evaluations/current.json',
    ],
  },
}
```

The context starts with the complete frozen project source, applies every
`exclude`, then restores the listed `include` exceptions. Paths are
case-sensitive and relative to the configured project root. Each path is an
exact file, `directory/**` for a subtree, or `**` for the whole
project. Arbitrary globs, absolute paths, `..`, backslashes, and control bytes
are rejected. A context accepts at most 256 total paths and 16 KiB of
normalized path data.

Forge always retains deployment controls needed to verify the build, including
the exact commit's `forgeBuild.ts`, referenced Wrangler configuration, project
package manifest, root package-manager files, `.forge`, and discovered
workspace dependencies. An include exception does not discover application
imports: projects must list every excluded file that their build reads. Compare
full and projected build outputs before adopting a broad exclusion.

Forge reads and statically evaluates only `forgeBuild.ts` from authoritative
Git objects before checkout. Repository code never supplies projection paths
at dispatch time. The canonical project root, context, forced control paths,
and policy version are hashed into the immutable exact-source checkpoint key,
so projected and full checkouts at the same commit cannot collide. The runner
still verifies the exact `HEAD` after materialization.

Custom contexts currently use an exact projected checkpoint or perform one
bounded projected tree traversal. Direct-parent object-layer reuse and sibling
checkpoint waiting remain disabled until those paths can prove identical
projection identities and closure-aware deltas.

### OpenNext Cloudflare preset

```ts
import { defineForge, importWrangler } from '@smolai/forge/config';

export default defineForge({
  version: 1,
  name: 'next-app',
  app: { runtime: 'opennext-cloudflare' },
  routes: [{ pattern: '/*', to: 'app.http' }],
  provider: {
    cloudflare: {
      wrangler: importWrangler('wrangler.toml', {
        shareProductionResources: true,
      }),
    },
  },
});
```

The project package must declare a registry-resolved
`@opennextjs/cloudflare` dependency, and npm, pnpm, or Bun must resolve it to an
exact version through the frozen root lockfile. Forge performs the frozen root
dependency install, verifies the installed exact version, invokes that
installed project-local adapter in the project root, and never fetches an
implicit `latest`. The trusted publisher receives only quiesced, validated
output; repository build code receives no provider credentials or secret
values.

Adapter and framework peer compatibility is a build eligibility contract, not
an install warning. The vetted OpenNext 1.19.11 target requires Next
`>=15.5.18 <16 || >=16.2.6 <17`; incompatible projects fail before
installation or publication.

The Wrangler contract must use `.open-next/worker.js`,
`.open-next/assets`, the `ASSETS` binding, and `nodejs_compat`. Separately,
Forge validates the supported R2 incremental-cache identity against the
administrator-approved inherited production binding. Unsupported output,
unsafe files, overflow, compressed Worker size, startup validation, and
binding mismatch fail before publication. See [limits](limits.md) and
[failure diagnostics](failure-diagnostics.md).

The native Forge runtime keeps deny-all egress and repository configuration
cannot widen it. Connected OpenNext Workers retain only their
administrator-approved provider runtime policy and inherited bindings; those
credentials and secret values never enter repository builds. `/.forge` is
reserved for platform endpoints, including the Forge Identity broker. Hosted
apps never receive Forge control-plane cookies
or unrestricted account and team directories.

## Portability boundary and future resources

Current v1 supports one application with built-in managed state and realtime;
it does not yet accept an author-defined `resources` map.

The forward contract will let repositories declare multiple named logical
resources by required semantics, such as SQLite-compatible data, key/value
data, object storage, or keyed actor state. Provider product names and resource
IDs belong in the compiled plan, not those portable declarations. Deterministic
preview seeds will target the same logical names.

The existing `provider.cloudflare` section is the deliberate exception. It is
a namespaced compatibility escape hatch for applications that depend on
Cloudflare-specific behavior and is excluded from Forge's portability
guarantee. Forge preserves those requirements and reports them as non-portable
rather than silently weakening or translating them.

## Static TypeScript subset

Forge accepts:

- exactly one import from `@smolai/forge/config`, containing unaliased
  `defineForge` and optionally unaliased `importWrangler`;
- exactly one `export default defineForge({...})`;
- object and array literals;
- strings, finite numbers, booleans, and `null`;
- comments, trailing commas, `as const`, `satisfies`, and type arguments.

Forge rejects:

- every other import or export;
- variables, identifier references, spreads, and computed properties;
- getters, setters, methods, functions, classes, and `new`;
- calls other than the outer `defineForge` and a literal
  `importWrangler('path/to/wrangler.toml', { shareProductionResources: true })`
  reference;
- interpolated templates, regular expressions, BigInt, and symbols;
- conditions, loops, `await`, promises, environment access, filesystem,
  network, clocks, and randomness;
- repository `tsconfig`, plugins, and path aliases.

This restriction makes configuration deterministic and cheap to validate
without creating another untrusted-code runtime.

## Bounds

- Source: 64 KiB.
- Syntax tree: 10,000 nodes.
- Literal nesting: 64 levels.
- Normalized configuration: 256 KiB.
- Application bundle: 5 MiB.

OpenNext and connected-provider limits are distinct from the generic
application bundle limit. See [Deploy limits](limits.md).

Paths must be normalized, repository-relative paths and cannot traverse outside
the checkout. Duplicate object keys and ambiguous routes are rejected.

Parser and unsupported-syntax diagnostics include a stable error code and
source position. Semantic diagnostics include the stable code and affected
field:

```text
Invalid forgeBuild.ts [FORGE_CONFIG_UNSUPPORTED_SYNTAX]:
18:15: spreads are not supported in Forge configuration
```

## UI overrides

Repository configuration is authoritative by default. A repository
administrator may explicitly override:

- build command: inherit, replace, or disable;
- asset directory: inherit or replace;
- SPA fallback: inherit, enable, or disable.

Forge validates the source object first, applies overrides, validates the
effective configuration again, and records both forms on the deployment.
Overrides are visible in the Deploy UI and build logs.

Saving an override offers **Save only** and **Save and deploy current branch
head**. Saving never silently deploys.

## Deployment identity

Before scheduling work, Forge reads `forgeBuild.ts` from the exact pushed
commit and records:

- Git blob SHA and raw source digest;
- normalized source configuration and digest;
- schema, evaluator, and policy versions;
- applied override snapshot;
- normalized effective configuration and digest.

The runner receives that frozen effective configuration and verifies the
checked-out file still matches the recorded Git blob. Repository build code
cannot rewrite its own deployment policy.

Activation and rollback move the active immutable deployment pointer. They do
not rewind production state. Preview state remains isolated by deployment.

## Errors

| Code | Meaning |
| --- | --- |
| `FORGE_CONFIG_MISSING` | `forgeBuild.ts` is absent. |
| `FORGE_CONFIG_YAML_UNSUPPORTED` | The commit contains `forge.yml` or `forge.yaml`. |
| `FORGE_CONFIG_TOO_LARGE` | Source or normalized data exceeds its bound. |
| `FORGE_CONFIG_PARSE_ERROR` | TypeScript syntax is invalid. |
| `FORGE_CONFIG_INVALID_MODULE` | Import/export structure is not allowed. |
| `FORGE_CONFIG_UNSUPPORTED_SYNTAX` | The file uses executable or dynamic syntax. |
| `FORGE_CONFIG_DUPLICATE_KEY` | An object contains a duplicate key. |
| `FORGE_CONFIG_INVALID` | The reduced object violates the v1 contract. |
| `FORGE_CONFIG_CHANGED` | Checkout differs from the frozen exact-SHA configuration. |

Invalid configuration creates a visible failed deployment and never starts a
repository build.
