# Deploy and update Githome

> How Githome is deployed to Cloudflare from its private repository with scripts/deploy.ts and scripts/setup.sh, how it updates, and local development.

Source: https://git.pkgforge.dev/docs/self-hosting/deploy

Githome runs on Cloudflare, with copies kept off Cloudflare, and the operator deploys it from its private repository. Nobody else deploys a copy: this page describes how the instance at `git.pkgforge.dev` is deployed, so that you can reason about what a deploy creates, changes and never touches. The commands run from a checkout of the repository; the repository is private and has no public clone URL.

There are two entry points, and both run the same script:

| Way | What it does |
| --- | --- |
| `node scripts/deploy.ts render \| plan \| apply` | The deployment itself. `render` writes the config, `plan` reads the account, `apply` creates and deploys. |
| `scripts/setup.sh` | Checks the toolchain, installs the workspace and starts `scripts/deploy.ts`. See [Deploy from your own machine](#deploy-from-your-own-machine). |

For what each Cloudflare product does in Githome, see [Architecture](https://git.pkgforge.dev/docs/self-hosting/architecture). For every setting, see [Configuration](https://git.pkgforge.dev/docs/self-hosting/configuration).

## What you need

| Requirement | Why |
| --- | --- |
| A Cloudflare account on the Workers Paid plan, with R2 turned on | Githome uses Workers, Durable Objects with SQLite, D1, R2, Queues and Workflows. The Worker asks for 300,000 ms of CPU time per request. |
| The zone `pkgforge.dev`, with the hosts of the instance | `git.pkgforge.dev`, `uploads.git.pkgforge.dev`, `dash.git.pkgforge.dev` and, once a module is deployed, its `NAME.git.pkgforge.dev` are Custom Domains of their Workers. |
| Node.js 26.7.0 and pnpm 12.11.2 | The Node of `.node-version` and the pnpm of `packageManager` in `package.json`. `scripts/setup.sh` refuses any other version. |
| A minter credential for Cloudflare | A file kept outside git (`.env/CF_TOKEN_MINTER.txt`). The deploy never uses a login or a token pasted into a command. See [The Cloudflare door](#the-cloudflare-door). |

Nobody who uses the instance needs a Cloudflare account. Only the deployment uses one.

## The resources

One file of the repository, `deploy/resources.jsonc`, lists the Cloudflare resources of each Worker, and `wrangler.jsonc` of each Worker holds the bindings; a test keeps the two in agreement. Each resource name starts with `githome-`, except the core Worker and its D1 database, which are named `githome`.

| Worker | Hosts | Resources |
| --- | --- | --- |
| `githome` (the core, `apps/githome`) | `git.pkgforge.dev`, `uploads.git.pkgforge.dev` | D1 `githome`; R2 bucket `githome-data`; queues `githome-events`, `githome-events-dlq`, `githome-mail`, `githome-mail-dlq` and `githome-mail-events`; Workflows `githome-run`, `githome-import` and `githome-account`; the Durable Object migrations `v1` to `v8`; a cron every 5 minutes |
| `githome-dash` (the dash, `apps/dash`) | `dash.git.pkgforge.dev` | D1 `githome-dash`, and a service binding to the core |
| each module (`modules/`) | `NAME.git.pkgforge.dev` | Its own Worker |

The deploy keeps a fixed list of secrets for each Worker. It generates `MASTER_KEY` and `OIDC_PRIVATE_KEY` the first time and never rotates them. The others come from the operator and are never generated: the mail, mirror and backup credentials, the Turnstile secret and, for the dash, the token that registers the first passkey. See [Worker secrets](https://git.pkgforge.dev/docs/self-hosting/configuration#worker-secrets).

## The Cloudflare door

The deploy reaches Cloudflare through one door, `scripts/cf-token.ts` with `scripts/lib/cf-auth.ts`. The operator keeps a minter credential in a file outside git. For each run the door mints a child token that holds only the permission groups of the run's purpose (the read-only `wrangler-read` for `plan`), hands it to `wrangler` from `node_modules` as `CLOUDFLARE_API_TOKEN` in that process only, and revokes it when the process ends, also on Ctrl-C. A token value is never printed, logged, put in an argument or written to a file, and the door refuses a command that deletes a resource. The account id comes from the door, never from the tree.

`node scripts/cf-token.ts verify` checks that the minter is active, and `node scripts/cf-token.ts check` mints a child token, makes two reads with it and revokes it.

## How a deployment works

1. **`render`** is local and makes no network call. For each Worker of the manifest it writes `wrangler.deploy.jsonc` in the Worker's folder (a generated file that git ignores; don't edit it) from `wrangler.jsonc`: no `env` section, a Custom Domain for each host, the D1 names of the manifest, no container, and the variable `GITHOME_CI=off`, so the forge CI stays dormant. Each Durable Object binding and migration stays.
2. **`plan`** is read only. It lists each resource of the manifest as present, missing or different: the Worker, each D1 database, R2 bucket, queue and Workflow, each Custom Domain, the crons and each secret that `apply` generates. It exits `3` on a missing or a different resource.
3. **`apply`** creates each missing D1 database, R2 bucket and queue, renders the config with the real D1 ids, applies the D1 migrations, deploys with `wrangler`, sets each missing generated secret from a value made in the process (never printed), and plans again. It stops on a resource that it cannot read or that differs, and it sets no secret when a read after the deploy fails, so a live key is never replaced. It never deletes or replaces a resource. `MASTER_KEY` is read from the operator's own copy when that copy exists; otherwise it is generated and written there before it is set, so a copy exists for a rebuild.

`--worker NAME` limits a command to one Worker. `--target NAME` makes a deployment of its own beside the live one, for a rebuild or a drill: each resource takes a name under `githome-NAME-`, the Worker has no Custom Domain and serves its `workers.dev` host, and it runs only the jobs of a rebuild.

Exit codes: `0` done, `1` a Cloudflare call or a step failed, `2` it could not run (bad arguments, no manifest, the door refused), `3` a resource is missing or differs.

## Deploy from your own machine

`scripts/setup.sh` runs from a checkout of the repository, for the first deploy and for each update. It checks `node`, `pnpm` and `git`, refuses a Node other than the one in `.node-version` or a pnpm other than the one in `packageManager`, runs `pnpm install --frozen-lockfile`, and starts the deploy script. It takes the Cloudflare token from the door, never from an argument or a prompt.

```sh
scripts/setup.sh                # plan: what the account lacks (read only)
scripts/setup.sh --dry-run      # build and dry-run each Worker: no Cloudflare call
scripts/setup.sh --apply        # deploy
scripts/setup.sh --target NAME  # with the plan or --apply: a deployment of its own
```

`pnpm setup:cloudflare` runs the same script. Every step creates only what is missing, so run it again with `--apply` to update; with no flag it plans and changes nothing.

## The first sign-in

Githome has no passwords, no Cloudflare Access sign-in and no open sign-up. The dash registers its first passkey with a bootstrap token that the operator sets as the Worker secret `DASH_BOOTSTRAP_TOKEN` (at least 32 characters). The dash records the one use of the token and refuses it afterwards. From then on the superadmin signs in on `https://dash.git.pkgforge.dev` with that passkey, invites each account and sends the one-time invitation link by hand. Githome sends no email for an invitation. A person who lost their passkeys asks the superadmin for a sign-in link.

## DNS

Each host is a Custom Domain of its Worker, which Cloudflare creates with the DNS record and the certificate. A new host can take a while to get its certificate. When `wrangler` is run without a terminal, it replaces an existing DNS record of a host that becomes a Custom Domain.

## Updating

Update by running `scripts/setup.sh --apply` (or `node scripts/deploy.ts apply`) again from the updated checkout. Each run:

- Creates nothing that already exists and keeps the D1 data, the R2 data and the secrets.
- Applies new D1 migrations before the new Worker goes live. Versions only move forward: D1 migrations can't be undone, so don't deploy an older version over a newer one.

Keep `MASTER_KEY` safe and don't simply replace it: it decrypts every stored secret, and links already sent are signed with it. To change it, keep the old key as `MASTER_KEY_PREV` as described in [Rotating MASTER_KEY](https://git.pkgforge.dev/docs/self-hosting/configuration#rotating-master_key). If `OIDC_PRIVATE_KEY` is deleted, the next deploy generates a new key with a new key ID; systems that trust the OIDC tokens fetch the new public key from `/.well-known/jwks`.

## Mirrors and backups

Push mirrors copy each repository, with its forge state in `refs/githome/meta`, to GitHub, Cloudflare Artifacts, GitLab, Forgejo or Codeberg. Backups start once their bucket and credentials are set (they are not set yet, so no backup runs); then they run each day, are sealed with their own key (`BACKUP_KEY`, used for nothing else) and go to an S3-compatible bucket off Cloudflare. A deployment made with `--target` is the place where a rebuild or a drill runs, beside the live one.

## Local development

You can run the core on your machine with `wrangler dev`, which simulates D1, R2, Queues, Durable Objects and Workflows locally.

```sh
pnpm install
pnpm -r build
pnpm --filter @githome/app db:migrate:local
pnpm dev
```

`pnpm dev` runs `wrangler dev -e dev` in `apps/githome` and serves `http://localhost:8787`. The `dev` environment differs from the deployment:

| | Deployment | `dev` environment |
| --- | --- | --- |
| `PUBLIC_URL` | `https://git.pkgforge.dev` | `http://localhost:8787` |
| Sign-in | Passkeys and tokens | Any request without credentials acts as `dev@githome.local` (`DEV_AUTH`). Tokens still work. |
| D1, R2, queues, Workflows | `githome`, `githome-data`, `githome-events`, `githome-mail`, `githome-run`, `githome-import` | The same names with `-dev`, all local |
| Containers | None | None |
| Forge CI | Off | Not switched off, as in the tests |
| Email | Sent when email sending is set up | Captured by Miniflare, not delivered |
| `MASTER_KEY` | Set by the deploy | Not set: storing secrets fails until you add one |

To store secrets locally, put a key in `apps/githome/.dev.vars` (ignored by git): `MASTER_KEY=<base64 of 32 random bytes>`. The dash has its own `dev` environment (`wrangler dev -e dev --port 8788`).

## Troubleshooting

| Message or exit | Where | What to do |
| --- | --- | --- |
| `Node X is needed (.node-version); this is Node Y` or the same for pnpm | `setup.sh` | Install the version that the file or `packageManager` names and run it again. |
| Exit `2`, the door refused | `deploy.ts` | The minter credential is missing or not active. Run `node scripts/cf-token.ts verify`. |
| Exit `3` after `plan` | `deploy.ts` | A resource of the manifest is missing or differs. Read the list; `apply` creates what is missing and stops on a difference. |
| `apply` stops on a resource that differs | `deploy.ts` | `apply` never replaces or deletes a resource. Settle the difference by hand, then run it again. |
| Exit `1` after the Worker is deployed and the read-back fails | `deploy.ts` | No secret was set, so a live key was not replaced. Run `plan`, then `apply` again. |
| A host answers with a certificate error | Browser | A new Custom Domain can take a while to get its certificate. |
