# Migrate an existing repository to Forge

Forge Deploy uses two repository-owned contracts. A project-root
`forgeBuild.ts` describes how to build one application. An optional root
`.forge/config.json` statically maps a monorepo's projects, defaults, and
reviewed Cloudflare binding proposals without running repository code. See
[Repository configuration](repository-config.md) for its schema and safety
boundaries.

You do not need to write `forgeBuild.ts` from scratch: the Deploy setup screen
and `sf migrate` inspect the exact Git commit, suggest a typed configuration,
and can commit that file for you.

The shortest migration is:

```bash
git remote set-url origin https://forge.smol.ai/<owner>/<repo>.git
git push -u origin "$(git branch --show-current)"
sf migrate
```

You can also push first and open the repository's **Sites** tab. The browser
setup and CLI use the same detection and configuration rules.

## Large assets and Git LFS

Forge builds can hydrate already-managed LFS pointers without a local
`git-lfs` binary. Agents may also call the standard LFS Batch/basic endpoints
directly. Install `git-lfs` when you want clean/smudge hydration and ordinary
local add/push behavior.

Forge uses standard Git LFS pointers and the Batch/basic transfer protocol, so
a repository remains compatible with normal Git LFS tooling. The optional
root `.forge/assets.json` adds reviewed Forge Asset Graph intent without
creating a Forge-only pointer format. See [Forge Assets v1](assets-v1.md) for
the complete contract.

After the repository is imported, the Sites page can inspect and adopt ordinary
committed blobs. This source implementation is not available on the live Forge
service until a separate platform release. Its contract:

1. freezes and inspects the expected branch head without running code;
2. proposes project `public/**` trees over 50 MiB aggregate and other
   individual files over 5 MiB by default;
3. lets an administrator include or exclude every literal proposal;
4. verifies selected digest bytes before committing standard pointers; and
5. opens a migration branch and pull request with immutable receipts.

The comparisons are strict `>` comparisons. The v1 inspection reports fixed
50 MiB/5 MiB defaults; it does not accept threshold overrides. Asset adoption
remains a separate action from the `forgeBuild.ts` assistant.

The adoption card reports missing objects before apply. Upload those bytes
through Forge's standard LFS Batch/basic HTTP API, then inspect again. No local
`git-lfs` binary is required. Apply is exact-SHA and idempotency-key protected;
it opens a derived migration branch and pull request, returns an immutable
receipt, and suppresses the migration PR's automatic Deploy preview. It never
changes the production branch, deploys, or changes DNS.

## What the migration assistant does

The assistant looks at repository files rather than running application code.
It detects common package managers, framework and build commands, likely asset
directories, server entrypoints, and Cloudflare configuration. It then:

1. shows the branch and exact commit it inspected;
2. explains what it detected and any compatibility warnings;
3. previews the complete `forgeBuild.ts`;
4. lets you download it, write it, overwrite an existing file, or commit it;
5. revalidates the resulting file with the same evaluator used by Deploy.

Asset adoption is a separate reviewed output. The configuration assistant does
not fold `.forge/assets.json`, `.gitattributes`, or pointer changes into a
`forgeBuild.ts` write.

Reading and previewing never modify the repository. Writing an existing
`forgeBuild.ts` requires an explicit overwrite choice. A server-side commit is
protected by the expected branch head and existing-file identity so it cannot
silently replace newer work.

## Importing a repository map

For a monorepo, commit `.forge/config.json` at the repository root after each
project has its own `forgeBuild.ts`. The optional `$schema` URL is
`https://forge.smol.ai/spec/deploy/v1/repository-config.schema.json`.

An administrator first inspects an exact SHA and reviews the normalized dry-run
plan. Importing that same plan creates new projects disabled. Existing projects
keep their enabled or disabled state while the reviewed root, production
branch, and publication mode are updated. A missing or disabled Cloudflare
target remains unresolved; an already-approved target that does not allow the
declared zone blocks import. Import never enables, deploys, attaches, detaches,
or applies provider state; a project administrator must use the separate
Cloudflare **Apply** action after reviewing the imported proposal.

