Self-host Forge
Run your own Forge with your GitHub App, Cloudflare account and JEV credential. The code and documentation are MIT licensed. This guide covers the accounts, configuration and checks you need to get started.
You choose who can connect and pay your own provider costs.
What you operate and pay for
You operate a Worker, one D1 database, a SQLite Durable Object binding and the single configured Cloudflare JEV route. Forge commits to GitHub; it does not execute, build, test, serve or deploy repository code. GitHub automation can react to those commits independently.
| Use | Who supplies the account and allowance |
|---|---|
| ChatGPT or another client | You supply its plan; that does not include Forge hosting or JEV allowance, and client support must be checked separately |
| Your self-host | You pay your Cloudflare/JEV charges; repository owners supply GitHub permissions, API allowance and any Actions spend |
The capture attempt limit is per user and UTC day. JEV's daily call limit and concurrency limit are shared across this deployment: defaults are 200 calls/day and 4 simultaneous calls. Every inference attempt consumes the daily allowance; an unknown outcome is not refunded and holds a fixed 60-second reservation. D1 stores aggregate attempts/completed/unknown/input/output counts and temporary random reservations, without user IDs, source or paths. A missing budget backend refuses inference. Completion accounting is bounded; validated evidence can return with accounting still unknown without refunding the original attempt. Input/output counts reflect reported usage; missing usage does not mean zero provider charge. These limits count calls, not money: they are not a billing guarantee. Verify provider spend notifications and edge rate limits separately.
1. Install and make your config
Use Node within the root package.json range and its pinned pnpm version. From
the repository root:
pnpm --dir worker install --ignore-workspace --frozen-lockfile
cp worker/wrangler.self-host.example.jsonc worker/wrangler.self-host.jsonc
cp .dev.vars.example worker/.dev.vars
Replace the account, URL, App and REPLACE_WITH_OWN_GITHUB_LOGIN placeholders in
worker/wrangler.self-host.jsonc; step 3 supplies the new D1 database identifiers.
Filled configuration and .dev.vars* secret files are ignored by Git. Keep
secret values out of Wrangler vars. The commands below select your own config. Root pnpm dev uses the local-only config;
pnpm run deploy checks and deploys your ignored wrangler.self-host.jsonc.
The example uses https://WORKER_NAME.YOUR_SUBDOMAIN.workers.dev at the root of
the hostname. Choose the Worker name and your Cloudflare workers.dev subdomain,
and set FORGE_PUBLIC_ORIGIN to that exact HTTPS address, without a trailing /.
account_id and CLOUDFLARE_ACCOUNT_ID must both name your own account.
2. Register your GitHub App
Create the App under your own GitHub account or organisation. Set its homepage
and setup URL to your FORGE_PUBLIC_ORIGIN, and its OAuth callback to
<FORGE_PUBLIC_ORIGIN>/oauth/callback. The setup URL receives installation query
parameters; keep the entire Worker hostname routed if you later use a custom
domain. Forge does not consume webhooks; webhook delivery can remain disabled.
Enable expiring user authorization tokens. Fill GITHUB_APP_CLIENT_ID and
GITHUB_APP_SLUG from your registration and put its OAuth client secret in the
secret file. Copy the client ID exactly: GitHub's App response example
uses Iv1.ab1112223334445c; dotted and undotted IDs are accepted. No App private
key or installation-wide token is used. Each request
uses the signed-in person's stored, encrypted user credential: GitHub intersects
the person's permissions with the App's repository access.
| Repository permission | Why |
|---|---|
| Contents: read and write; Pull requests: read and write; Metadata: read | Read source, commit edits and prepare/review a proposal |
| Checks: read; Commit statuses: read; Actions: read | Exact-SHA execution evidence and repository-produced analysis artifacts |
| Administration: read and write | Required only if you want Forge to create repositories; existing-repository edits do not need it |
| Workflows: read and write | Required only for authorised edits to GitHub workflow files |
Install your App on a disposable repository you control. Approve changed permissions in existing installations. Missing evidence permissions remain explicitly unavailable; zero checks never mean success. A private App works for its owning account; accepting other accounts requires a public registration and their own installation/consent. Another operator’s registration is not reused by this setup.
3. Create your Cloudflare resources
Log Wrangler into your account, then run from worker/:
pnpm exec wrangler d1 create YOUR_DATABASE_NAME --config wrangler.self-host.jsonc
Copy that new database's name and ID into d1_databases, keeping binding
METADATA and migrations_dir: "migrations". Keep MCP_SESSIONS bound to
ForgeMcpSession and the v1 new_sqlite_classes migration: Wrangler creates the
Durable Object namespace on deployment. This is distinct from D1 SQL migrations.
pnpm exec wrangler d1 migrations apply METADATA --remote --config wrangler.self-host.jsonc
Apply all checked-in migrations, including 0004_semantic_budget.sql, before
deploying code that uses the shared inference limits. 0002 retains legacy
capture ownership for deletion; new capture does not use an object store.
4. Set secrets and processing policy
Set FORGE_ALLOWED_LOGINS in your Wrangler config to your own GitHub login.
For example, "FORGE_ALLOWED_LOGINS": "your-github-login". This is the service
access list: explicit logins, comma-separated and case-insensitive. Missing,
blank or any malformed entry denies everyone; there is no wildcard. Include
your login in local config too. GitHub OAuth, App installation and a capture
consent entry cannot grant access outside this list.
Generate FORGE_SIGNING_KEY from at least 32 random bytes (for example
openssl rand -hex 32). It signs tokens and approval links and derives the
GitHub-credential encryption key. Rotation invalidates all three and requires
reconnection. Use separate credentials for local development and production.
Set these values in ignored worker/.dev.vars for local checks and development:
-
GITHUB_APP_CLIENT_SECRET: your App's OAuth client secret. -
FORGE_SIGNING_KEY: your random signing key. -
CLOUDFLARE_API_TOKEN: Browser Rendering permission scoped to your account. -
TYPESAFE_API_KEY: the credential admitted by your configured Cloudflare JEV route. -
FORGE_DEV_AUTH_TOKEN: a local bearer token, used only with a local/development config.
Set TYPESAFE_BASE_URL in Wrangler config to your admitted route with exactly
this shape: https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/ai/run.
Use the account to which your JEV route/credential is granted; do not copy the
another operator’s route or substitute an arbitrary model endpoint. Forge requests
typesafe/jev and validates its Cloudflare output contract. Missing credentials,
an unavailable route, malformed responses and failed semantic stages are explicit
failures. There is no alternate provider, model or heuristic fallback. Exact
source reads and ordinary writes remain independent of inference.
FORGE_JEV_PRIVATE_SOURCE is an explicit operator policy. The template sets
deny: private source is refused before semantic processing. Change it to
allow only after informing users that bounded relevant private source and task
context are sent to the configured Cloudflare JEV route. GitHub handles identity
and repository operations; Cloudflare hosts Forge, D1, session state, public-page
rendering and configured inference. Review your processor terms, log retention
and public privacy notice before accepting other people's data. Shape-only logs
exclude source, paths, repository names, queries and user identifiers; frozen
approval evidence is retained separately in D1. See operations
and account deletion for retained data.
Before accepting users, provide them with a private contact route for support
and deletion. The operator who controls the METADATA database must handle
these requests; the source maintainer cannot delete data from your installation.
Review /privacy against your actual processors and retention settings.
Set FORGE_CAPTURE_ALLOWED_LOGINS to your own authenticated GitHub login only
if you consent to public-page capture. This is separate from FORGE_ALLOWED_LOGINS:
capture requires membership in both lists. You may explicitly add a separate
consenting user who already has service access, comma-separated. Empty or absent
denies all captures before quota/provider work; there is no wildcard or public
opt-out flag. Enabled accounts can capture arbitrary already-public URLs. This
restriction controls who can use capture. Browser request isolation depends on
the provider; keep capture limited to trusted, consenting people and public pages.
From worker/, check locally without network calls or deployment:
node scripts/check-config.mjs --config wrangler.self-host.jsonc --secrets-file .dev.vars
The checker parses JSONC, refuses placeholders, requires a valid nonempty FORGE_ALLOWED_LOGINS list, checks
routes/resources/bindings/policies, and checks required secret
presence. It prints field names and errors, never values. Check provider
permissions and the connection after deploying.
--config-only explicitly skips credentials. With no --secrets-file it checks
the process environment; it never fetches deployed secrets.
Upload production secrets interactively, from worker/:
pnpm exec wrangler secret put GITHUB_APP_CLIENT_SECRET --config wrangler.self-host.jsonc
pnpm exec wrangler secret put FORGE_SIGNING_KEY --config wrangler.self-host.jsonc
pnpm exec wrangler secret put CLOUDFLARE_API_TOKEN --config wrangler.self-host.jsonc
pnpm exec wrangler secret put TYPESAFE_API_KEY --config wrangler.self-host.jsonc
Do not upload FORGE_DEV_AUTH_TOKEN to production or set a GitHub private key.
The deployment login and the runtime Browser Rendering token have separate jobs,
even when a deployment subprocess uses the same environment variable name.
5. Check, deploy and connect
From the root, pnpm check checks TypeScript, Vitest and the focused Node
setup/runbook regressions. The root Node range is >=22.18.0 <27. To run only
these fixtures, use node --test worker/scripts/*.node-test.mjs. Then from worker/:
pnpm exec wrangler deploy --config wrangler.self-host.jsonc
Open <FORGE_PUBLIC_ORIGIN>/health and verify the returned version, then open
the landing and /privacy pages and check OAuth discovery. Connect your client
to <FORGE_PUBLIC_ORIGIN>/mcp, authorise your App and install it on your
disposable repository. Refresh the connection and start a new conversation
after tool changes. See the release checklist.
6. Try a read and edit
Ask for the disposable repository's README.md with an exact path. Confirm
the result includes the immutable source SHA and the expected content. Then
explicitly authorise one harmless marker edit in that disposable repository:
In
MY_LOGIN/DISPOSABLE_REPO, addForge self-host check: DATEto README.md directly on the default branch.
Inspect the returned branch, full commit SHA and GitHub URL. Open that actual GitHub commit and confirm the repository, branch, marker diff and resulting README at that SHA. Read the file back to check the result.
For review, explicitly ask for a proposal; it uses the one fixed forge branch.
Only merge/discard require a human approval link. Open and approve each on a
disposable proposal and verify GitHub's resulting commit or deletion separately.
Screenshots, when enabled, return in the conversation. Forge does not store a gallery.
Local development
Copy your filled config to worker/wrangler.self-host.local.jsonc. Set
FORGE_ENVIRONMENT to local and FORGE_PUBLIC_ORIGIN to
http://localhost:8787. From worker/:
node scripts/check-config.mjs --config wrangler.self-host.local.jsonc --secrets-file .dev.vars --local
pnpm exec wrangler d1 migrations apply METADATA --local --config wrangler.self-host.local.jsonc
pnpm exec wrangler dev --local --config wrangler.self-host.local.jsonc
This uses .dev.vars beside the config, with no named environment.
A freshly migrated local D1 database has no users. Complete GitHub OAuth once
against this local Worker and its local METADATA database before using the
development bearer token. Authorising the production Worker populates its
remote database and does not create a local user.
Register http://localhost:8787/oauth/callback on your own development GitHub
App, and use that App's client ID and secret in the local config/secret file.
With the local Worker running, connect an OAuth-capable client that can reach
http://localhost:8787/mcp, without the development bearer token, and complete
GitHub authorisation. Keep the same local config and database persistence when
returning to development bearer authentication.
The development token must resolve an existing allowed local user; use
your own single-user development database and include your login in
FORGE_ALLOWED_LOGINS. With no allowed user, development bearer access is
refused even when the token matches. Do not insert GitHub tokens
by hand or create a default user to bypass OAuth. Local bearer authentication
does not itself test OAuth or populate the database. This bootstrap is required
setup. Use the release checklist to check your deployed instance.
Optional Codex connection
The clone does not configure an MCP server automatically. To opt in, copy
.codex/config.example.toml to .codex/config.toml and replace
<YOUR_FORGE_ORIGIN> with your deployed origin. The local file is ignored by Git.
Use only your own configured server URL.
Optional Cloudflare Access
Use Cloudflare Access when you want a separate identity policy for the people
who can reach Forge through an MCP portal. Keep FORGE_ALLOWED_LOGINS explicit:
Forge still checks GitHub identity and repository authority for every user.
Follow Cloudflare’s guide for MCP servers with existing OAuth.
Register your own MCP URL in Access AI controls, select your identity provider,
and allow only named people. Keep Forge’s existing OAuth flow; do not enable
Managed OAuth, because Forge does not validate Access JWTs as MCP credentials.
Allow only the callback hosts required by your chosen portal in
FORGE_OAUTH_ALLOWED_REDIRECT_HOSTS, then complete upstream GitHub authorization
and reconnect your client through the portal. Keep these deployment values out
of the public repository.
A portal policy does not by itself remove direct access to the upstream URL.
Forge’s GitHub allowlist must remain enforced there. Do not place a generic
browser login screen in front of /mcp and assume MCP clients can use it.
Test allowed and denied accounts, token refresh and approval links before
switching an existing connection. If you disable workers.dev and preview URLs,
verify your custom route first so your own client remains connected.