Self-hosting
gitfor deploys to your own Cloudflare account. This guide covers creating the resources, configuring the instance and deploying it. For local development see development.md.
Requirements
- Bun
- Docker, to build the runner image
- A Cloudflare account on the Workers Paid plan with Artifacts enabled
bun install
wrangler.jsonc is the production config. bun run dev and bun run preview layer development overrides
from vite.config.ts on top of it, so local development never touches the deployed repositories.
Production setup
bun run deploy first runs bun run deploy:check, which fails while any D1, KV or Secrets Store id is still
the all-zero placeholder, or while PUBLIC_URL or USERCONTENT_ORIGIN is empty. A deployed Worker also
refuses to serve (503, with the reasons logged) if:
PUBLIC_URLis loopback or plain httpUSERCONTENT_ORIGINis missing or unsafeSECRETS_KEYis shorter than 32 charactersARTIFACTS_SHARDSnames a missing binding
Resources
Create each resource and put its id in wrangler.jsonc. Also set account_id if your login has several
accounts.
- [ ] D1:
bunx wrangler d1 create gitfor→d1_databases[0].database_id - [ ] D1 (advisory index):
bunx wrangler d1 create gitfor-advisories→d1_databases[1].database_id(theADVISORY_DBbinding; published-advisory matching for dependency graphs) - [ ] KV:
bunx wrangler kv namespace create gitfor→kv_namespaces[0].id - [ ] R2:
bunx wrangler r2 bucket create gitfor - [ ] Queues:
bunx wrangler queues create <name>forgitfor-events,gitfor-securityandgitfor-integrations - [ ] Dead-letter queues:
gitfor-events-dlq,gitfor-security-dlqandgitfor-integrations-dlq - [ ] Artifacts namespace
stig(theartifactsbinding and both trigger filters). Check it withbunx wrangler artifacts namespaces get stig. Development usesstig-dev; never share a namespace between deployments. - [ ] Analytics Engine dataset
gitfor_usage(created on first write, nothing to run) - [ ] Secrets Store:
bunx wrangler secrets-store store create gitfor→secrets_store_secrets[0].store_id, then add thegitfor-oidc-signing-keysecret to it
Secrets and vars
- [ ]
SECRETS_KEY, at least 32 random characters:openssl rand -base64 48 | bunx wrangler secret put SECRETS_KEY - [ ]
PUBLIC_URL: your https forge URL - [ ]
USERCONTENT_ORIGIN: an https origin on a separate registrable domain, for examplehttps://gitforusercontent.dev(see User content origin) - [ ] Routes or custom domains for the forge host and the user-content host (and the
SITES_DOMAINandEDITOR_ORIGINhosts if you use them), all pointing at this Worker - [ ]
SUPPORT_EMAIL(falls back toLEGAL_CONTACT_EMAIL; the deploy check warns when both are empty) - [ ] Optional:
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETsecrets enable GitHub login
Optional integrations each have their own guide: billing, analytics, notifications and integrations.
First admin
Set ADMIN_EMAIL: while the instance has no admin, the account whose verified primary email matches becomes
admin (at verification, or when that address is made primary).
Alternatively, bunx wrangler secret put BOOTSTRAP_TOKEN (at least 16 characters), open
/signup?bootstrap=<token>, and that signup becomes the first admin. It is the only signup allowed while
SIGNUPS_ENABLED=false and the instance is empty. Delete the secret afterwards.
Only local development makes the first signup an admin automatically.
Further admins are granted and revoked under Site admin -> Site admins. Revoke there rather than editing
users.is_admin in D1: revoking also removes deploy tokens the person minted on repositories they only
administered as a site admin, and the last admin can't be removed.
Sign-up cap and waitlist
Site admin → Sign-up cap limits how many user accounts may exist. With no cap set (the default), anyone
can sign up while SIGNUPS_ENABLED is on.
Once the cap is reached, the signup page asks new visitors for their email and adds them to a waitlist. When the cap is raised, or a seat frees up, the people at the front of the line are emailed an invite link, in the order they joined. An invite holds a seat for 48 hours and works only with the address it was sent to. If it isn't used in time, that person loses their place and the next person is invited. Removing the cap invites everyone waiting. A cron advances the line every five minutes.
Deploy
bun run deploy
This checks the config, builds, applies D1 migrations and runs wrangler deploy.
User content origin
Set USERCONTENT_ORIGIN to a separate registrable domain routed to the same Worker, the way GitHub serves user
files from githubusercontent.com. Raw files, archives, release assets, attachments, avatars and proxied
markdown images are then served only from that origin, which never receives the forge's __Host- session
cookie. Private content gets a short-lived signed grant on redirect.
The value must not be the forge host, the editor origin, or under SITES_DOMAIN. Local development maps
localhost to usercontent.localhost automatically.
Sites
Sites (design) are served under SITES_DOMAIN.
- Public Suffix List: before opening Sites to the public, get
SITES_DOMAIN(for examplegitforstatic.dev) into the private section of the Public Suffix List. Until it ships in browsers, every site and scratch site shares one cookie and storage site. See PSL submission. - Cost controls:
SITES_ENABLED("false"stops all Sites traffic),SITES_DAILY_CPU_MS_LIMITandSITES_DAILY_REQUEST_LIMIT("0"is unlimited; past either, dynamic sites answer 503 for the rest of the UTC day and admins are notified). - Custom domains use Cloudflare for SaaS. Set
CUSTOM_DOMAINS_ZONE_IDto the SaaS zone (with a fallback origin and a*/*Worker route) and add aCF_API_TOKENsecret with "SSL and Certificates: Edit" on that zone. Leave it empty to turn custom domains off. - Billing for custom domains (optional): create a recurring $5/month Stripe price (lookup key
gitfor_custom_domain_monthly,metadata.addon = "custom_domains") and set it as theSTRIPE_PRICE_CUSTOM_DOMAINsecret. Without it, the plan'scustomDomainsflag or an admin grant allows custom domains. See billing.md.
Chargebacks
A Stripe chargeback disables the billed account and tells the customer to email SUPPORT_EMAIL. Winning the
dispute does not re-enable the account; an admin does that from /admin. See billing.md.
Artifacts namespaces
Artifacts rate-limits control-plane calls per namespace (2,000 per 10 seconds), so a large instance can spread repositories over several namespaces.
Each extra namespace needs its own binding, named ARTIFACTS_1, ARTIFACTS_2, … (the first stays
ARTIFACTS), and its own cf.artifacts.repo.pushed and cf.artifacts.repo.forked trigger entries in
wrangler.jsonc. Without them, pushes and forks in that namespace never reach the push and provisioning
Workflows. Then set ARTIFACTS_SHARDS to the total count.
Only new repositories are spread out. The shard is part of each repository's Artifacts name, so never remove a binding that still holds repositories.
Orphan sweep
A cron finds Artifacts repositories with no matching row, which keep incurring storage charges.
ARTIFACTS_ORPHAN_SWEEP controls it:
| Value | Effect |
|---|---|
report (default) |
Logs orphans only |
off |
Disables the sweep |
delete |
Deletes orphaned repos |
delete only takes effect with ARTIFACTS_NAMESPACE_EXCLUSIVE=true, a non-localhost PUBLIC_URL and outside
local dev; otherwise it logs an error and keeps reporting. It also halts without deleting, logging
ARTIFACTS ORPHAN SWEEP HALTED, when more than 20 orphans turn up in one run or more than 5% of the
repositories on a listing page look orphaned. That usually means D1 and the namespace belong to different
deployments. Set ARTIFACTS_NAMESPACE_EXCLUSIVE only when no other deployment, local development included,
uses the namespace.