Omitting a previous entry never requests a detach. Imports across independent
projects are not atomic and can partially succeed, so after a failure or
control-plane change, inspect again and approve the new plan digest before
retrying. See the complete
[repository configuration contract](repository-config.md).

### Build caches in monorepos

Generated Deploy builds use `cache: auto`; no cache setting is required in
`forgeBuild.ts`. Forge detects the frozen npm, pnpm, or Bun lockfile. The first
trusted build may capture an extracted exact dependency snapshot after the
frozen install and before application code runs; a matching rebuild of that
same source SHA skips installation but still executes the complete application
build. Cache
identities include the normalized Deploy project root, so two projects that
share a repository lockfile warm independently. A cache miss or cache service
error falls back to the same frozen install and cannot change release output.

An oversized manager store is skipped instead of split or force-saved. The
default manager-cache ceiling remains 512 MiB of logical files because a very
large mounted cache can be slower than a clean frozen install. This is a cache
miss, not a deployment failure. Exact dependency snapshots have their own
512 MiB compressed ceiling. Operators may raise
the ceiling for a measured project through
`CI_EXACT_CACHE_PROJECT_LIMITS={"dprj_example":805306368}` without raising the
manager-store ceiling. Invalid or absent entries fail back to 512 MiB, and both
save and restore enforce the project-scoped bound. The extracted snapshot is
keyed by exact source, lockfile, runtime, project root, and runner image.

The project administrator action **Full rebuild current SHA** is the controlled
same-commit warm-build canary and recovery path. It deliberately bypasses
finished OpenNext artifact reuse, while retaining eligible exact-source and
exact-dependency restores for the unchanged SHA, then runs the complete
application build, validation, ISR seed, and provider publication path. Pushes
and ordinary redeploys may still take the faster finished-artifact path. This
distinction makes `prebuild_reuse outcome=skipped reason=manual_full_build` an
observable full-build receipt instead of relying on a cache-key perturbation or
deleting prior artifacts.

Do not use this same-SHA receipt as the warm baseline for ordinary new commits.
The exact dependency identity includes the full source SHA because dependency
lifecycle scripts can read arbitrary repository and monorepo-sibling source.
Cross-SHA reuse of that post-install tree would be unsafe. New commits may reuse
the selected manager store and source projections, but not the exact installed
tree.

For unchanged OpenNext inputs, Forge has a faster path than dependency caching:
after exact-SHA checkout and configuration validation, it may reuse a verified
immutable provider artifact before dependency installation. The key includes
the source SHA, project ID and root, normalized `forgeBuild.ts`, Wrangler and
lockfile digests, the pinned OpenNext version and dependency specifier, Forge
and provider adapter versions, runner image and compiler toolchain, provider
target/base/policy/binding generation, and connected-asset entitlements. Old
evidence without this complete key is ineligible. A hit still rechecks that the
Cloudflare Worker Version exists, requires ready ISR evidence, and creates a
new project-scoped Forge release and preview pointer.

This path intentionally does not compare fresh build bytes. Next.js can create
new draft-mode keys and serialize an otherwise identical prerender manifest in
a different route order, so two correct builds of one SHA may be byte-different.
Changing any keyed input forces the ordinary frozen install, build, validation,
and publication path.

### AIE three-project OpenNext plan

Forge includes a dedicated read-only assistant for the AIE hosting snapshot:

```bash
node scripts/inspect-aie-hosting-migration.mjs /path/to/aiecode2025
```

It inspects exactly these approved project roots:

1. `apps/main`
2. `apps/2025-sites`
3. `apps/2023-2024-sites`

