162 lines · 8.3 KB

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_URL is loopback or plain http
  • USERCONTENT_ORIGIN is missing or unsafe
  • SECRETS_KEY is shorter than 32 characters
  • ARTIFACTS_SHARDS names 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 (the ADVISORY_DB binding; 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> for gitfor-events, gitfor-security and gitfor-integrations
  • [ ] Dead-letter queues: gitfor-events-dlq, gitfor-security-dlq and gitfor-integrations-dlq
  • [ ] Artifacts namespace stig (the artifacts binding and both trigger filters). Check it with bunx wrangler artifacts namespaces get stig. Development uses stig-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 the gitfor-oidc-signing-key secret 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 example https://gitforusercontent.dev (see User content origin)
  • [ ] Routes or custom domains for the forge host and the user-content host (and the SITES_DOMAIN and EDITOR_ORIGIN hosts if you use them), all pointing at this Worker
  • [ ] SUPPORT_EMAIL (falls back to LEGAL_CONTACT_EMAIL; the deploy check warns when both are empty)
  • [ ] Optional: GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET secrets 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 example gitforstatic.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_LIMIT and SITES_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_ID to the SaaS zone (with a fallback origin and a */* Worker route) and add a CF_API_TOKEN secret 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 the STRIPE_PRICE_CUSTOM_DOMAIN secret. Without it, the plan's customDomains flag 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.