# 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](development.md). ## Requirements - [Bun](https://bun.sh/) - Docker, to build the runner image - A Cloudflare account on the Workers Paid plan with Artifacts enabled ```sh 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 ` 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](#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](billing.md), [analytics](analytics.md), [notifications](notifications.md) and [integrations](integrations.md). ### 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=`, 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 ```sh 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](sites-design.md)) 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](https://github.com/publicsuffix/list). Until it ships in browsers, every site and scratch site shares one cookie and storage site. See [PSL submission](sites-design.md). - **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](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](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.