The JSON report proposes a separate project-root `forgeBuild.ts` using
`app.runtime: "opennext-cloudflare"` for each project. It also checks the
project-local registry `@opennextjs/cloudflare` declaration, exact resolved
lockfile version, Next.js compatibility, configuration, and project-local
`wrangler.toml`. The vetted OpenNext 1.19.11 target accepts Next
`>=15.5.18 <16 || >=16.2.6 <17`: `apps/main` and `apps/2025-sites` satisfy that
window, while `apps/2023-2024-sites` remains blocked on its Next 13 line. Other Next.js
workspaces are reported as excluded and never silently receive deployment
authority. The assistant opens files only; it has no write option and reports a
digest of the inspected snapshot.

Run this planner before modifying AIE. Its proposed manifests are review input,
not commits, builds, provider provisioning, DNS changes, or a Vercel
retirement.

## CLI

Run the interactive assistant in a repository:

```bash
sf migrate
```

Use another directory:

```bash
sf migrate ../my-app
```

For scripts and agents, inspect the result without prompts or changes:

```bash
sf migrate --json
```

When Forge detects both an HTTP Worker and static assets, choose the output
explicitly in the wizard or in automation:

```bash
sf migrate --target assets --write
```

Write the suggestion only when `forgeBuild.ts` does not exist:

```bash
sf migrate --write
```

Replacing an existing configuration and creating a Git commit are separate,
explicit choices:

```bash
sf migrate --write --force --commit
```

The CLI does not push. Review the generated configuration before publishing it
to Forge.

## Primary branch selection

Forge treats both `main` and `master` as conventional primary branches:

- if only `main` exists, Forge uses `main`;
- if only `master` exists, Forge uses `master`;
- if both exist, Forge uses `main` and warns that the two primary-looking
  branches are likely to cause confusion;
- otherwise Forge preserves the repository's valid configured default branch.

The selected branch and warning are visible before Forge writes or deploys
anything. Rename or remove the unused branch when practical.

## Existing Cloudflare applications

Detection is not a promise of runtime compatibility. If the repository uses
Cloudflare bindings, Durable Objects, secrets, scheduled jobs, service
bindings, or compatibility flags that Forge cannot yet reproduce, the assistant
lists those findings instead of pretending the migration is complete.

Static assets and applications using the documented Forge runtime can migrate
today. Provider-specific applications may still use Forge as their Git source
while their existing deployment remains active, then adopt Forge Deploy when
the required compatibility is available. Changing the Git remote does not
change DNS, Cloudflare Workers, storage, or the live domain.

### Connected Cloudflare previews

Forge has a narrow compatibility path for a deployment that is still published
as a Cloudflare Worker. Once the trusted publisher records that deployment's
HTTPS `workers.dev` version-preview origin, Forge can serve its HTTP routes
through the deployment's unlisted `sites.smol.ai` preview URL:

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

