Skip to content
408 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#!/usr/bin/env node
2// Writes the Wrangler configs a self-hosted g1t runs with, derived from the
3// hosted ones, so the two never drift apart.
4//
5// Each hosted service's wrangler.jsonc is read and changed only where
6// Cloudflare-only things live:
7//
8// - account, routes, placement, observability and builds are dropped;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.9// - GITSTORE (git storage, Cloudflare Artifacts) becomes a service binding to workers/gitstore,
Running g1t yourself: the design, a docker compose proof, and a guide to what works today10// which keeps repositories in the git store (gitstore/server.mjs);
11// - EMAIL (Email Sending) becomes a service binding to workers/mail;
Merge branch 'worktree-agent-af58ac8933b0dd125'12// - the packages service keeps files in S3-compatible storage (RustFS)
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store13// instead of R2, the repos service its nightly backups (a bucket of
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.14// their own, BACKUP_S3_BUCKET), and the artifacts service the files in pages
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store15// (DOCS_S3_BUCKET);
Running g1t yourself: the design, a docker compose proof, and a guide to what works today16// - services that are off in this phase (agents, the context hub, the
17// g1t.page dispatcher, model proxy) are bound to workers/off instead, and
18// events stop queueing work for them;
19// - URLs that name g1t.sh name PUBLIC_URL instead, and billing is free.
20//
21// Usage: node configs.mjs [outDir]
22// Environment: PUBLIC_URL, GITSTORE_URL, GITSTORE_SECRET, MAIL_URL,
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.23// ACTIONS_KEY, INTEGRATIONS_KEY, WEBHOOKS_KEY, IDENTITY_KEY, USERCONTENT_KEY, CONNECTORS_KEY,
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 address24// USERCONTENT_URL,
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store25// PACKAGES_TOKEN_SECRET, S3_ENDPOINT, S3_BUCKET, BACKUP_S3_BUCKET, DOCS_S3_BUCKET, S3_REGION,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member26// S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_PUBLIC_ENDPOINT, and optionally
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27// your own GitHub App: GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID,
28// GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRET.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today29//
30// The output is for `wrangler dev` (see start.sh): every Worker in one
31// workerd, the site first, with D1, KV and Queues kept on disk.
32
33import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
34import { dirname, join, relative, resolve } from "node:path";
35import { fileURLToPath } from "node:url";
36
37const here = dirname(fileURLToPath(import.meta.url));
38const root = resolve(here, "../..");
39const out = resolve(process.argv[2] ?? join(here, ".generated"));
40mkdirSync(out, { recursive: true });
41
42const PUBLIC_URL = (process.env.PUBLIC_URL ?? "http://localhost:8787").replace(/\/$/, "");
43
Merge branch 'worktree-agent-aaf03bdceac799c89'44/**
45 * Where the API (REST, OAuth) is reached: API_URL, or PUBLIC_URL's host on
46 * API_PORT (8789). The MCP server is a path on it: MCP_URL, or
47 * `<API_URL>/mcp`. The API's OAuth issuer is API_URL.
48 */
49export function apiAddresses(env = process.env, publicUrl = PUBLIC_URL) {
50 let api = (env.API_URL ?? "").trim().replace(/\/$/, "");
51 if (!api) {
52 const url = new URL(publicUrl);
53 url.port = env.API_PORT || "8789";
54 api = url.origin;
55 }
56 const mcp = (env.MCP_URL ?? "").trim().replace(/\/$/, "") || `${api}/mcp`;
57 return { api, mcp };
58}
59const { api: API_URL, mcp: MCP_URL } = apiAddresses();
60
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow61// What runs, and what is off, is each unit's `self_host` in
62// deploy/stack.jsonc: the list hosted g1t deploys from.
63const STACK = Object.values(parseJsonc(readFileSync(join(root, "deploy/stack.jsonc"), "utf8")).units);
64
Running g1t yourself: the design, a docker compose proof, and a guide to what works today65/** Services that run, in the order Wrangler is given them (the site first). */
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow66export const RUNNING = STACK.filter((unit) => unit.self_host === "run")
67 .map((unit) => ({ name: unit.worker, dir: unit.path, web: unit.kind === "react-router" }))
68 .sort((a, b) => Number(b.web) - Number(a.web));
Running g1t yourself: the design, a docker compose proof, and a guide to what works today69
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow70/** What the off Worker calls each service that is off in phase 1. */
71const OFF_NAMES = {
Running g1t yourself: the design, a docker compose proof, and a guide to what works today72 "g1t-runner": "Agents",
73 "g1t-context": "Context search and memory",
Chat and workspace agents: channels, DMs and named agents you talk to74 "g1t-models": "Hosted models",
Running g1t yourself: the design, a docker compose proof, and a guide to what works today75};
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow76const OFF = Object.fromEntries(
77 STACK.filter((unit) => unit.self_host === "off").map((unit) => [unit.worker, OFF_NAMES[unit.worker] ?? unit.worker]),
78);
Running g1t yourself: the design, a docker compose proof, and a guide to what works today79
80/** Sealing keys, by the service that holds each (hosted: Wrangler secrets). */
81const SECRETS = {
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 address82 g1t: "USERCONTENT_KEY",
Running g1t yourself: the design, a docker compose proof, and a guide to what works today83 "g1t-actions": "ACTIONS_KEY",
84 "g1t-integrations": "INTEGRATIONS_KEY",
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.85 "g1t-agents": "CONNECTORS_KEY",
Running g1t yourself: the design, a docker compose proof, and a guide to what works today86 "g1t-webhooks": "WEBHOOKS_KEY",
87};
88
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look89/**
90 * g1t.sh's GitHub App is its own: an installation registers one of its
91 * own, or has none, and then no GitHub buttons appear. Its public settings
92 * replace the hosted vars; its secrets go only to the service that uses each.
93 */
94const GITHUB_VARS = ["GITHUB_APP_ID", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID"];
95const GITHUB_SECRETS = {
96 "g1t-identity": ["GITHUB_APP_CLIENT_SECRET", "IDENTITY_KEY", "REGISTRATION_MODE"],
97 "g1t-integrations": ["GITHUB_APP_PRIVATE_KEY", "GITHUB_APP_WEBHOOK_SECRET"],
98};
99
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member100/**
101 * Services whose cron triggers scheduler.mjs runs here: sweeps and
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.102 * reminders that need nothing self-hosting lacks (the artifacts service's is
The docs service answers every artifacts call: docs can be made, listed, shared, moved, trashed, restored, searched, versioned and edited live in their own rooms, agents read, write and recall them only where their person and everyone in the conversation can, and folio events go out on the bus, while Docs' pages keep working as before.103 * emptying artifacts' trash after 30 days). Not run: actions (its
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member104 * minute would start scheduled workflows with no runner to take them),
105 * billing (reconciles against Cloudflare and Stripe), deployments (calls
106 * Cloudflare's API) and the services that are off.
107 */
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.108const SELF_HOST_CRONS = new Set(["g1t-repos", "g1t-events", "g1t-identity", "g1t-security", "g1t-webhooks", "g1t-packages", "g1t-artifacts"]);
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member109
Running g1t yourself: the design, a docker compose proof, and a guide to what works today110/** Queues whose consumers are off: events stops sending to them. */
111const OFF_QUEUES = new Set(["g1t-events-runner", "g1t-events-context"]);
112
113/** Strips comments and trailing commas from JSONC. Strings are respected. */
114function parseJsonc(text) {
115 let result = "";
116 let inString = false;
117 for (let i = 0; i < text.length; i++) {
118 const char = text[i];
119 if (inString) {
120 result += char;
121 if (char === "\\") result += text[++i];
122 else if (char === '"') inString = false;
123 } else if (char === '"') {
124 inString = true;
125 result += char;
126 } else if (char === "/" && text[i + 1] === "/") {
127 while (i < text.length && text[i] !== "\n") i++;
128 result += "\n";
129 } else if (char === "/" && text[i + 1] === "*") {
130 i = text.indexOf("*/", i + 2) + 1;
131 } else {
132 result += char;
133 }
134 }
135 return JSON.parse(result.replace(/,(\s*[}\]])/g, "$1"));
136}
137
138const rel = (path) => relative(out, resolve(root, path)).replaceAll("\\", "/");
139
140function hostedUrl(value) {
Merge branch 'worktree-agent-aaf03bdceac799c89'141 return typeof value === "string"
142 ? value.replace(/https:\/\/api\.g1t\.sh/g, API_URL).replace(/https:\/\/g1t\.sh/g, PUBLIC_URL)
143 : value;
Running g1t yourself: the design, a docker compose proof, and a guide to what works today144}
145
146function selfHosted(service) {
147 const hosted = parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8"));
148 const config = {
149 name: hosted.name,
150 compatibility_date: hosted.compatibility_date,
151 compatibility_flags: hosted.compatibility_flags,
152 rules: hosted.rules,
153 vars: {},
154 };
155
156 if (service.web) {
157 // The site as React Router built it (apps/web/build), not its sources.
158 config.main = rel(`${service.dir}/build/server/index.js`);
159 config.no_bundle = true;
160 config.rules = [{ type: "ESModule", globs: ["**/*.js", "**/*.mjs"] }];
161 config.assets = { directory: rel(`${service.dir}/build/client`) };
162 } else {
163 config.main = rel(join(service.dir, hosted.main));
164 }
165
166 for (const [key, value] of Object.entries(hosted.vars ?? {})) config.vars[key] = hostedUrl(value);
167
168 if (hosted.d1_databases) {
169 config.d1_databases = hosted.d1_databases.map((db) => ({
170 binding: db.binding,
171 database_name: db.database_name,
172 database_id: db.database_id,
173 migrations_dir: rel(join(service.dir, db.migrations_dir ?? "migrations")),
174 }));
175 }
176 if (hosted.kv_namespaces) config.kv_namespaces = hosted.kv_namespaces.map(({ binding, id }) => ({ binding, id }));
177 if (hosted.triggers) config.triggers = hosted.triggers;
178
179 if (hosted.queues) {
180 config.queues = {};
181 if (hosted.queues.producers) {
182 config.queues.producers = hosted.queues.producers.filter((producer) => !OFF_QUEUES.has(producer.queue));
183 }
184 if (hosted.queues.consumers) config.queues.consumers = hosted.queues.consumers;
185 }
186
187 config.services = (hosted.services ?? []).map((binding) =>
188 OFF[binding.service] ? { binding: binding.binding, service: offName(binding.service) } : binding,
189 );
190
191 // Cloudflare-only bindings, and what stands in for them.
192 if (hosted.artifacts) {
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.193 for (const store of hosted.artifacts) {
194 config.services.push({ binding: store.binding, service: "g1t-gitstore" });
Running g1t yourself: the design, a docker compose proof, and a guide to what works today195 }
196 }
197 if (hosted.send_email) {
198 for (const email of hosted.send_email) config.services.push({ binding: email.name, service: "g1t-mail" });
199 }
200
201 // Secrets the hosted services hold, given here from the environment, each
202 // only to the service that uses it.
203 const secret = SECRETS[hosted.name];
204 if (secret && process.env[secret]) config.vars[secret] = process.env[secret];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look205 for (const name of GITHUB_VARS) {
206 if (name in config.vars) config.vars[name] = process.env[name] ?? "";
207 }
208 for (const name of GITHUB_SECRETS[hosted.name] ?? []) {
209 if (process.env[name]) config.vars[name] = process.env[name];
210 }
Running g1t yourself: the design, a docker compose proof, and a guide to what works today211
212 // Self-hosted g1t charges nothing: billing records usage at cost and never
213 // stops work for it.
214 if (hosted.name === "g1t-billing") config.vars.FREE_WHILE_BUILDING = "true";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 // Anyone may register on an installation of your own unless you set
216 // REGISTRATION_MODE=invite; invites then work as on g1t.sh, and the owners
217 // of INVITE_STAFF_WORKSPACES (yours, not g1t.sh's) invite without limit.
218 if (hosted.name === "g1t-identity") {
219 config.vars.REGISTRATION_MODE = process.env.REGISTRATION_MODE || "open";
220 config.vars.INVITE_STAFF_WORKSPACES = process.env.INVITE_STAFF_WORKSPACES ?? "";
221 if (process.env.INVITES_PER_USER) config.vars.INVITES_PER_USER = process.env.INVITES_PER_USER;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas222 // Access requests are summarised to your own address, not g1t.sh's.
223 config.vars.WAITLIST_NOTIFY_EMAIL = process.env.WAITLIST_NOTIFY_EMAIL ?? "";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look224 }
Merge branch 'worktree-agent-af58ac8933b0dd125'225 // Packages' files go to the compose file's RustFS (or any S3-compatible
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member226 // store) instead of R2, with no request size limit, and package
227 // addresses start with this installation's host.
228 if (hosted.name === "g1t-packages") {
229 Object.assign(config.vars, {
230 BLOB_STORE: "s3",
Merge branch 'worktree-agent-af58ac8933b0dd125'231 S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member232 S3_BUCKET: process.env.S3_BUCKET ?? "g1t-packages",
233 S3_REGION: process.env.S3_REGION ?? "us-east-1",
234 S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
235 S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
236 S3_PUBLIC_ENDPOINT: process.env.S3_PUBLIC_ENDPOINT ?? "",
237 MAX_REQUEST_BYTES: "0",
238 STORAGE_LIMITS: "off",
239 REGISTRY_HOST: new URL(PUBLIC_URL).host,
240 PACKAGES_TOKEN_SECRET: process.env.PACKAGES_TOKEN_SECRET ?? "",
241 });
242 delete config.vars.R2_ACCOUNT_ID;
243 delete config.vars.R2_BUCKET;
244 }
Merge branch 'worktree-agent-aaf03bdceac799c89'245 // Where this installation is reached, for the links and addresses each
246 // shows: the site's clone URLs, meta tags and agent setup, the API's
247 // OAuth issuer and MCP server, and identity's mail. No social cards: the
248 // card service (services/og) is not run here.
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 address249 // Repository files and avatars: USERCONTENT_URL, a host of its own that
250 // reaches this same site, or, empty, a path on it (PUBLIC_URL/-/usercontent).
251 if (service.web) {
252 Object.assign(config.vars, { SITE_URL: PUBLIC_URL, API_URL, MCP_URL, OG_URL: "", USERCONTENT_URL: (process.env.USERCONTENT_URL ?? "").trim() });
253 }
Merge branch 'worktree-agent-aaf03bdceac799c89'254 if (hosted.name === "g1t-api") Object.assign(config.vars, { SITE_URL: PUBLIC_URL, API_URL, MCP_URL });
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)255 // Notifications on the g1t phone app go through Expo's push service (and
256 // from there Google's and Apple's), with no setup here, unless
257 // PHONE_PUSH=off: notify then sends phones nothing, and the site's
258 // /.well-known/g1t.json tells the app so.
259 if (service.web || hosted.name === "g1t-notify") config.vars.PHONE_PUSH = process.env.PHONE_PUSH === "off" ? "off" : "on";
Merge branch 'worktree-agent-aaf03bdceac799c89'260 if (hosted.name === "g1t-identity") config.vars.SITE_URL = PUBLIC_URL;
Merge branch 'worktree-agent-ac5b181a013e54348'261 // Nightly backups' bundles go to a bucket of their own on the same
Merge branch 'worktree-agent-aaf03bdceac799c89'262 // S3-compatible store, instead of the BACKUPS R2 bucket, and so do the
263 // packs kept for fresh clones (src/pack_cache.rs), instead of GIT_PACKS.
Merge branch 'worktree-agent-ac5b181a013e54348'264 if (hosted.name === "g1t-repos") {
265 Object.assign(config.vars, {
266 BACKUP_STORE: "s3",
267 BACKUP_S3_BUCKET: process.env.BACKUP_S3_BUCKET ?? "g1t-backups",
Merge branch 'worktree-agent-aaf03bdceac799c89'268 PACK_STORE: "s3",
269 PACK_S3_BUCKET: process.env.PACK_S3_BUCKET ?? "g1t-git-packs",
Merge branch 'worktree-agent-af58ac8933b0dd125'270 S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
Merge branch 'worktree-agent-ac5b181a013e54348'271 S3_REGION: process.env.S3_REGION ?? "us-east-1",
272 S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
273 S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
274 });
275 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store276 // Files people put in Docs pages go to a bucket of their own on the same
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.277 // S3-compatible store, instead of the FILES R2 bucket (services/artifacts
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store278 // src/files.ts, `s3FileStore`).
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.279 if (hosted.name === "g1t-artifacts") {
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store280 Object.assign(config.vars, {
281 DOCS_FILES: "s3",
282 DOCS_S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
283 DOCS_S3_BUCKET: process.env.DOCS_S3_BUCKET ?? "g1t-docs-files",
284 DOCS_S3_REGION: process.env.S3_REGION ?? "us-east-1",
285 DOCS_S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
286 DOCS_S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
287 });
288 }
Running g1t yourself: the design, a docker compose proof, and a guide to what works today289 // Nothing to deploy to: deployments are off (no Cloudflare API token).
290 if (hosted.name === "g1t-deployments") delete config.vars.CUSTOM_HOSTNAMES_ZONE_ID;
291
292 return config;
293}
294
295function offName(service) {
296 return `${service}-off`;
297}
298
299function write(name, config) {
300 const path = join(out, `${name}.json`);
301 writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
302 return path;
303}
304
305const files = [];
306for (const service of RUNNING) files.push(write(service.name, selfHosted(service)));
307
308const compatibility_date = "2026-09-26";
309files.push(
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.310 write("g1t-gitstore", {
311 name: "g1t-gitstore",
312 main: rel("deploy/self-host/workers/gitstore/index.js"),
Running g1t yourself: the design, a docker compose proof, and a guide to what works today313 compatibility_date,
314 vars: {
315 GITSTORE_URL: process.env.GITSTORE_URL ?? "http://gitstore:8080",
316 GITSTORE_SECRET: process.env.GITSTORE_SECRET ?? "",
317 },
318 }),
319);
320files.push(
321 write("g1t-mail", {
322 name: "g1t-mail",
323 main: rel("deploy/self-host/workers/mail/index.js"),
324 compatibility_date,
325 vars: {
326 PUBLIC_URL,
327 MAIL_URL: process.env.MAIL_URL ?? "",
328 MAIL_FROM: process.env.MAIL_FROM ?? "",
329 },
330 }),
331);
332for (const [service, feature] of Object.entries(OFF)) {
333 files.push(
334 write(offName(service), {
335 name: offName(service),
336 main: rel("deploy/self-host/workers/off/index.js"),
337 compatibility_date,
338 vars: { OFF_NAME: feature },
339 }),
340 );
341}
342
343// The order Wrangler takes them in: the site first, as the one that serves.
344writeFileSync(join(out, "workers.txt"), `${files.map((file) => relative(out, file)).join("\n")}\n`);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas345
Merge branch 'worktree-agent-aaf03bdceac799c89'346// The API (REST, MCP and OAuth) is served on a port of its own (API_PORT),
347// by a second `wrangler dev` (start.sh): not in workers.txt. Its service
348// bindings reach the Workers above through Wrangler's dev registry, as
349// any two `wrangler dev` sessions on one machine do.
350{
351 const api = STACK.find((unit) => unit.worker === "g1t-api");
352 write("g1t-api", selfHosted({ name: api.worker, dir: api.path, web: false }));
353 writeFileSync(join(out, "api.json"), `${JSON.stringify({ api: API_URL, mcp: MCP_URL }, null, 2)}\n`);
354}
355
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member356// What scheduler.mjs runs: each service's own crons, as hosted g1t's Cron
357// Triggers run them, for the services in SELF_HOST_CRONS.
358const schedules = RUNNING.filter((service) => SELF_HOST_CRONS.has(service.name))
359 .map((service) => ({
360 worker: service.name,
361 crons: parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8")).triggers?.crons ?? [],
362 }))
363 .filter((schedule) => schedule.crons.length > 0);
364writeFileSync(join(out, "schedules.json"), `${JSON.stringify(schedules, null, 2)}\n`);
365
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas366// The status page runs in a workerd of its own (status.sh, the `status`
367// service in docker-compose.yml), so it stays up when the site does not:
368// not in workers.txt. It checks the site from inside Compose
369// (STATUS_CHECK_URL) and links to it at PUBLIC_URL. Parts this
370// installation does not run (the API, MCP, docs, g1t.page, the model
371// proxy, billing) are left off its page; a public repository of yours in
372// STATUS_PROBE_REPO adds the git check.
373{
374 const hosted = parseJsonc(readFileSync(join(root, "apps/status/wrangler.jsonc"), "utf8"));
375 const db = hosted.d1_databases[0];
376 write("g1t-status", {
377 name: hosted.name,
378 main: rel(join("apps/status", hosted.main)),
379 compatibility_date: hosted.compatibility_date,
380 rules: hosted.rules,
381 triggers: hosted.triggers,
382 d1_databases: [
383 {
384 binding: db.binding,
385 database_name: db.database_name,
386 database_id: db.database_id,
387 migrations_dir: rel(join("apps/status", db.migrations_dir)),
388 },
389 ],
390 vars: {
391 SITE_URL: (process.env.STATUS_CHECK_URL ?? "http://g1t:8787").replace(/\/$/, ""),
392 PUBLIC_SITE_URL: PUBLIC_URL,
393 API_URL: "",
394 MCP_URL: "",
395 DOCS_URL: "",
396 PAGES_URL: "",
397 MODELS_URL: "",
398 PROBE_REPO: process.env.STATUS_PROBE_REPO ?? "",
399 SUPPORT_URL: `${PUBLIC_URL}/support`,
400 OG_IMAGE: "",
401 // Its own address, for links made outside a request. No email
402 // binding here: subscribing by email is off, the feeds work.
403 STATUS_URL: `http://localhost:${process.env.STATUS_PORT ?? "8788"}`,
404 STATUS_ALERT_EMAIL: "",
405 },
406 });
407}
Running g1t yourself: the design, a docker compose proof, and a guide to what works today408console.log(`Wrote ${files.length} configs to ${out}`);

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