Skip to content
584 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Running g1t yourself: the design, a docker compose proof, and a guide to what works today1# Self-hosting g1t
2
3The goal (2026-10-05): g1t should not be locked to Cloudflare. Anyone should
4be able to run it on their own machine with `docker compose up`. The free
5core is MIT and self-hostable; managed hosting at g1t.sh is the paid
6product, and it stays on Cloudflare. Self-hosting must never make hosted
7g1t worse, so hosted code paths do not change to make room for it.
8
9This document covers:
10
111. An inventory of every Cloudflare dependency in the code.
122. The design: ports and adapters, with the runtime choice weighed.
133. What phase 1 ships today: `deploy/self-host/`, and what was verified.
144. The phased plan, with estimates.
155. The risks.
16
17The user guide is `apps/docs/src/content/docs/guides/self-hosting.md`.
18
19## The short version
20
21- **g1t is already shaped for this.** Every service talks to every other
22 over plain HTTP: `POST /rpc/<method>` with a JSON body (`crates/kit`,
23 `packages/contracts/src/clients.ts`). Every database is SQLite (D1).
24 Git storage sits behind a `GitStore` port (`services/repos/src/store.rs`),
25 and everything past that port speaks git's smart HTTP to a remote URL
26 with a bearer token.
27- **The fastest credible path is to run the Workers themselves in
28 workerd**, the open-source Workers runtime, not to port them. Phase 1
29 runs every core Worker unchanged (the same WebAssembly and the same
30 bundles that deploy to Cloudflare) in one workerd process under
31 `wrangler dev`. D1, KV and Queues are kept on a volume as SQLite files.
32- **Three things replace Cloudflare-only bindings**, as small Workers bound
33 in their place, with no change to the services:
34 - `ARTIFACTS` becomes an Artifacts-compatible shim in front of a git
35 store, which keeps plain bare repositories on disk and serves them
36 with `git http-backend`.
37 - `EMAIL` becomes a shim that logs each message and hands it to Mailpit,
38 which can relay to any SMTP server.
39 - The runner and the context hub are bound to an "off" Worker, which
40 answers "agents are off" instead of failing.
41- **Proven on this machine with Docker:** sign up, confirm the email
42 through Mailpit, create a workspace and a repository, push and clone over
Merge branch 'worktree-agent-af58ac8933b0dd125'43 HTTP (the second clone from the clone pack cache in RustFS), open an
Merge branch 'worktree-agent-aaf03bdceac799c89'44 issue, and browse code, commits and files in the site; then the REST API,
Merge branch 'worktree-agent-af58ac8933b0dd125'45 OAuth metadata and MCP on their own port, an npm package published and
46 installed and a container image pushed and pulled through RustFS, pull
47 requests from a branch and from a fork merged onto `main`, the merge queue taking a pull request and
Merge branch 'worktree-agent-aaf03bdceac799c89'48 giving it back, and every cron handler the scheduler runs. All of it runs
49 against local storage. See [Phase 1: what works today](#3-phase-1-what-works-today).
Running g1t yourself: the design, a docker compose proof, and a guide to what works today50- **Long term:** keep workerd as the runtime, because it is what hosted
51 runs. Replace `wrangler dev` with a production workerd configuration.
52 Move the binding shims into code-level ports in `g1t_kit` and a TS
53 `@g1t/platform` package, so each primitive has a hosted and a
54 self-hosted adapter behind one interface.
55
56## 1. Inventory
57
58The sources are every `wrangler.jsonc` plus a grep of the code. Coupling
59is graded:
60
61- **thin**: one call site or a config switch;
62- **adapter**: already behind a port, or easy to put behind one;
63- **woven**: the logic is shaped around the product.
64
65### By primitive
66
67| Primitive | Where | Coupling | Self-hosted equivalent |
68| --- | --- | --- | --- |
69| **Workers runtime**, service bindings | Every service. Rust through `worker` 0.8 (`#[event(fetch\|queue\|scheduled)]`, `Env`, `Fetcher`); TS as `export default { fetch, queue, scheduled }` | woven (as a runtime), thin (as an API) | **workerd**: the same runtime, open source. Service bindings work as they do hosted. Calls are HTTP (`POST /rpc/<method>`), so a native port could use plain HTTP clients. |
70| **Workers RPC** (JS methods across a binding) | Only `RUNNER`: `RunnerService extends WorkerEntrypoint` (`services/runner/src/index.ts:626`). `apps/web` calls `env.RUNNER.enabled/run/plan/...` directly in 11 routes. | thin | workerd supports it. A native port needs these on `/rpc/*` as well; the runner already has a `fetch` shim for Rust callers. |
71| **D1** | System of record for 13 services. Rust: `env.d1("DB")`; TS: `D1Database`; `db.batch()` in `crates/kit` `rename` | woven (SQL), thin (API) | **SQLite files**. workerd/Miniflare implements D1 on SQLite, and the same `migrations/` apply with `wrangler d1 migrations apply --local`. A native port would need a `Database` port over `rusqlite`/`better-sqlite3`; the SQL is already SQLite, including FTS5. |
Merge branch 'worktree-agent-af58ac8933b0dd125'72| **KV** | `BLOBS`: Actions artifacts and cache (`apps/api/src/blobs.rs`, `apps/web/app/lib/artifacts.server.ts`). `AVATARS`: `services/identity/src/avatars.rs`, `apps/web/workers/app.ts`, `services/og`. `DOMAINS`: `services/deployments/src/domains.ts`, `services/pages` | thin | Miniflare KV on disk (SQLite plus blob files). Natively: a `BlobStore` port on the filesystem or S3. |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today73| **Queues**: the event bus | Producer: `services/events` `BUS.sendBatch` (`lib.rs:67`). The consumer writes the log, then fans out to every binding named `SUBSCRIBER_*` (`lib.rs:161`). Twelve consumers, one queue each. Private job queues in search (`g1t-search-jobs`) and context (`g1t-context-jobs`); consumers branch on the queue name. | woven | Miniflare Queues: in-process and persisted, which works today. Natively: a `Bus` port with a SQLite outbox and a poller per subscriber, or NATS/Redis Streams. At-least-once delivery and idempotent consumers are already the contract. |
74| **Durable Objects** | Only `AttemptSandbox` (runner), as the containers library's base class. Uses `ctx.storage.get/put/delete`, `schedule()` (alarm), `idFromName`/`idFromString`, DO RPC (`run`, `destroy`, `noteBlocked`). **Not used:** WebSocket hibernation, raw `alarm()`, `ctx.storage.sql`, `ctx.exports`. | woven, in the runner only | workerd supports Durable Objects (on-disk SQLite). Runner state can move to the sandbox supervisor (phase 2). |
75| **Containers** (`@cloudflare/containers`) | `services/runner`: one sandbox per agent run, Actions job and deploy build. `sleepAfter`, `start({ envVars, enableInternet })`, `onStop`. Image: `services/runner/Dockerfile` (node 24, git, toolchains, Claude Code, `g1t-runner`). | woven | **Docker or Podman** through the socket, with the same image. Wrangler can already run Containers locally through Docker; whether that covers outbound interception has to be tested. |
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76| **Outbound interception** (guardrails egress) | `services/runner/src/guard.ts` (`egress()`, `abuse()`), `egress.ts` (`sandboxHosts`, `buildHosts`, `EGRESS_CA = /etc/cloudflare/certs/cloudflare-containers-ca.crt`), `AttemptSandbox.outboundHandlers`, `interceptHttps = true`, `setOutboundHandler("egress", { hosts })`, `setOutboundByHost("sandbox.g1t.internal", "abuse")` (a sandbox reporting it stopped itself for mining; `crates/runner/src/abuse.rs`) | woven | The sandbox joins an internal network with no route out, and gets `HTTP(S)_PROXY` pointing at an allow-list proxy that checks the `CONNECT` host. The CA bundle is then not needed, because nothing is re-signed. Blocked hosts are reported to the runner as `noteBlocked` does today. |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today77| **Artifacts** (git storage) | Only `services/repos/src/store.rs` (`ArtifactsStore`, behind the `GitStore`/`GitRepo` traits). Methods used: `create`, `get`; then `info`, `createToken`, `log`, `readCommit`, `readTree`, `readBlob`, `readFile`, `fork`, and `[Symbol.dispose]`. Everything else (push, fetch, landing, catch-up, import, ref listing) is smart HTTP to `info().remote` with `Bearer <token>`: `land.rs`, `catch_up.rs`, `refs.rs`, `import.rs`, `git_http.rs`. | adapter | **Bare repositories on disk plus `git http-backend`.** Built in phase 1: `deploy/self-host/gitstore`. Forks are local clones with hard links. Tokens are HMAC-signed, scoped, and expire. |
78| **Cache API** | `services/repos/src/store.rs` (trees and blobs up to 1 MiB, by hash), `apps/web/workers/app.ts` (avatars), `services/og` | thin, optional | Miniflare's cache, or none. Every use tolerates a miss. |
79| **Vectorize** | `services/context/src/index.ts` (`VECTORS.upsert/deleteByIds/query`); optional, guarded by `if (!AI \|\| !VECTORS)` | thin | **sqlite-vec** (default: one file, next to D1), pgvector or Qdrant behind a `VectorIndex` port; or off, which already degrades to keyword search. |
80| **Workers AI** | `services/context` only: `@cf/baai/bge-base-en-v1.5` embeddings (768 dims) | thin | An OpenAI-compatible `/v1/embeddings` endpoint (Ollama, vLLM, LM Studio, or a hosted API) behind an `Embedder` port. Changing models means re-embedding (a backfill job already exists). |
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier81| **AI Gateway** | `services/runner/src/model-env.ts` (`modelEnv`), `services/models/src/route.ts:70` (gateway URL, `cf-aig-*` headers), `services/billing/src/keeper.rs` (reads gateway logs to settle) | thin | Optional already: an empty `AI_GATEWAY_ID` goes straight to the provider. Any Anthropic- or OpenAI-compatible base URL works for a workspace's own provider. |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today82| **Workers for Platforms** | `services/pages` (dispatcher: `env.APPS.get(script).fetch`), `services/deployments/src/cloudflare.ts` (script and asset upload through the REST API) | woven | Phase 1: off. Later: a self-hosted app host in workerd, using the Worker Loader binding to load uploaded scripts, or a workerd per app (see [Deployments](#deployments)). |
83| **Cloudflare for SaaS** (custom hostnames) | `services/deployments/src/custom-hostnames.ts` (`/zones/{id}/custom_hostnames`) | thin | Caddy with on-demand TLS, asking g1t whether a hostname is allowed. |
84| **Cloudflare REST API** | deployments: script upload, list, delete, assets, GraphQL usage. Billing keeper: AI Gateway logs, `billable-usage`, GraphQL container usage. Ops scripts in `scripts/`. | thin (deployments), woven (keeper pricing) | Deployments: the app-host adapter. Keeper: off when self-hosted, because there is no bill to reconcile. |
85| **Email Sending** | `services/identity/src/email.rs` (`EMAIL.send({to, from, subject, text, html})`); callers: verification, password reset, `admin.rs` limit warnings | thin | Built in phase 1: a shim that logs and hands mail to Mailpit, which relays over SMTP. Later: a `Mailer` port with an SMTP adapter. |
86| **Cloudflare Access** | `apps/sudo/app/lib/access.ts` (verifies `Cf-Access-Jwt-Assertion` against `/cdn-cgi/access/certs`, `ACCESS_AUD`, `STAFF_EMAILS`) | woven, in sudo only | A local admin flag: `G1T_ADMINS` usernames checked against the normal g1t session. Self-hosters rarely need sudo, which is about billing. |
Merge branch 'worktree-agent-aaf03bdceac799c89'87| **Cron Triggers** | actions (every minute), webhooks (every minute), identity (`*/15`), security (`*/30`), repos and packages (hourly), events (daily), billing (`*/15` and daily), deployments (`*/10`), runner (`*/5`) | thin | workerd runs `scheduled()` when asked, but never on its own. `deploy/self-host/scheduler.mjs` asks: once a minute, inside the g1t container, it runs each due cron through Wrangler's local API (`POST /cdn-cgi/local/explorer/api/local/scheduled?worker=<name>`, answered only on localhost), the same handler Cron Triggers run. The services and crons come from `schedules.json`, which `configs.mjs` writes from each `wrangler.jsonc` for the services in its `SELF_HOST_CRONS`: repos, events, identity, security, webhooks and packages. Not run: actions (it would start scheduled workflows with no runner), billing (Cloudflare and Stripe) and deployments (Cloudflare's API). A handler still running from the minute before is not started again, and one is given up on after ten minutes. `scheduler.mjs --once` runs every cron of every service now and exits non-zero if one failed (smoke.sh uses it). The status page has its own loop (`status.sh`). |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today88| **`cloudflare:workers` imports** | `apps/web` (`env` in 15 files), `apps/sudo`, `services/runner` (`WorkerEntrypoint`) | thin | Provided by workerd. A Node port would pass `env` through context instead. |
89| **Static Assets** | `apps/web` (Vite plugin build), `apps/docs`, `apps/sudo` (`run_worker_first`) | thin | workerd serves them. |
90| **`placement`, `observability`, routes, custom domains** | every `wrangler.jsonc` | config only | Dropped by `deploy/self-host/configs.mjs`. |
91| **`cf-ray`** | Used as an audit request id, with a fallback: `services/repos/src/run_access.rs:131`, `apps/api/src/audit.rs:37` | thin | Falls back already. |
Merge branch 'worktree-agent-af58ac8933b0dd125'92| **R2** | `services/packages` (`BLOBS`: container layers and other package files), `services/repos` (`BACKUPS`: nightly backup bundles; `GIT_PACKS`: the clone pack cache), the API's Actions cache (`ACTIONS_CACHE`), the runner's downloads | thin | **S3-compatible storage**: the `BlobStore` port in `crates/blobstore` has an R2 adapter and an S3 one (`s3.rs`, SigV4 over fetch); each service names its own bucket (`BLOB_STORE`/`S3_BUCKET` for packages, `BACKUP_STORE`/`BACKUP_S3_BUCKET` for backups, `PACK_STORE`/`PACK_S3_BUCKET` for clone packs), run against RustFS in the compose file. |
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member93| **Not used** | Hyperdrive, Workflows, Analytics Engine, Browser Rendering, Images, Turnstile, Secrets Store, `connect()`, HTMLRewriter, `request.cf` | — | — |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today94
95### By service
96
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow97The deployable units, their stage and what each is built from are listed in
98`deploy/stack.jsonc` (see `docs/DEPLOYING.md`); its `self_host` field is
99what `deploy/self-host/configs.mjs` runs, turns off or leaves out. A test
100checks this table names every unit.
101
Running g1t yourself: the design, a docker compose proof, and a guide to what works today102| Service | Runs on | Cloudflare dependencies beyond Workers and D1 | Phase 1 self-hosted |
103| --- | --- | --- | --- |
104| `apps/web` | TS Worker plus assets | KV (`BLOBS`, `AVATARS`), Cache API, `cloudflare:workers` `env`, RPC to `RUNNER` | Runs unchanged |
Merge branch 'worktree-agent-aaf03bdceac799c89'105| `apps/api` | Rust Worker | KV `BLOBS`, R2 `ACTIONS_CACHE`; `api.g1t.sh`/`mcp.g1t.sh` addresses (now the `API_URL`, `MCP_URL` and `SITE_URL` settings, hosted defaults when unset: `src/addresses.rs`) | Runs in a second workerd on its own port (`API_PORT`, 8789; `self_host: "separate"`), started by `start.sh` once the first is up. Its service bindings reach the other Workers through Wrangler's dev registry. `API_URL` (default: `PUBLIC_URL`'s host on `API_PORT`) is its OAuth issuer; MCP is the path `/mcp` on it (`MCP_URL`). Its KV is its own, apart from the site's: Actions artifacts need the runner, which is off |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today106| `apps/sudo` | TS Worker plus assets | Access JWT | Not run |
107| `apps/docs` | Static | — | Not run (docs.g1t.sh serves them) |
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow108| `apps/status` | TS Worker | Email Sending, cron; bound only to billing | Runs in a process of its own (`status.sh`), so it stays up when the site does not |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today109| `services/identity` | Rust | Email Sending, KV `AVATARS` | Runs unchanged; `EMAIL` goes to the mail shim |
Merge branch 'worktree-agent-af58ac8933b0dd125'110| `services/repos` | Rust | **Artifacts**, **R2** (`BACKUPS`), Cache API, optional KV `GIT_CACHE` with `REPOS_KEY`, optional R2 `GIT_PACKS` | Runs unchanged; `ARTIFACTS` goes to the git store, backups to the `g1t-backups` bucket in RustFS (`BACKUP_STORE=s3`), and the clone pack cache to `g1t-git-packs` (`PACK_STORE=s3`: the `PackStore` port in `src/pack_cache.rs` over the shared `BlobStore`, multipart, an object only once whole; `storage-setup` gives the bucket a lifecycle rule that deletes packs after 7 days and aborts uploads unfinished after a day). Without `GIT_CACHE` and `REPOS_KEY`, credentials and ref listings are kept per isolate only. Its nightly cron queues backups, but bundles are cut by the runner, which is off in phase 1: none are made yet |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today111| `services/work` | Rust | Queue consumer | Runs unchanged |
112| `services/events` | Rust | Queues (producer and fan-out) | Runs unchanged; the off services' queues are not produced to |
113| `services/projects` | TS | Queue consumer | Runs unchanged |
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)114| `services/chat` | TS | Durable Objects (one room per channel, WebSocket hibernation), KV `AVATARS` (custom emoji images, under `emoji/`) | Runs unchanged; workerd runs its Durable Objects, and the site serves emoji images from the same KV |
Docs: a workspace knowledge base people and agents write together115| `services/docs` | TS | Durable Objects (one room per page: the Yjs document, WebSocket hibernation, SQLite storage, alarms), **R2** (`FILES`, files in pages, behind the `FileStore` interface in `src/files.ts`), D1 with FTS5 | Runs unchanged; workerd runs its Durable Objects, and `FILES` is the local R2 bucket Wrangler keeps on disk (an S3 `FileStore` for RustFS is the next step) |
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)116| `services/notify` | TS | Durable Objects (one feed per person: WebSocket hibernation, SQLite storage); outbound HTTPS to browsers' push services | Runs unchanged; browser push needs a VAPID key pair (`node scripts/ops/vapid-keys.mjs`), else notifications are live in open tabs only |
Chat and workspace agents: channels, DMs and named agents you talk to117| `services/agents` | TS | Durable Objects (one desk per agent, alarms) | Runs unchanged; replies reach a model through the `MODELS` binding (the model proxy), which is off, so an agent answers with a short apology |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today118| `services/search` | Rust | Queues (events and its own jobs); FTS5 | Runs unchanged |
119| `services/billing` | Rust | Cron, Cloudflare REST API (keeper), Stripe | Runs with `FREE_WHILE_BUILDING=true` and no Stripe key: nothing is charged |
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member120| `services/security` | Rust | Queue, cron | Runs unchanged; its cron through `scheduler.mjs` |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today121| `services/actions` | Rust | Queue, cron, `ACTIONS_KEY` | Runs; jobs need the runner, which is off |
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member122| `services/webhooks` | Rust | Queue, cron, `WEBHOOKS_KEY` | Runs; retries through `scheduler.mjs` |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today123| `services/integrations` | Rust | Queue, `INTEGRATIONS_KEY` | Runs unchanged |
Merge branch 'worktree-agent-af58ac8933b0dd125'124| `services/packages` | Rust | **R2** (`BLOBS`), cron, queue, `PACKAGES_TOKEN_SECRET` | Runs with `BLOB_STORE=s3` against the compose file's RustFS (`deploy/self-host/configs.mjs`); no request size limit (`MAX_REQUEST_BYTES` 0 means none) |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today125| `services/deployments` | TS | Workers for Platforms, REST API, KV `DOMAINS`, cron | Runs with no API token: nothing deploys |
126| `services/runner` | TS | **Containers**, Durable Objects, outbound interception, AI Gateway, cron | Off: bound to the off Worker |
127| `services/context` | TS | **Vectorize**, **Workers AI**, Queues | Off: bound to the off Worker |
Chat and workspace agents: channels, DMs and named agents you talk to128| `services/models` | TS | AI Gateway; public at `models.g1t.sh` | Not run; bound to the off Worker (sandboxes and agent replies call it) |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today129| `services/pages` | TS | Dispatch namespace, wildcard routes, KV | Not run |
130| `services/og` | TS | Cache API | Not run (social cards are optional) |
131| `crates/runner` | native, in the sandbox | Talks to `https://api.g1t.sh` and `https://g1t.sh` (hard-coded in the runner Worker); `wrangler deploy --dry-run` for builds | Phase 2 |
132| `crates/sshd` | native | Not deployed; calls `/_internal/ssh/*` endpoints that do not exist yet | Phase 3 |
133
134### Hard-coded hosted addresses
135
Merge branch 'worktree-agent-aaf03bdceac799c89'136Self-hosting needs one setting, `PUBLIC_URL`, in place of these, and
137`configs.mjs` derives the rest from it. These are settings now, each with
138the hosted address as its default, so hosted g1t sets nothing:
Running g1t yourself: the design, a docker compose proof, and a guide to what works today139
Merge branch 'worktree-agent-aaf03bdceac799c89'140- The site (`apps/web/app/lib/addresses.ts`): `SITE_URL`, `API_URL`,
141 `MCP_URL` and `OG_URL` (empty: no social card tags). The root loader
142 hands them to the page; `meta.ts`, the clone box, agent setup, the
143 pull request and merge box remotes, the tokens page and the OAuth
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address144 consent's `iss` read them. `USERCONTENT_URL` (raw files and avatars,
145 `apps/web/workers/usercontent.ts`): g1tusercontent.com hosted, else
146 `<SITE_URL>/-/usercontent` unless set to a host of its own;
147 `USERCONTENT_KEY` (made by `start.sh`) signs private files' addresses.
Merge branch 'worktree-agent-aaf03bdceac799c89'148- The API (`apps/api/src/addresses.rs`): `SITE_URL`, `API_URL` (the OAuth
149 issuer) and `MCP_URL` (the protected resource; a path on the API's host
150 self-hosted).
151- Identity's mail (`services/identity/src/email.rs`): `SITE_URL` for
152 links and the logo, `MAIL_FROM` for the sender. The mail shim still
153 rewrites any `https://g1t.sh` link left in a message.
154
155Still hard-coded, for phase 2:
156
157- `apps/web/workers/app.ts` (`DOCS`); defaults in
158 `components/invites-section.tsx`, `components/runners.tsx`,
159 `lib/invites.ts` and `lib/legal.ts`.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today160- `services/runner/src/index.ts` (`G1T_API`, `GIT_REMOTE` and the other
161 remotes handed to sandboxes, in 12 places).
162- `services/billing` (`stripe.rs`, `accounts.rs`, `limits.rs`).
163- `crates/sshd` (`G1T_API` default).
164
165There are about 150 occurrences of `g1t.sh` in non-test code. Most are
166docs links and copy, and need no change.
167
168## 2. Design
169
170### Principles
171
1721. **Hosted is the reference.** Hosted code does not change behaviour to
173 make room for self-hosting. A self-hosted adapter is added beside the
174 hosted one, and the hosted one stays the default.
1752. **Swap at the narrowest seam that exists.** A binding-shaped seam (a
176 Worker that offers the same methods as the Cloudflare binding) needs no
177 code change, and is how phase 1 works. A code-level port (a trait or
178 interface with two adapters) is cleaner and testable, and is the long-term
179 shape. Each primitive moves from the first to the second when it is next
180 touched.
1813. **One runtime, two hosts.** The Workers stay Workers. workerd runs them
182 self-hosted, so one build serves both and there is no second code path
183 to keep correct.
1844. **Off is a real mode.** Every optional subsystem (agents, context,
185 deployments, billing) has an "off" answer that pages already handle.
186
187### The ports
188
189| Port | Hosted adapter | Self-hosted adapter | Lives in | Status |
190| --- | --- | --- | --- | --- |
191| `GitStore` / `GitRepo` | `ArtifactsStore` | Git store (bare repos, `git http-backend`) through an Artifacts-compatible shim; later a `LocalGitStore` that calls the git store's HTTP API from Rust directly | `services/repos/src/store.rs` (exists) | **Built** (binding level) |
192| `Mailer` | Email Sending binding | SMTP (through Mailpit relay now; a direct SMTP adapter later) | `g1t_kit::mail` | **Built** (binding level) |
193| `Sandbox` | `AttemptSandbox` (Containers, DO) | `DockerSandbox`: a supervisor that starts the runner image through the Docker API | `services/runner` (TS) | Phase 2 |
194| `Egress` | Containers outbound handler | Allow-list HTTP(S) proxy on an internal network | `services/runner` | Phase 2 |
195| `ModelRoute` | AI Gateway, or direct | Any Anthropic/OpenAI-compatible base URL | `services/models`, `runner/model-env.ts` | Exists (env-switchable) |
196| `Embedder` | Workers AI | OpenAI-compatible `/v1/embeddings` | `services/context` | Phase 3 |
197| `VectorIndex` | Vectorize | sqlite-vec / pgvector / Qdrant | `services/context` | Phase 3 |
198| `AppHost` | Workers for Platforms plus REST API | workerd app host (Worker Loader) | `services/deployments` | Phase 3 |
199| `Domains` | Cloudflare for SaaS | Caddy on-demand TLS | `services/deployments` | Phase 3 |
200| `AdminAuth` | Cloudflare Access | `G1T_ADMINS` plus the normal session | `apps/sudo` | Phase 4 |
201| `Bus` | Queues | Miniflare Queues now; SQLite outbox later | `services/events`, `g1t_kit` | Works (runtime) |
202| `Database` | D1 | SQLite files through workerd | — | Works (runtime) |
203| `Blobs` | KV | Miniflare KV now; filesystem/S3 later | — | Works (runtime) |
Merge branch 'worktree-agent-aaf03bdceac799c89'204| `Scheduler` | Cron Triggers | `scheduler.mjs`: a ticker that calls `scheduled()` through Wrangler's local API | `deploy/self-host` | **Built** for the services in `SELF_HOST_CRONS`; each handler checked by `smoke.sh` |
205| `PackStore` | R2 `GIT_PACKS` | S3 (`PACK_STORE=s3`) through `crates/blobstore` | `services/repos/src/pack_cache.rs` | **Built** |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today206| `UsageKeeper` | Cloudflare bill plus AI Gateway logs | None (billing off) | `services/billing` | Off |
207
208For Rust, the code-level ports go in `crates/kit` as traits (`g1t_kit::ports`),
209with the Cloudflare adapters next to them. For TypeScript, they go in a new
210`packages/platform` package, with each adapter in its own module so a
211hosted bundle never pulls in a self-hosted adapter. The adapter is chosen at
212startup from one setting, `G1T_MODE=hosted|self`, never per request.
213
214### The runtime: workerd, or native binaries
215
216| | **workerd (phase 1: `wrangler dev`; later a plain workerd config)** | **Native: Rust on axum/hyper, TS on Node** |
217| --- | --- | --- |
218| Effort to first boot | **Done.** About a day, almost all of it shims and config. | Weeks. The Rust services use `worker::*` throughout: `Env`, `Fetcher`, `D1Database`, the `#[event]` macros and `js_sys` interop. Each needs an abstraction layer before it compiles natively. TS needs an `env` provider in place of `cloudflare:workers`, plus Queues, D1 and KV clients. |
219| Fidelity to hosted | **The same runtime and the same bundles.** A bug self-hosted is a bug hosted. | A second implementation of every platform API, with its own bugs. |
220| Performance | Good for one node: WebAssembly in V8, SQLite on local disk. Single-threaded per isolate; plenty for a team. | Better per core, and multi-threaded. That matters only at a scale where people use g1t.sh. |
221| Maintenance | Low. New hosted features run self-hosted for free, unless they add a new Cloudflare-only binding. The config generator then drops it or binds a stand-in. | High. Every feature is built twice, or behind a port that both sides keep honest. |
222| Scale-out | One process, one disk. D1-on-SQLite is single-writer. | Could use Postgres and many processes. |
223| Operational risk | `wrangler dev` is a development tool (see risks). Moving to `workerd serve` with a generated config, or a small Miniflare-API launcher, removes the dev-tool surface. | Conventional. |
224
225**Recommendation.**
226
227- **Phase 1:** workerd under `wrangler dev`, as built in `deploy/self-host/`.
228 It needs no change to any service and is proven end to end.
229- **Phase 2:** replace `wrangler dev` with a launcher that drives Miniflare's
230 API directly, or a generated `workerd` config. It should expose only the
231 site and the API, with no dev endpoints, run cron, and run as a proper
232 service.
233- **Long term:** keep workerd as the runtime and push the remaining
234 Cloudflare-only bindings behind code-level ports. Compile to native only
235 what already is native (the runner binary, sshd), or a service where
236 workerd is a real limit, case by case.
237
238A full native port is not worth it. It would double the maintenance of
239every feature, which is the opposite of what makes self-hosting
240sustainable for a small team.
241
242### Sandboxes (phase 2)
243
244- **Engine.** Docker or Podman through the socket, mounted into a small
245 supervisor (`g1t-sandboxd`), not into the workerd container. The
246 supervisor exposes the runner's `Sandbox` port over HTTP: start with
247 env, stop, status, and the exit report that becomes `onStop`. It uses
248 the **same image** (`services/runner/Dockerfile`).
249- **Runner.** The runner Worker runs in workerd with `AttemptSandbox`
250 replaced by a `DockerSandbox` adapter. Run state that lives in
251 `ctx.storage` moves to the supervisor's SQLite; `schedule()`/`timeUp`
252 becomes a supervisor timer.
253- **Egress guardrails.** Each restricted sandbox joins an internal Docker
254 network with no default route. Its `HTTP_PROXY`/`HTTPS_PROXY` point at an
255 allow-list proxy (for example a small Go or Node `CONNECT` proxy) that
256 admits the hosts `sandboxHosts()` computes and reports refusals.
257 Nothing is intercepted, so no CA is injected. `EGRESS=off` puts the
258 sandbox on a normal network.
259- **Addresses.** `G1T_API`, `GIT_REMOTE` and the rest become
260 `PUBLIC_URL`-derived settings. Sandboxes reach the site and API on the
261 compose network.
262- **Shortcut to evaluate first.** Wrangler can already run Containers
263 classes locally through Docker. If its local Containers support
264 `setOutboundHandler`, the runner could run nearly unchanged. Test this
265 before building the supervisor.
266
267### Git storage
268
269Artifacts gives g1t: named repositories, scoped short-lived tokens, a smart
270HTTP remote, typed reads (commit, tree, blob, file, log), copy-on-write
271forks, and jurisdictions. g1t uses everything except jurisdictions.
272
273The self-hosted git store (`deploy/self-host/gitstore/server.mjs`, about 450
274lines of Node with no dependencies) provides the same:
275
276| Artifacts | Git store |
277| --- | --- |
278| `create(name, { setDefaultBranch, description })` | `git init --bare --initial-branch`, plus `g1t.json` beside it for metadata |
279| `get(name)` then `info()` | Metadata, `HEAD`, and the last push time; `remote` is `GITSTORE_URL/git/<name>.git` |
280| `createToken(scope, ttl)` | HMAC-SHA256 over `{ key, scope, expiry }` with the shared secret |
281| Smart HTTP remote | `git http-backend`; a read token cannot push |
282| `log`, `readCommit`, `readTree`, `readBlob`, `readFile` | `git rev-list --first-parent`, `cat-file`, `ls-tree` |
283| `fork(name, { defaultBranchOnly })` | `git clone --bare [--single-branch]` with hard-linked objects |
284
285Hosted is untouched: `ArtifactsStore` is still the only adapter compiled
286into `services/repos`. Self-hosted, the `ARTIFACTS` binding is a service
287binding to `deploy/self-host/workers/artifacts`, which offers Artifacts'
Merge branch 'worktree-agent-a2013627e5ea4ab13'288methods and calls the git store. The git store can later gain `git gc`
289scheduling and object-store-backed packs for large installations.
290
291The same git store is hosted g1t's cold fallback for an Artifacts outage
292(docs/ARTIFACTS.md, R12). For that it takes namespaced keys as well as
293plain ones (`g1t-us-1/acme--rocket`, kept at
294`<GITSTORE_ROOT>/g1t-us-1/acme--rocket.git` and served at
295`/git/g1t-us-1/acme--rocket.git`, the shape Artifacts gives remotes), and
296`GITSTORE_READ_ONLY=1` refuses pushes, write tokens and making, forking or
297deleting repositories. `services/repos/src/fallback.rs` calls its API from
298Rust, the start of the `LocalGitStore` adapter: a namespace named in
299`GIT_FALLBACK_NAMESPACES` is served from it, with the shim out of the path.
300Self-hosted installations keep using the shim, with plain keys; nothing
301changes for them.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today302
303### Search and context
304
305- **Site search** (`services/search`) is D1 FTS5. It works self-hosted as
306 is (verified: `/search?q=hello` answers 200).
307- **Context hub** (`services/context`) needs an `Embedder` and a
308 `VectorIndex`:
309 - Default: **sqlite-vec** in the context service's own SQLite, with
310 embeddings from an **OpenAI-compatible endpoint**. Ollama's
311 `nomic-embed-text` has the same 768 dimensions as today's
312 `bge-base-en-v1.5`.
313 - Alternatives: pgvector or Qdrant, for installations that already run
314 them.
315 - Off: the service already degrades to keyword search when `AI` or
316 `VECTORS` is missing (`index.ts:919`), so phase 3 can first run context
317 with neither bound.
318
319### Models
320
321Already portable. The model proxy (`services/models`) and the runner's
322model environment take any Anthropic-compatible base URL, and an empty
323`AI_GATEWAY_ID` skips AI Gateway. Self-hosted:
324
325- a workspace connects its own provider under Integrations (Anthropic, or
326 any Anthropic-compatible endpoint, including a local gateway in front of
327 OpenAI-compatible models);
328- "hosted models" are off, because there is no g1t key to spend.
329
330### Deployments
331
332- **Phase 1–2: off.** `services/deployments` runs, but with no
333 `CLOUDFLARE_API_TOKEN` nothing deploys. Its pages and settings still
334 render.
335- **Phase 3: an app host on workerd.** The self-hosted `pages` dispatcher
336 uses workerd's **Worker Loader** binding to load each uploaded app's
337 modules on demand, keyed by deployment id, from the blob store. Static
338 assets are served from the same store. Builds run in Docker sandboxes
339 (phase 2), which already bundle with `wrangler deploy --dry-run`.
340 Wildcard hosts (`*.apps.example.com`) and custom domains go through
341 Caddy with on-demand TLS. The alternative, one workerd process per app,
342 is simpler to isolate but heavier.
343
344### Billing and the feature map
345
346Self-hosted billing is **off by default**: no Stripe, no usage limits,
347no keeper. Billing still runs, because the shell reads `billing.account`
348on every page, but with `FREE_WHILE_BUILDING=true` and no Stripe key.
349
350| Feature | Hosted (g1t.sh) | Self-hosted default | Self-hosted, when turned on |
351| --- | --- | --- | --- |
352| Accounts, workspaces, repos, git over HTTP | On | On | — |
Merge branch 'worktree-agent-aaf03bdceac799c89'353| Issues, pull requests, review | On | On | — |
354| Merge queue | On | Takes pull requests; testing and landing them needs sandboxes | Phase 2 |
355| Bringing a pull request up to date before it lands (catch-up) | On | Off: needs a sandbox | Phase 2 |
Merge branch 'worktree-agent-af58ac8933b0dd125'356| Clone pack cache | R2 | RustFS (`g1t-git-packs`) | — |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today357| Site search (FTS5) | On | On | — |
358| Email | Email Sending | Mailpit, logged | SMTP relay |
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member359| Webhooks, integrations | On | On (retries through `scheduler.mjs`) | — |
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent360| g1t's agent | On | Off | Phase 2: Docker sandboxes plus your own model provider |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today361| Guardrails egress | Containers interception | n/a | Phase 2: allow-list proxy |
362| Hosted models (g1t's key) | On (billed) | Off | Never: bring your own |
363| Context hub semantic search | Vectorize plus Workers AI | Off | Phase 3: sqlite-vec plus an OpenAI-compatible embedder |
364| Deployments | Workers for Platforms | Off | Phase 3: workerd app host |
365| Custom domains | Cloudflare for SaaS | Off | Phase 3: Caddy on-demand TLS |
366| Billing, limits, Stripe, keeper | On | Off | Not planned |
367| sudo (staff console) | Access | Off | Phase 4: `G1T_ADMINS` |
368| Git over SSH | Not yet | Off | Phase 3 (`crates/sshd`, which is native already) |
Merge branch 'worktree-agent-aaf03bdceac799c89'369| REST API, OAuth, MCP | On | On, on `API_PORT` | — |
370| CLI | On | Off | Phase 2 |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today371
372### Auth, Access and email
373
374- `apps/sudo` checks Cloudflare Access. Self-hosted, it should check a
375 normal g1t session against `G1T_ADMINS`. That needs an `AdminAuth` port in
376 `workers/app.ts` with two adapters. It is phase 4, because sudo is about
377 billing.
378- Sessions are random tokens stored hashed in D1, with no signing key, so
379 nothing to configure. The cookie is `Secure`: fine on `localhost`, but any
380 other address needs HTTPS. The compose file should gain an optional Caddy
381 service in phase 2.
382- **Email verification is required** before creating anything, and there is
383 no bypass in code. That is why the proof ships a working mail path
384 (Mailpit) rather than "email off". An admin "mark verified" or
385 `G1T_SKIP_EMAIL_VERIFICATION` belongs with `G1T_ADMINS`.
386
387### Configuration, upgrades and backups
388
389- **Today:** environment variables in the compose file (`PUBLIC_URL`,
Merge branch 'worktree-agent-aaf03bdceac799c89'390 `G1T_PORT`, `API_PORT`, `API_URL`, `MCP_URL`, `MAIL_URL`, `MAIL_FROM`,
391 the S3 store and its buckets). Keys (`ACTIONS_KEY`,
Running g1t yourself: the design, a docker compose proof, and a guide to what works today392 `INTEGRATIONS_KEY`, `WEBHOOKS_KEY`, the git store secret) are generated on
393 first start and kept on volumes.
394- **Phase 2:** one `g1t.toml`, read by the launcher and turned into
395 bindings and variables:
396
397 ```toml
398 public_url = "https://git.example.com"
399
400 [mail]
401 smtp = "smtp://user:pass@smtp.example.com:587"
402 from = "g1t <git@example.com>"
403
404 [agents] # off when absent
405 docker = "unix:///var/run/docker.sock"
406 egress = "enforce"
407
408 [context] # off when absent
409 embeddings = "http://ollama:11434/v1"
410 model = "nomic-embed-text"
411
412 [admins]
413 usernames = ["alice"]
414 ```
415
416- **Upgrades.** The container applies every service's D1 migrations to its
417 SQLite file on start (`wrangler d1 migrations apply --local`). Applied
418 migrations are recorded in `d1_migrations`, so this is idempotent. Hosted
419 and self-hosted run the same migration files, which keeps them
420 forward-compatible. Rule to keep: migrations stay additive, or come with
421 a backfill a self-hoster's start can run.
422- **Backups.** Phase 1: stop, then tar the `g1t-data` and `g1t-git`
423 volumes (documented in the guide). Phase 2: online backups with
Merge branch 'worktree-agent-ac5b181a013e54348'424 `sqlite3 .backup` per database, or Litestream for continuous
425 replication. The repositories get hosted g1t's nightly bundles
426 (docs/ARTIFACTS.md, R11) once the runner runs: the storage is already
427 configured (`BACKUP_STORE=s3`, the `g1t-backups` bucket that
Merge branch 'worktree-agent-af58ac8933b0dd125'428 `storage-setup` makes, `BACKUP_S3_BUCKET` to choose another), and the
Merge branch 'worktree-agent-ac5b181a013e54348'429 restore drill reads a copy of that bucket
Merge branch 'worktree-agent-af58ac8933b0dd125'430 (`aws --endpoint-url <S3_ENDPOINT> s3 sync s3://g1t-backups ./copy`, then
Merge branch 'worktree-agent-ac5b181a013e54348'431 `node scripts/ops/backup-restore-drill.mjs --bundles ./copy --repo-id <id> --live <bare repository>`).
Running g1t yourself: the design, a docker compose proof, and a guide to what works today432
433## 3. Phase 1: what works today
434
435Everything is in `deploy/self-host/`:
436
437| File | What it is |
438| --- | --- |
Merge branch 'worktree-agent-af58ac8933b0dd125'439| `docker-compose.yml` | `g1t` (every core Worker in one workerd on 8787, and the API in a second on 8789), `status`, `gitstore` (bare repositories), `rustfs` (S3-compatible storage for packages, backups and clone packs; `RUSTFS_IMAGE`, default `rustfs/rustfs:1.0.1`), `storage-setup` (the AWS CLI, `AWS_CLI_IMAGE`, default `amazon/aws-cli:2.37.10`: makes the three buckets and puts the packs' bucket's lifecycle rule, which expires `packs/` after 7 days and aborts multipart uploads unfinished after a day; RustFS's scanner applies it), and `mailpit` (mail). Volumes: `g1t-data`, `g1t-git`, `g1t-objects`, `g1t-status`, `g1t-secrets`. |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today440| `Dockerfile` | Compiles the ten Rust services to WebAssembly with `worker-build`, as hosted does. Builds the site with React Router. The runtime image has Node, Wrangler, workerd and the built Workers. |
441| `Dockerfile.dockerignore` | Build-context rules for this image only (the root `.dockerignore` leaves out the site). |
Merge branch 'worktree-agent-aaf03bdceac799c89'442| `start.sh` | Makes the sealing keys once, writes the configs, applies migrations, runs `wrangler dev` with every config on `0.0.0.0:8787`, persisting to `/data/state`, and the API's `wrangler dev` on `0.0.0.0:8789` once the first answers. Starts `scheduler.mjs`. |
443| `scheduler.mjs` | The cron ticker (see Cron Triggers above). |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today444| `configs.mjs` | Derives each self-hosted Wrangler config from the hosted `wrangler.jsonc`. It drops routes, account and placement, rebinds `ARTIFACTS`/`EMAIL` and the off services, and rewrites hosted URLs. Derived, so it cannot drift. |
445| `gitstore/server.mjs`, `gitstore/Dockerfile` | The git store. |
446| `workers/artifacts/index.js` | The `ARTIFACTS` binding, implemented against the git store. |
447| `workers/mail/index.js` | The `EMAIL` binding: logs, then sends to Mailpit. |
448| `workers/off/index.js` | The runner and the context hub when they are off. |
Merge branch 'worktree-agent-af58ac8933b0dd125'449| `smoke.sh` | The end-to-end check, including the clone pack cache, the API, an npm package published and installed, pull requests, the merge queue and (with `SCHEDULER_ONCE`) every cron handler. |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today450
451Workers running: the site; identity, repos, work, events, projects, search,
Merge branch 'worktree-agent-aaf03bdceac799c89'452billing, security, actions, webhooks, integrations, packages and
453deployments; the artifacts, mail and two off stand-ins; and, in a second
454workerd, the API.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today455
456### Verified
457
458On this machine (Windows 11, Docker Desktop 29.8, engine on Linux):
459
4601. **Without Docker, with local processes.** The git store ran under Node
461 on Windows and every Worker ran under `wrangler dev`, using the configs
462 from `configs.mjs` and the existing Rust builds. `smoke.sh` passed every
463 step: sign-up, confirmation through the logged link, workspace, repo,
464 `git push` and `git clone` over HTTP, issue, and the code, blob and
465 commits pages. Seventeen other pages answered 200: home, workspace,
466 repo overview, issues, pulls, settings, agents, actions, deployments,
467 security, people, usage, explore, search, account settings and tree.
468 The workspace context page answered 403 from the off stand-in, as
469 intended.
Merge branch 'worktree-agent-aaf03bdceac799c89'4702. **With Docker Compose** (2026-10-07, `docker compose up --build`, with
Merge branch 'worktree-agent-af58ac8933b0dd125'471 `API_PORT=18789` because 8789 was taken on this machine; the store is
472 RustFS 1.0.1, its buckets made by `storage-setup`). `smoke.sh`
Merge branch 'worktree-agent-aaf03bdceac799c89'473 passed every step: the ones above; a second clone answered from the pack
Merge branch 'worktree-agent-af58ac8933b0dd125'474 cache (`Server-Timing: pack;desc=hit`), with the packs in RustFS's
475 `g1t-git-packs` and its lifecycle rule in place (packs expire after 7
476 days, unfinished uploads are aborted after 1); an access token made in the
477 site; an npm package published to the installation's registry and
478 installed back, its tarball in `g1t-packages`; `GET /user`, the API index (`mcp_url`, `git_url`), the OAuth
Merge branch 'worktree-agent-aaf03bdceac799c89'479 metadata (`issuer` the API's address, `authorization_endpoint` on the
480 site), MCP's 401 challenge and `tools/list`; a pull request from a branch
481 and one from a fork (`create_pull_request` without a branch: a fork in
482 the git store, pushed to with the token, marked ready) merged onto
483 `main`; the merge queue turned on, a pull request merged into it and
484 shown `waiting`, taken out (`unqueue`), the queue turned off and the
485 pull request merged; and `scheduler.mjs --once`, every handler `ok`.
486 The repository page's clone box and MCP line named the installation's
Merge branch 'worktree-agent-af58ac8933b0dd125'487 own addresses, with no social card tags. By hand against the same
488 stack: `docker push` and `docker pull` of an image with a 20 MB layer
489 (uploaded in 10 MiB parts), the layer read back from `/v2/.../blobs/`
490 with a matching digest, the `g1t-backups` bucket present (empty: the
491 runner cuts bundles), no unfinished upload in any bucket, and the
492 guide's upgrade copy from an old MinIO volume into RustFS.
4933. **The clone pack cache against RustFS without the stack.**
494 `node services/repos/dev/clone-check.mjs --s3` (RustFS in Docker, the
495 bucket and lifecycle rule made with the AWS CLI): misses then hits for
496 full and shallow clones over protocol v2 and v0, a miss after the refs
497 version moves, five whole packs in the bucket (12 MB each, multipart
498 objects of 3 parts), each reading back at its listed size, no
499 unfinished upload, and both lifecycle rules.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today500
501### Not verified, or not working yet
502
Merge branch 'worktree-agent-aaf03bdceac799c89'503- The merge queue past `waiting`, and catch-up (bringing a pull request
504 up to date before it lands): both need a sandbox, and the runner is off.
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member505- Actions schedules (`on: schedule`), and billing's and deployments' crons.
Merge branch 'worktree-agent-aaf03bdceac799c89'506- The CLI, and git over SSH.
507- An OAuth sign-in from start to finish (the metadata and issuer are
508 checked, the consent flow is not).
Running g1t yourself: the design, a docker compose proof, and a guide to what works today509- Anything on an address other than `localhost` without HTTPS (the
510 session cookie is `Secure`).
511- Restart durability beyond one restart, upgrades across schema changes,
512 and backup and restore.
513
514## 4. Phased plan
515
516Estimates are for one engineer who knows the codebase, working with
517agents. They include docs and tests.
518
519| Phase | Scope | Estimate |
520| --- | --- | --- |
Merge branch 'worktree-agent-aaf03bdceac799c89'521| **1. Core forge** | **Done:** compose stack, git store, Artifacts/Email shims, off stand-ins, config generator, smoke test, guide; the API (REST, MCP, OAuth) on its own port with a `PUBLIC_URL`-derived issuer; `PUBLIC_URL`-derived settings in identity mail, the site's meta tags, clone box and agent setup, and the API; the cron ticker; the clone pack cache on S3; pull requests from branches and forks and the merge queue's enqueue and removal in `smoke.sh`. **Left:** optional Caddy for HTTPS; a CI job that builds the images and runs `smoke.sh`; the remaining hard-coded addresses listed above. | 2–3 days left |
Running g1t yourself: the design, a docker compose proof, and a guide to what works today522| **2. Agents with Docker sandboxes** | Test Wrangler's local Containers first. Otherwise: `g1t-sandboxd` supervisor, `DockerSandbox` adapter in the runner, egress allow-list proxy and internal network, runner addresses from `PUBLIC_URL`, models through a workspace's own provider, `g1t.toml` and a launcher that replaces `wrangler dev`. | 2–3 weeks |
523| **3. Search, context and deployments** | `Embedder` (OpenAI-compatible) and `VectorIndex` (sqlite-vec first) ports in context; app host on workerd with Worker Loader; Caddy on-demand TLS for app and custom domains; git over SSH through `crates/sshd` plus the missing `/_internal/ssh/*` endpoints. | 3–4 weeks |
524| **4. Parity and upgrade path** | Code-level ports in `g1t_kit` / `@g1t/platform` replacing the binding shims (`LocalGitStore` in Rust, `Mailer` with SMTP); `AdminAuth` for sudo; online backups (Litestream or `.backup`); versioned releases with published images; an upgrade test in CI that migrates a snapshot of the previous release; a self-host column in the docs for every feature. | 3–4 weeks |
525
526Total to parity: about 10–13 weeks. Phase 1 alone is already a credible
527"run it yourself" for the core forge.
528
529## 5. Risks
530
531- **`wrangler dev` is a development tool.** It exposes Miniflare's dev
532 endpoints (`/cdn-cgi/...`, including a local data explorer) on the same
533 port as the site. Treat phase 1 as **localhost or a trusted private
534 network only** until the launcher in phase 2 replaces it. Its flags and
535 behaviour can also change between Wrangler releases. Pin the Wrangler
536 version, as the lockfile already does.
Merge branch 'worktree-agent-aaf03bdceac799c89'537- **The API reaches the other Workers through Wrangler's dev registry.**
538 Two `wrangler dev` processes in one container find each other through
539 a registry directory, a development feature like the rest. If the API
540 starts and a binding says `[not connected]`, restart the container. The
541 phase 2 launcher serves both from one workerd.
Merge branch 'worktree-agent-af58ac8933b0dd125'542- **The object store.** The compose file runs RustFS (Apache-2.0),
543 pinned to a release tag, and makes its buckets with the AWS CLI
544 (Apache-2.0). Any S3-compatible store can take its place
545 (`S3_ENDPOINT`), given the same buckets and the packs' lifecycle rule.
546 Installations started before 2026-10-07 kept these files in MinIO, in
547 the `g1t-packages` volume; the guide's "Upgrade" section copies them
548 across.
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member549- **Cron goes through Wrangler's local API.** `scheduler.mjs` asks
550 `/cdn-cgi/local/explorer/api/local/scheduled`, a development endpoint
551 that may change between Wrangler releases (pinned by the lockfile).
552 Actions schedules, billing and deployments are not run.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today553- **New Cloudflare-only bindings break self-hosting silently.**
554 `configs.mjs` passes unknown keys through untouched. A new binding type
555 could make `wrangler dev` reach for a remote resource (for example
556 `remote: true`, AI or Vectorize) and prompt for a login. Mitigation: CI
557 that builds the compose stack and runs `smoke.sh` on every change, and an
558 allow-list in `configs.mjs` that fails on unknown binding types.
559- **Artifacts semantics drift.** The shim copies the Artifacts methods g1t
560 uses today. If repos starts using another method (`import`,
561 `listTokens`, `revokeToken`), self-hosted fails at runtime. Mitigation: a
562 contract test that runs `services/repos` against the git store, and the
563 long-term `LocalGitStore` port.
564- **Error codes over RPC.** `ArtifactsStore::create` and `fork` tolerate
565 `ALREADY_EXISTS` by reading the thrown error's `code`. Workers RPC may not
566 carry custom error properties across a service binding. If it does not,
567 retrying a half-finished create fails self-hosted where it would succeed
568 hosted. This was not seen in testing, because creates were never retried.
569- **Single node, single writer.** SQLite (D1 local) and one workerd
570 process suit a team, not a large organisation. Scaling out means
571 Postgres behind a `Database` port, which is a large change and is not
572 planned.
573- **The `Secure` cookie.** It needs HTTPS anywhere but `localhost`. A LAN
574 install over plain HTTP cannot sign in.
575- **Building from a working tree that is mid-change.** The image compiles
576 every Rust service, including ones other work is changing. A service that
577 does not compile breaks the whole image. Released images (phase 4) fix
578 this.
579- **Image size and build time.** The first build compiles ten Rust crates
580 to WebAssembly and installs the site's dependencies. Expect minutes and
581 several GB. Published images remove this for users.
Merge branch 'worktree-agent-aaf03bdceac799c89'582- **Hard-coded hosted URLs.** The few left (listed above) point at the
583 hosted service until they read a setting: the docs link, invite and
584 runner defaults, and the runner's remotes.

This file's history is long; its oldest lines are credited to the oldest commit read.