export default defineForge({
  version: 1,
  name: 'my-worker',
  build: { command: 'npm run build' },
  app: { entrypoint: '.output/worker.js' },
  routes: [{ pattern: '/*', to: 'app.http' }],
  provider: {
    cloudflare: {
      wrangler: importWrangler('wrangler.toml', {
        shareProductionResources: true,
      }),
    },
  },
});
```

`importWrangler()` is an inert typed reference, not executable repository code.
Forge reads and validates the TOML from the exact Git commit. The explicit
`shareProductionResources` acknowledgement is required because this preview
inherits the connected Worker's existing bindings and can read or mutate the
same D1, KV, R2, Durable Objects, secrets, and services.

Successful pushes and administrator-requested redeploys of the current
production-branch head populate Forge's content-addressed caches. Generated
Deploy jobs automatically select the detected frozen npm, pnpm, or Bun
lockfile. npm may additionally reuse an exact dependency snapshot. That exact
snapshot is keyed by immutable commit SHA and normalized project root because
package lifecycle scripts and monorepo installs may depend on repository
source; package-manager download caches remain reusable across commits and
sibling projects. Exact dependencies are compressed immediately
after Forge's frozen install, before application build code runs. The 512 MiB
default limit applies to the compressed payload, and Forge checks the expanded
dependency size plus free workspace capacity before staging it. The snapshot
preserves every root and workspace-local `node_modules` tree without capturing
repository source.

In a pnpm monorepo, declare every local package used by the application as a
`workspace:*` dependency, including packages referenced only by framework
configuration, Webpack aliases, build scripts, or code generators. Forge's
exact-source projection follows the declared workspace dependency graph; an
undeclared sibling is intentionally excluded even when a relative path in a
configuration file points to it.

For a normal Forge Deploy build, the runner reads the exact commit and its tree
directly from Forge Git storage. It verifies every Git object digest, transfers
only that commit's files into the restricted build sandbox, creates a real
detached Git checkout, and verifies `HEAD`. Repository history and HTTP clone
negotiation are not in the normal build path. Repositories above the bounded
object, byte, or depth limits fall back to the existing exact-SHA Git transport;
missing or corrupt authoritative objects fail closed.

For pnpm v9 monorepos, the direct path first reads the lockfile's frozen
`importers` index to discover the selected project and declared transitive
`workspace:*` dependencies without walking unrelated sibling workspaces. If
that syntax is absent or unsupported, Forge falls back to the conservative
full-tree discovery path rather than guessing. The selected projection still
includes root package-manager files and `.forge` policy.

The direct path retains only the verified compressed object closure. Concurrent
decompression shares a 64 MiB byte budget and releases each object immediately
after its digest, type, and tree relationship have been checked; the compressed
closure itself remains capped at 256 MiB.

Older immutable previews remain readable but cannot write caches. This keeps a
migration retry fast without letting stale code seed a later build.

After the repository build stops, Forge's pinned compiler creates the provider
module and trusted ingest reads its assets. Forge, not the repository and not
Wrangler running inside the sandbox, uses the connected Cloudflare API. It pins
binding inheritance to the one active 100% production Worker version, uploads
static assets through Cloudflare's content-addressed asset session, and creates
an immutable Worker Version with `deploy=false`. No route, DNS record, schedule,
custom domain, or live traffic pointer is changed.

Cloudflare-compatible builds may also emit WebAssembly modules. Forge uploads
them beside the generated ESM entrypoint with deterministic names and includes
their digests in the reusable provider artifact identity.

Trusted ingest is entitlement-driven. Forge preserves the 25 MiB per-file
provider ceiling, the 100,000-file provider ceiling, and a 1 GiB aggregate
safety ceiling. The AIE projects may be provisioned for 1 GiB per release;
ordinary projects receive their own frozen entitlement. WebAssembly remains
limited to 10 MiB per module and 25 MiB in total. Binary RPC streams are
counted while they are read and cancelled as soon as a limit is crossed,
before another full-size copy can be allocated. OpenNext applies narrower
module, asset, cache, and compressed Worker limits before connected ingest;
see [Deploy limits](limits.md).

Selected unchanged public LFS binaries stay content-addressed during this
step. Forge derives their provider-manifest identities from the verified LFS
digest, then streams only the hashes Cloudflare requests in bounded base64
multipart chunks. Large repositories therefore do not need their whole public
tree hydrated into the sandbox or buffered in the runner to publish a release.

Every connected Worker version also receives a Forge-managed
`FORGE_SOURCE_SHA` plain-text binding containing the exact 40-hex release
commit. Repository configuration must not declare that reserved name. Runtime
diagnostics can use it to distinguish a Forge preview from another provider
without depending on build-tool environment inlining.

Forge uses a pinned Wrangler compiler in credential-free dry-run mode for this
compatibility bundle. This preserves Cloudflare's ESM and compiled WebAssembly
module semantics without allowing repository code or Wrangler to publish,
activate, route, or change provider resources.

Forge requires Cloudflare's per-version Preview URLs for every connected
release. During the first publication it enables Preview URLs on the approved
Worker while preserving the existing `workers.dev` setting, then requires the
version-creation receipt to contain the unique version hostname. It never uses
the mutable production Worker alias for a release preview.

Cloudflare does not generate version-specific Preview URLs for Workers that
implement Durable Objects. Forge retains a bounded compatibility path for
those Workers: it stages one candidate at 0% and reaches it through a version
override while production remains at 100%. Only the newest such preview is
reviewable; publishing another candidate replaces that slot. Do not treat
historical Durable Object preview links as immutable archives. Keep Durable
Objects behind a separately connected service when retained previews matter.

Wrangler remains an independent administrator recovery path. A later Forge
candidate records whichever production provider version it actually observed
as its immutable base, and promotion compares against that version before it
changes traffic. Forge therefore returns to control without overwriting a newer
unobserved recovery release.

The migration compatibility path accepts a generated ESM entrypoint up to
25 MiB before provider compression, matching large framework outputs while
retaining an explicit trusted-ingest bound.

Forge validates the connected resource identities returned with that exact base
version against the repository allowlist: exact D1 database, KV namespace, R2
bucket, and bound Worker identities. It can preserve secret bindings without
reading secret values. The deployed application still receives the inherited
bindings, which is why the shared-resource acknowledgement remains a real
production-data warning rather than a publisher permission warning.

The provider artifact identity is computed before upload from the pinned
adapter, compiled module, static asset manifest, `_headers` and `_redirects`
control files, complete Wrangler configuration digest, provider target, active
base version, and binding/settings digests.
Another deployment with the same effective provider bytes reuses the verified
immutable Worker Version instead of uploading another one. Forge records the
provider version receipt before it checks the preview URL, and a deterministic
provider tag lets a retry reconcile an upload whose response was interrupted.
For the OpenNext preset, the complete prebuild key described above can reuse
that verified version before build execution; byte-level artifact reuse remains
available after a full build for other connected runtimes.

### Make framework build IDs reproducible

Forge exposes the immutable source commit as `CLOUDFORGE_TRIGGER_SHA` during
the repository build. Use it anywhere a framework asks for a build or
application version. A clock, random value, or mutable branch name changes the
effective bytes on every build, so Forge correctly treats the result as a new
provider artifact instead of reusing the previous version.

SvelteKit defaults `kit.version.name` to the build timestamp. Connected
SvelteKit applications should make that value commit-based:

```js
import { execFileSync } from 'node:child_process';

