gitfor
A git forge built entirely on Cloudflare: repositories live in Artifacts, and everything else runs on Workers, D1, KV, R2, Queues, Workflows, Durable Objects and Containers.
Features
- Repositories: create, import from HTTPS remotes, fork, archive, delete, public/private visibility
- Browsing: trees, syntax-highlighted files, raw files, README rendering, commit history, commit diffs, branches, tags,
.tar.gzarchives - Git over HTTPS: clone/fetch/push through a Worker proxy authenticated with personal access tokens
- Web editing: create, edit, rename and delete files, optionally on a new branch
- Branch protection: required pull requests, approvals and status checks, deletion protection, admin bypass
- Issues: labels, assignees, comments, locking, closing keywords (
fixes #12), full-text search - Pull requests: cross-fork PRs, inline review threads, approvals and change requests, review requests, drafts, merge/squash/rebase, mergeability checks
- Actions: GitHub-Actions-style workflows in
.gitfor/workflows/*.ymlwithneeds, matrices,if:expressions, secrets, artifacts and live logs - Runners: Cloudflare Containers by default, plus self-hosted runners (
gfd runner, a container per job) - Releases with R2-backed assets, webhooks with HMAC signatures and redelivery, notifications, activity feeds, organizations
Architecture
| Concern | Cloudflare primitive |
|---|---|
| Git objects and refs | Artifacts (Workers binding + smart HTTP) |
| Users, repos, issues, PRs, CI state | D1 (with FTS5 search) |
| Sessions, refs/object/diff caches, token cache | KV |
| Logs, CI artifacts, release assets | R2 |
| Webhook fan-out and delivery, notifications | Queues (with retries and a DLQ) |
| Post-push pipeline, CI run orchestration | Workflows (PushWorkflow, CiRunWorkflow) |
| Job execution, log streaming, merges | Durable Object Runner with a Container |
| Push events from Artifacts | triggers.events (cf.artifacts.repo.pushed) |
Key code paths:
src/server.ts— Worker entry: git proxy, runner API, raw/archive/asset routes, queue and cron handlerssrc/server/git/— pkt-line, refs, packfile writer, receive-pack client, tree diff, merge-basesrc/server/ci/— workflow schema, expressions, compiler, bash job script, runner DO, schedulersrc/fns/— TanStack Start server functions used by the UImigrations/— D1 schema
Setup
Requirements: Bun, Docker (for building the runner image), a Cloudflare account on the Workers Paid plan with Artifacts enabled.
bun install
Create resources and put their IDs in wrangler.jsonc (also set account_id if your login has several accounts):
bunx wrangler d1 create gitfor
bunx wrangler kv namespace create gitfor
bunx wrangler r2 bucket create gitfor
bunx wrangler queues create gitfor-events
bunx wrangler queues create gitfor-events-dlq
bunx wrangler secret put SECRETS_KEY
The Artifacts namespace (gitfor) is referenced by the binding in wrangler.jsonc; check it with bunx wrangler artifacts namespaces get gitfor.
Set PUBLIC_URL in wrangler.jsonc to your deployment URL. Optional: GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET secrets enable GitHub login.
Set USERCONTENT_ORIGIN to a separate registrable domain routed to the same Worker (for example https://gitforusercontent.dev), 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 or PAGES_DOMAIN.
Local development maps localhost to usercontent.localhost automatically.
Develop
echo "SECRETS_KEY=dev-secret" > .dev.vars
bun run db:migrate
bun run dev
The Artifacts binding uses remote: true, so local development talks to your real Artifacts namespace.
Deploy
bun run deploy
The first account to sign up becomes the instance administrator. Set SIGNUPS_ENABLED to "false" to close registration afterwards.
Using git
Create a token under Settings → Access tokens, then use your username and the token as the password:
git clone https://gitfor.dev/alice/project.git
Workflows
# .gitfor/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: cloudflare
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- run: bun install && bun test
- uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.node }}
path: coverage
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, linux]
steps:
- run: ./deploy.sh
env:
TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Supported triggers: push (branches, tags, paths filters), pull_request (types, branches, paths), release, workflow_dispatch (with inputs).
Steps are run scripts (bash, sh, python, node, bun) plus checkout, upload-artifact and setup actions treated as no-ops; third-party actions are not supported.
Job status is published as commit statuses named <workflow> / <job>, which branch protection can require.
Self-hosted runners
Create a registration token under Repository settings → Runners (or /orgs/<owner>/runners for owner-wide runners), choosing the labels and the isolation, then on the host (needs Docker or Podman):
gfd runner register --url https://gitfor.dev --token gitfor_reg_...
gfd runner run
Each job runs in a fresh, non-root container with a read-only root filesystem and an egress firewall that blocks cloud metadata and private networks.
Fork pull request jobs never reach self-hosted runners unless a repository admin opts in, and then only runners whose container isolation the admin declared on the token.
See the gfd README for what is and isn't isolated. The old runner/gitfor-runner.ts is retired.
Pages
Static sites are served from <owner>.<PAGES_DOMAIN>/<repo>/ (a repo named <owner>.<PAGES_DOMAIN> is served at the root) and pull request previews from /_pr/<number>/. PAGES_DOMAIN must be a separate registrable domain from the forge so user content can never read forge cookies.
- Add Worker routes for
*.<PAGES_DOMAIN>/*(with a wildcard DNS record and certificate). - Custom domains use Cloudflare for SaaS: set
PAGES_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. - Deploys copy files into content-addressed R2 objects (
pages/objects/<git blob sha>), publish a manifest to KV and flip a KV pointer, so rollbacks are instant.
Scripts
| Command | Purpose |
|---|---|
bun run dev |
Local dev server |
bun run build |
Production build and type-check |
bun run deploy |
Apply D1 migrations and deploy |
bun run cf-typegen |
Regenerate worker-configuration.d.ts |
bun run lint / bun run format |
oxlint / oxfmt |
License
Released under the 0BSD license.