Configuration
The settings Githome's core reads: Worker variables and secrets, bindings, what is fixed on git.pkgforge.dev, the dev environment and rotating MASTER_KEY.
Githome's core is configured in three places: Worker variables in apps/githome/wrangler.jsonc, Worker secrets that the operator sets through the Cloudflare door, and the Cloudflare bindings in the same wrangler.jsonc. This page lists the settings the core reads, their defaults and what they change, and says which values are fixed on git.pkgforge.dev. To see how the deploy uses them, read Deploy and update Githome.
Where configuration lives
| File or place | What it holds | Edit it? |
|---|---|---|
apps/githome/wrangler.jsonc | Worker name, route, variables, bindings, cron, and the dev environment. | Yes, in the private repository. It is the source of truth. |
apps/githome/wrangler.deploy.jsonc | wrangler.jsonc without the dev environment, with a Custom Domain for each host, the real D1 database ID, no container and GITHOME_CI=off. Written by node scripts/deploy.ts render and ignored by git. | No. It is regenerated by each deploy. |
| Worker secrets | MASTER_KEY and OIDC_PRIVATE_KEY, which the deploy generates once, and the secrets that only the operator holds (mail, mirrors, backups, Turnstile). | Only to restore them or to add an optional one. Never change MASTER_KEY without the rotation below. |
apps/githome/.dev.vars | Local secrets for wrangler dev, such as MASTER_KEY. Ignored by git. | Yes, for local development only. |
The settings of the other Workers are separate: the dash has its own wrangler.jsonc in apps/dash, and each module its own.
Worker variables
Set under vars in wrangler.jsonc. All values are strings. The last column says what the deployment of git.pkgforge.dev has.
| Variable | Required | What it does | On git.pkgforge.dev |
|---|---|---|---|
PUBLIC_URL | Yes | The canonical origin of the instance. See PUBLIC_URL. | https://git.pkgforge.dev |
MAIL_FROM | No | Sender address of every email. The display name is "Githome", or <actor> (Githome) for emails about someone's action. When empty, the core sends from notifications@<hostname of PUBLIC_URL>. The domain must be onboarded to Email Sending, or, with SMTP_URL, be one your SMTP server lets you send from. | notifications@git.pkgforge.dev |
SMTP_URL | No | The SMTP server email goes through when the Worker has no EMAIL binding: smtps://<user>@<host>:465 or smtp://<user>@<host>:587. The password goes in the SMTP_PASSWORD secret. See SMTP. | Not set |
SIGNUP | No | open lets anyone create an account at /signup. See SIGNUP. Any other value, or none, keeps the instance invite-only. | Not set: open sign-up is off |
GITHOME_CI | No | The switch of the forge CI. off turns it off: each run, job, sandbox and terminal is refused with CI is off on this deployment (403 at the REST doors), and no Container starts. on turns it on, with the execution backends of GITHOME_CI_BACKENDS. Any other value counts as off, so a typo never turns the CI on. Not set, the CI runs with each backend, as in the dev environment and the tests. | off |
GITHOME_CI_BACKENDS | With GITHOME_CI=on | The execution backends that the deployment turns on, separated by commas or spaces: pool runs the jobs of registered runners (runs-on: [self-hosted, ...]), and containers runs hosted jobs and agent sandboxes in Cloudflare Containers. A job or sandbox whose backend is off fails with the execution backend <name> is off on this deployment (GITHOME_CI_BACKENDS). | Not set |
GITHOME_REBUILD | No | on makes a deployment a rebuild target: its cron runs only the jobs of a rebuild. Only node scripts/deploy.ts --target sets it, on a deployment of its own. | Not set |
TURNSTILE_SITE_KEY | With SIGNUP=open; for anonymous security reports and contact messages | The site key of the Cloudflare Turnstile widget on the sign-up page, the security report form and the contact form. See Turnstile. Setting it doesn't open sign-up. | Not set: until the operator sets it and TURNSTILE_SECRET, only people who are signed in can use the contact and security report forms |
ARTIFACT_QUOTA_BYTES | No | Workflow artifact storage per repository, in bytes. Default 10737418240 (10 GB). Anything that isn't a positive whole number counts as not set. | Not set |
QUOTA_HOSTED_MINUTES, QUOTA_HOSTED_JOBS, QUOTA_SANDBOXES, QUOTA_REPOSITORIES, QUOTA_STORAGE_BYTES | No | The default per-account quotas: hosted minutes per month (default 3000), hosted Actions jobs at once (20), agent sandboxes open at once (5), repositories (500) and storage in bytes (10737418240, 10 GB). -1 is unlimited; text that isn't a whole number counts as not set. The superadmin changes them for one account in the dash. See Limits. | Not set |
IMPORT_MAX_REPO_KB | No | The largest GitHub repository a one-repository import accepts, in KB as GitHub reports a repository's size. Default 10485760 (10 GB). A larger source is refused before the import starts. See GitHub imports. | Not set |
IMPORT_GHE_HOST | No | The host name of a GitHub Enterprise Server (like ghe.example.com) that imports may download release assets from. Without it, imports download release assets only from github.com and its asset hosts. | Not set |
GITHUB_OAUTH_CLIENT_ID | No | The client ID of a GitHub OAuth app, for linking GitHub accounts with a GitHub sign-in instead of a gist. Needs the secret GITHUB_OAUTH_CLIENT_SECRET too. | Not set |
SCHEDULE_TICK_MINUTES | No | Minutes between the workflow scheduler's checks, a whole number from 1 to 60. Default 5. Anything else counts as not set. | Not set |
EGRESS | No | web limits hosted Actions jobs and agent sandboxes to HTTP and HTTPS and DNS. See EGRESS. | Not set |
SECURITY_CONTACT | No | More places people can report a vulnerability, listed in /.well-known/security.txt after the instance's own report form: an email address or an https:// URL, several separated by commas. See Security contact. | Not set |
MCP_REGISTRY_AUTH | No | The proof record served at /.well-known/mcp-registry-auth. Not set, that path is a 404. | Not set |
INDEXNOW | No | off stops telling search engines about new, changed and removed public pages through IndexNow. Not set, it is on whenever PUBLIC_URL is a public https origin and MASTER_KEY is set. | Not set |
TERMINALS | No | off switches terminals off. Anything else, or not set, leaves them on (with GITHOME_CI=off they are refused anyway). | Not set |
RATE_LIMITS | Where the ratelimits bindings don't exist | durable counts requests in the RATE_LIMITER Durable Object for each rate limit whose binding is missing, with the same limits. Leave it unset on Cloudflare and in local development. See Rate limits. | Not set |
DEV_AUTH | Never in production | Local development only. See DEV_AUTH. | Not set |
IMPORT_GIT_AGENT | Never in production | With external, an import's Git transfer waits for an import agent started by hand, as in the dev environment. | Not set |
wrangler.jsonc also carries BOOTSTRAP_ADMINS, ACCESS_TEAM_DOMAIN and ACCESS_AUD, all empty. BOOTSTRAP_ADMINS is empty on git.pkgforge.dev, so no account becomes an administrator at its first sign-in, and the instance has no Cloudflare Access sign-in: the superadmin works in the dash (https://dash.git.pkgforge.dev), with its own passkey sign-in, and there is no "Make admin" action in the core. With ACCESS_TEAM_DOMAIN or ACCESS_AUD empty, /auth/access answers "Account recovery by email is not set up on this instance. Ask an administrator for a sign-in link." A person who lost their passkeys asks the superadmin for a sign-in link.
PUBLIC_URL
PUBLIC_URL must be the exact origin people and tools use, with https:// and no path. Githome uses it for:
- Passkeys. The WebAuthn relying party ID is the hostname of
PUBLIC_URL, and the expected origin is its origin. Passkeys only work on that hostname. - Every generated link: emails, invitation and sign-in links,
html_urlandclone_urlin API responses, webhook payloads, the sitemap and canonical tags. - Workflows:
github.server_urlisPUBLIC_URLandgithub.api_urlisPUBLIC_URL/api/v3. - OIDC: the issuer (
iss) of workflow ID tokens, and their default audience. - The container registry host name, the hostname of
PUBLIC_URL(images are<hostname>/<owner>/<name>). - The cross-site check: a cookie-authenticated write with an
Originother than this origin is refused.
Important: Changing
PUBLIC_URLon a running instance breaks every existing passkey (they are bound to the old hostname), changes the OIDC issuer that cloud providers trust, and changes every image name.
SIGNUP
With SIGNUP=open, the sign-in page links to Create an account (/signup). Someone enters their email address, gets a 6-digit code by email, enters it, picks a username and creates a passkey; their account is active at once. Unset, or any other value, the instance is invite-only: /signup answers 404 (or, where Turnstile is set, leads to /early-access) and the sign-in page says so. The code exists in Githome, and the deployment of git.pkgforge.dev keeps it off: accounts are invited by the superadmin, who sends the one-time invitation link by hand.
Open sign-up needs Cloudflare Turnstile, so scripts can't use it to send email. Without both Turnstile keys, /signup answers 503 with "Sign-up needs Turnstile configured", except when PUBLIC_URL is on the local machine. The limits on codes are in Limits.
EGRESS
EGRESS=web limits hosted job and sandbox containers to ports 80 and 443 and DNS, and forwards HTTP and HTTPS through the Worker. It applies only where hosted compute runs. On git.pkgforge.dev the forge CI is off and no Container starts, so the setting does nothing there, and it is not set.
Security contact
/.well-known/security.txt (RFC 9116) tells security researchers where to report a problem. Githome always serves it, with:
| Field | Value |
|---|---|
Contact | First <PUBLIC_URL>/security/report, the instance's private report form, then one line per entry of SECURITY_CONTACT. An email address becomes a mailto: URI; https://, mailto: and tel: URIs are kept as they are; anything else is left out. |
Expires | Midnight UTC 360 days after the request, so the file never goes stale. |
Policy | <PUBLIC_URL>/docs/security/disclosure, the disclosure policy. |
Preferred-Languages | en |
Canonical | <PUBLIC_URL>/.well-known/security.txt |
The report form needs no setup: reports go to the superadmin, who reads them in the dash. People who are signed in can always send one. People who aren't pass Cloudflare Turnstile first, with TURNSTILE_SITE_KEY and TURNSTILE_SECRET. Without both keys, the form asks visitors to sign in, except on the local machine. SECURITY_CONTACT is optional; use an address someone reads.
Turnstile
The security report and contact forms, and open sign-up when it's on, check with Cloudflare Turnstile that a visitor without an account is a person. The site key is the variable TURNSTILE_SITE_KEY, and the secret key is the Worker secret TURNSTILE_SECRET. The deploy does not create the widget: the operator makes it and gives the deploy the site key and the secret, and the secret is one that the deploy never generates. On git.pkgforge.dev neither is set yet, so people who aren't signed in can't send security reports or contact messages; a visitor without an account uses the public tracker.
SMTP
Without an EMAIL binding, the core can send email through any SMTP server that speaks TLS. When the EMAIL binding is there, it is used and SMTP_URL is ignored.
- Set
SMTP_URLundervarsinwrangler.jsonc.smtps://<user>@<host>:465uses TLS from the start. The port defaults to 465.smtp://<user>@<host>:587starts plain and upgrades with STARTTLS. If the server doesn't offer STARTTLS, the core sends nothing (E_SMTP_REJECTED), so the password never crosses the network in the clear.- Write an
@in the user as%40. Leave the user out for a relay that needs no sign-in. - Cloudflare Workers can't open connections to port 25, so use 465 or 587.
- Store the password as the secret
SMTP_PASSWORD. Never put it inSMTP_URL: the core refuses such a URL. - Make sure
MAIL_FROMis an address the SMTP user may send from. It is both theFromheader and the envelope sender.
The core signs in with AUTH PLAIN, or AUTH LOGIN when the server offers only that. Each email is one connection, and each reply has 30 seconds to arrive. A 2xx answer is sent; a 4xx, a timeout or a dropped connection is E_SMTP_TRANSIENT and the queue tries again later, up to 6 times; a 5xx to RCPT TO that says the address doesn't exist is failed with E_SMTP_RECIPIENT_REJECTED and the address goes on the suppression list; any other 5xx is failed with E_SMTP_REJECTED. The core never logs the password or the content of an email.
Notification email depends on the email sending of the deployment: the core has the EMAIL binding, and delivery is not promised. No invitation is ever sent by email.
DEV_AUTH
When DEV_AUTH is set, ACCESS_AUD is empty and both PUBLIC_URL and the request's own address are on the local machine (localhost, 127.x.x.x or [::1]), every request without credentials acts as the account with the email in DEV_AUTH, with full access.
Important: Never set
DEV_AUTHon a deployed instance. As a safeguard it does nothing there: a request that reaches the Worker under any other host name, or aPUBLIC_URLthat isn't on the local machine, turns it off.
Worker secrets
Set with wrangler secret put through the Cloudflare door (the deploy sets the generated ones for you), never in wrangler.jsonc. A secret value is never printed or put in an argument.
| Secret | Format | Set by | What it does | If it is missing |
|---|---|---|---|---|
MASTER_KEY | 32 random bytes, base64 | The deploy, once | Root key for envelope encryption of stored secrets (repository, organization and environment secrets, webhook secrets, model provider keys, the GitHub token of a running import). It also signs the unsubscribe links in emails, the proof in invitation links, the codes and OAuth state for linking GitHub accounts, and the tokens and URLs of the Actions cache and artifact services. | Secrets can't be stored or read (could not store the secret: ... (is MASTER_KEY configured?)). Webhooks can't be created or delivered. Imports that come with a GitHub token can't be started. |
MASTER_KEY_PREV | The MASTER_KEY you replaced | The operator, only while rotating MASTER_KEY | Everything sealed or signed with the old key keeps working. Nothing new uses it. | The normal state. |
OIDC_PRIVATE_KEY | An ECDSA P-256 private key as a JWK (JSON), with a kid | The deploy, once | Signs the ID tokens that workflow jobs request with permissions: id-token: write (ES256). Its public half is served at /.well-known/jwks. | /.well-known/jwks returns no keys, and GET /api/v3/meta returns "oidc": false. |
TURNSTILE_SECRET | The secret key of a Cloudflare Turnstile widget | The operator | With TURNSTILE_SITE_KEY, checks the Turnstile widget. See Turnstile. | The security report and contact forms take input only from people who are signed in. |
SMTP_PASSWORD | The password of the user in SMTP_URL | The operator, optionally | Signs in to the SMTP server. See SMTP. | With a user in SMTP_URL, nothing is sent (E_SMTP_CONFIG). |
MAIL_EVENTS_SECRET | A random string, at least 32 characters | The operator, optionally | Turns on POST /-/email/events, the signed intake for bounces and complaints from anything other than Cloudflare's own event subscriptions. | The endpoint answers 404. |
GITHUB_OAUTH_CLIENT_SECRET | The client secret of a GitHub OAuth app | The operator, optionally | With GITHUB_OAUTH_CLIENT_ID, turns on Link with GitHub on Settings → Linked accounts. | People link GitHub accounts with the gist code only. |
GITHUB_MIRROR_TOKEN, GITHUB_MIRROR_ORG | A token (or the JSON of an App installation) and the organization of the push mirrors on GitHub | The operator | Each repository gets a push mirror to that organization. | The cron makes no new mirror; existing mirrors keep their sealed credential. |
GITHUB_MIRROR_HOOK_SECRET | A random string | The operator | Verifies the pushes that GitHub sends to the webhook of the pull mirrors. | The webhook answers 404. |
BACKUP_KEY, BACKUP_S3_ENDPOINT, BACKUP_S3_BUCKET, BACKUP_S3_ACCESS_KEY_ID, BACKUP_S3_SECRET_ACCESS_KEY | A 32-byte key in base64, and the endpoint, bucket and credentials of an S3-compatible provider | The operator | A backup runs each day. BACKUP_KEY seals each object of a backup and opens nothing else. | With one unset, no backup runs. |
None of these secrets is rotated automatically, and the deploy never replaces one that exists. Keep a copy of MASTER_KEY somewhere safe: replacing it without keeping the old one as MASTER_KEY_PREV makes every stored secret undecryptable. To rotate the OIDC key, delete OIDC_PRIVATE_KEY and deploy: a new key with a new kid is generated, and relying parties pick it up from /.well-known/jwks.
Rotating MASTER_KEY
Rotate MASTER_KEY when it may have leaked, or on your own schedule. Githome accepts two keys while a rotation is under way: MASTER_KEY, which encrypts and signs everything new, and MASTER_KEY_PREV, the key it replaced, which is only used to decrypt and verify what was made before.
Make a new key and keep it with your copy of the current one:
NEW_KEY="$(openssl rand -base64 32)".Set the current key as
MASTER_KEY_PREVand the new one asMASTER_KEYin one step, so no version of the Worker runs with the new key alone.OLD_KEYis the current value from your safe copy (Cloudflare doesn't show a secret's value):jq -n --arg old "$OLD_KEY" --arg new "$NEW_KEY" '{MASTER_KEY_PREV: $old, MASTER_KEY: $new}' | wrangler secret bulk -c wrangler.deploy.jsoncRun
wranglerthrough the Cloudflare door. Withwrangler secret putinstead, putMASTER_KEY_PREVfirst andMASTER_KEYsecond.Save the new key where you kept the old one. Nothing else needs to change: no deploy, migration or restart.
From then on, new and replaced secrets are encrypted with the new key, each stored value names the key it was encrypted with, and stored values move to the new key by themselves as they are used. Unsubscribe links, invitation links, codes, runtime tokens and cache and artifact URLs signed with the old key keep working; new ones are signed with the new key.
Remove MASTER_KEY_PREV only when nothing you still need depends on the old key: every secret you still need has been used or saved again since the rotation, and links already sent have expired or may stop working. If the old key leaked, treat the secrets as exposed: replace their values at the source and save the new values, then remove MASTER_KEY_PREV right away.
Bindings
Defined in wrangler.jsonc. node scripts/deploy.ts apply creates the underlying resources that are missing.
| Binding | Kind | Resource (production) | What Githome uses it for |
|---|---|---|---|
DB | D1 database | githome (migrations in migrations/) | Accounts, organizations, tokens, repository list, topics and access, invitations, sessions and passkeys, notifications, audit log, schedules, organization secrets and variables, the registry index, email settings and log, GitHub imports and linked GitHub accounts. |
BUCKET | R2 bucket | githome-data | Large git objects, LFS objects, archived job logs, caches, artifacts, container image layers and manifests, the event archive, release assets, cached source archives and profile pictures. |
EVENTS | Queue producer, with a consumer on the same Worker | githome-events, dead-letter queue githome-events-dlq | Repository events for webhooks, notifications and the audit log. |
MAIL | Queue producer, with a consumer on the same Worker | githome-mail, dead-letter queue githome-mail-dlq | Email jobs. Optional: without it emails are dropped with a warning in the logs. |
EMAIL | send_email (Email Sending) | Your onboarded domain | Sending email. Optional: without it email goes to the SMTP server in SMTP_URL, and without that too every email is logged as skipped. |
| (none) | Queue consumer | githome-mail-events | Bounces and complaints from Email Sending's event subscriptions. |
RL_AUTH, RL_ANON, RL_ACCOUNT, RL_CREATE, RL_MODEL | Rate limiting (ratelimits) | Namespaces 1001 to 1005; 20, 300, 2,000, 60 and 600 requests per 60 seconds | Request rate limits. Optional: a missing binding lets requests through. |
REPO | Durable Object namespace | Class RepoDO (SQLite) | One object per repository. |
SCHEDULER | Durable Object namespace | Class SchedulerDO (SQLite) | One object that starts scheduled workflow runs on its own alarm. |
COMPOSE | Durable Object namespace | Class ComposeDO (SQLite) | Joins the blocks of large artifact and cache uploads. |
RATE_LIMITER | Durable Object namespace | Class RateLimiterDO (SQLite) | Request rate limits when RATE_LIMITS is durable and the ratelimits bindings are missing. Unused otherwise. |
RUN_WORKFLOW | Workflow | githome-run, class RunWorkflow | One durable instance per workflow run. Starts none while the forge CI is off. |
IMPORT_WORKFLOW | Workflow | githome-import, class ImportWorkflow | One durable instance per attempt of a GitHub import. |
ACCOUNT_WORKFLOW | Workflow | githome-account, class AccountWorkflow | Account exports and the clean-up after an account is deleted. |
The code also declares the container classes (RunnerContainer and its smaller sizes, SandboxContainer and ImportContainer) with their Durable Object bindings, because the Actions code stays in githome. The deployed config that render writes has "containers": [], so no Container is built or started on git.pkgforge.dev. The Durable Object classes are declared in eight migrations, tags v1 to v8, which wrangler deploy applies when the account doesn't have them yet.
Other Worker settings
| Setting | Value | Notes |
|---|---|---|
name | githome | The Worker's name in the account. |
main | src/index.ts | |
compatibility_date | 2026-10-01 | |
compatibility_flags | nodejs_compat | |
workers_dev | false | The instance is only served on its hosts. |
routes | git.pkgforge.dev/* | In the deployed config, render replaces the route with a Custom Domain for git.pkgforge.dev and uploads.git.pkgforge.dev. |
observability | Enabled, head_sampling_rate: 1 | Every request is logged to Workers Logs. |
upload_source_maps | true | Readable stack traces in the logs. |
limits.cpu_ms | 300000 | Up to 5 minutes of CPU time per request (large pushes, diffs and merges). |
triggers.crons | ["*/5 * * * *"] | Token expiry reminders, email log pruning, registry cleanup, the mirror and backup jobs, and setting the workflow scheduler's alarm if it isn't set. |
The dev environment
wrangler.jsonc has an env.dev section that pnpm dev (wrangler dev -e dev) and the tests use. It is removed from the deployed config.
| Setting | dev value |
|---|---|
routes, workers_dev | No routes; workers_dev: true |
PUBLIC_URL | http://localhost:8787 |
BOOTSTRAP_ADMINS, DEV_AUTH | dev@githome.local |
DB, BUCKET | githome-dev, githome-data-dev |
| Queues | githome-events-dev and githome-mail-dev, batches waiting up to 1 second, no dead-letter queues |
EMAIL | Captured locally by Miniflare, not delivered |
IMPORT_GIT_AGENT | external |
GITHOME_CI | Not set, so the forge CI is not switched off |
triggers.crons | None |
MASTER_KEY is not set in dev. Add it to apps/githome/.dev.vars (MASTER_KEY=<base64 of 32 random bytes>) to store secrets locally.
GitHub imports
Imports from GitHub need no setup beyond a normal deploy: the IMPORT_WORKFLOW Workflow and the D1 tables of the imports are created by the deploy, and imports from GitLab, Forgejo, Gitea and Codeberg run without a container. An import from GitHub needs the import container (the IMPORTER binding), which the dormant CI keeps off, so on git.pkgforge.dev it stops at its Git step with "no import container is configured". What you can tune:
| Setting | Where | Effect |
|---|---|---|
IMPORT_MAX_REPO_KB | vars in wrangler.jsonc | The size ceiling for one-repository imports (default 10 GB). |
IMPORT_GHE_HOST | vars in wrangler.jsonc | Lets imports download release assets from a GitHub Enterprise Server host. |
GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET | vars and a Worker secret | Adds Link with GitHub to Settings → Linked accounts. |
To offer linking through GitHub sign-in, create an OAuth app on github.com with the authorization callback URL https://git.pkgforge.dev/settings/linked/github/callback, put the client ID under vars and store the secret as GITHUB_OAUTH_CLIENT_SECRET. The OAuth flow asks GitHub for the read:user scope only and binds its state to the signed-in account. Without the two settings, people link accounts with the gist code, which needs no configuration.
Imports reach GitHub only on HTTPS hosts with public names; a GitHub API URL on a private address, localhost, .local or .internal is refused with 422 github_api_url must be a public host.
Deploy script flags
The deploy is node scripts/deploy.ts render | plan | apply (see Deploy and update Githome). It reads no environment variable for the hostname, the zone or the administrators: the hosts are in deploy/resources.jsonc, and the account id and the token come from the Cloudflare door.
| Flag | Effect |
|---|---|
--worker NAME | Limits the command to one Worker of the manifest. |
--target NAME | A deployment of its own beside the live one: each resource is named githome-NAME-..., the Worker serves its workers.dev host and carries GITHOME_REBUILD=on. NAME is 2 to 16 lower-case letters and digits. |
--file NAME.jsonc | With render: writes another file in the Worker's folder, so a dry run never overwrites a config that apply rendered. |
scripts/setup.sh takes --dry-run, --apply and --target NAME; with no flag it plans.
Settings that are not configuration
Some values are fixed in code and can't be changed without editing it, for example the job timeout ceiling, matrix size, the 10 GB cache budget per repository and session lifetime. They are listed in Limits.