const sourceVersion =
  process.env.CLOUDFORGE_TRIGGER_SHA ||
  process.env.CF_PAGES_COMMIT_SHA ||
  process.env.GITHUB_SHA ||
  execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();

export default {
  kit: {
    // ...your adapter and other settings
    version: { name: sourceVersion },
  },
};
```

This preserves SvelteKit's update and cache identity while making repeated
builds of one exact commit byte-for-byte reproducible across Forge, Cloudflare
Pages, GitHub Actions, and local Git. Forge does not rewrite generated files
after the build.

- Forge resolves repository visibility, suspension, deployment expiry, and the
  immutable deployment record on every request;
- only routes compiled to `app.http` are proxied; ordinary Forge asset routes
  keep using immutable Forge asset storage;
- client-supplied Forge and Cloudflare forwarding headers are removed before
  the provider request;
- provider-internal response headers and parent-domain cookies are removed;
- dynamic responses use `no-store`, baseline security headers, `noindex`, and
  the Forge deployment receipt; and
- the global dynamic-runtime kill switch also stops connected previews.

The recorded origin must be a root HTTPS hostname beneath `workers.dev`.
Credentials, paths, query strings, fragments, nonstandard ports, and
lookalike domains are rejected. The provider publisher enables and records
Cloudflare's immutable per-version Preview URL whenever the provider supports
one. The no-URL Durable Object compatibility path is explicitly limited to the
single staged candidate described above; it never represents the production
Worker alias as immutable.

This path is for migration evaluation, not confidentiality or final domain
cutover. A Cloudflare preview hostname may still be reachable directly if it
is discovered, and direct requests do not pass through Forge's access checks.
Do not place private data in a connected preview. Forge does not change the
existing production route, DNS, or custom domain until an administrator
separately chooses domain graduation.

For connected production routing, keep the main Worker as the Custom Domain
and declare archive applications as logical wildcard-free `path_route`
prefixes. The default `exact_and_descendants` mode creates and receipts both
`host/archive` and `host/archive/*`, preserving lexical siblings. When the
archive intentionally owns the whole lexical namespace, explicitly review
`reserved_prefix`: it emits `host/archive*`, solves the query-bearing root, and
also captures siblings such as `/archive-old`. Neither mode captures Next
Pages Router `/_next/data/<build-id>/archive.json` URLs outside the archive
prefix, so keep the reviewed application fallback for those navigations.

Deployment logs expose `checkout_phase=git_objects`, transferred object/byte
counts, source transfer/materialization time, bundle size, asset count, and
whether the connected version was published or reused. These are owning-surface
measurements; a cached dependency install and a reused provider version are
reported separately.

The Deploy page also presents a compact receipt. End-to-end readiness is shown
separately from trusted runner phases such as provisioning, exact-SHA checkout,
dependency restore, dependency installation, dependency snapshot save,
application build, provider publication, and finalization.
Assets and runtime reuse are independent: a deployment can reuse either one
without reusing the other. Forge persists phase timings from runner-owned code;
repository log output cannot create or alter this evidence.

Connected previews follow the same retention window as Forge-native previews.
Forge preserves the active deployment and two newest successful rollback
candidates. Once an older preview expires, Forge may delete the exact immutable
Worker Version it published. That cleanup is serialized with provider
activation, verifies the version is not present in Cloudflare's current
deployment, removes an expired candidate from a zero-percent preview slot when
needed, and retains Forge's deployment, artifact, version ID, and cleanup
receipts. A 100-percent production version is never eligible. Cleanup does not
delete the Worker or migrate/delete bound provider data.

Cloudflare currently labels the
[Delete Version API](https://developers.cloudflare.com/api/resources/workers/subresources/beta/subresources/workers/subresources/versions/methods/delete/)
beta. If that provider operation is unavailable or fails, Forge preserves the
version and its receipts and retries the fenced cleanup later.

Recent successful deployments are compared with runner-time phase bars;
queueing is excluded. Each row is labeled `Fast` or `Full` so mixed execution
paths remain visible. P50 appears after three comparable deployments and P95
after twenty. Those percentiles use the newest measured deployment's execution
path and exact effective Forge configuration across up to fifty recent
records, so they never mix Fast and Full paths or unrelated configuration
changes. The chart separately reports how many rows are shown and the
percentile sample size. Regression and improvement signals use the same
comparison identity. The page calls out a dominant checkout, restore, install,
or application-build phase, suggests a bounded next investigation, and reports
the recent runtime-reuse rate.

## Build and repository contracts remain separate

The general migration assistant only authors `forgeBuild.ts`. The AIE planner
is strictly read-only. Every new deployment still requires a valid project-root
`forgeBuild.ts`, read from the exact commit before the build begins.
`.forge/config.json` can propose projects and routing intent, but it cannot
replace build configuration or grant a build DNS authority. UI overrides
remain visible, audited exceptions; they do not replace either repository
contract.

See the [public Deploy specification](https://forge.smol.ai/spec/deploy/v1) for
the complete syntax and
[`forgeBuild.ts` v1](manifest-v1.md) for evaluator limitations. See
[Repository configuration](repository-config.md) for the repository map and
its import/apply workflow.
