Skip to content

Self-host the App

Self-hosting means operating the Community App under your own GitHub App identity and infrastructure. You own registration, repository scope, Cloudflare resources, credentials, upgrades, incidents, retained data and the information you give your users. Community supplies the basic Free capability set without the official hosted service’s repository-count, import or retention quotas; your resources and selected limits govern it. It needs no hosted subscription.

Rich historical file exploration and Business coordination come from separately maintained private modules. A public checkout or Community release does not contain that premium implementation. Commercial self-hosting is a separately licensed contact route; Plans and shared access explains the distinction. The engine and ordinary policy remain the same.

The supported adapter is Cloudflare Workers, Queues, and D1. Application meaning is kept outside those provider bindings, but that does not make an unimplemented Docker image or alternate provider a supported deployment. For a service someone else operates, use Use the managed service.

Use an immutable source revision whose application changes you have reviewed, Node.js 22 or later, npm, Git, and the declared dependencies. Building the npm CLI alone does not install the App application tree. Keep local configuration, credential files, database exports, and provider receipts outside tracked source and access-controlled; the repository’s ignored artifacts/ directory is convenient for non-secret build evidence, not a secure secret store.

The App server and signed-in application share one independent App release family. Current App metadata has no named version selected, so qualification currently pins source rather than an available app-vX.Y.Z release. When a release is published, verify its actual attached App manifest and source identity. Neither an npm version nor a database migration number identifies your deployed App. Releases owns that update boundary.

From the repository root:

Terminal window
npm ci --ignore-scripts
npm run build
npm run check:github-app

The final command performs the maintained Worker dry-run and default-policy qualification. It does not create a Worker, queue, database, App, credential, installation, or deployment. A passing bundle is necessary but does not establish real permissions, delivery, retention, or useful PR effects. The operator source and Development command register own the exact supported commands.

Run the ordinary application tests before selecting an upgrade. They exercise signature admission, real application use cases against isolated boundaries, D1 state transitions, retries, and credential-helper failure paths without enrolling a repository. They are not evidence that your production account has those resources.

The signed-in Community App lives under apps/app/. Its Worker includes the managed runtime routes on the same host and bindings. The standalone webhook Worker under apps/github-app/ remains buildable; selecting which unit serves your host is a deployment choice, not a second independently versioned App.

For the joined Community unit, also run the maintained npm run check:app route and follow the App build and deployment instructions. Use that unit’s complete assets, routing, OAuth/session configuration and migrations. A working standalone webhook canary does not establish the joined sign-in, history or Manage experience. Premium composition has its own matching source and qualification requirements; connecting billing alone does not install premium modules.

You need authority over the account or organization that will own the registration, an actual HTTPS webhook destination, and the ability to review the installation’s repository access. Keep the App registration, a repository installation, and execution enablement distinct.

For a new registration, start with the native manifest registration and capture guide. The local helper prepares GitHub’s personal or organisation form, captures its response encrypted, and hands the validated inputs to the installer. Select your own namespace, visibility, callbacks and deployment; the guide’s staging values are specimens. Registration, protected installation and deployment remain separate operator actions.

For an existing registration or file-based recovery, the supported file-input installer below remains available. This complete, read-only specimen prints registration inputs and a provider registration link:

Terminal window
bash -p apps/github-app/operator-launch.sh operator registration --webhook-url https://example.invalid/webhooks/github

example.invalid is deliberately non-operational. Replace it with your selected webhook destination when preparing a real registration; running this specimen registers nothing. The helper selects Contents read, Pull requests write, Checks write, and the PR/check events. GitHub supplies installation lifecycle events. Review the resulting App identity and scope rather than assuming the example selected your organization.

Keep the numeric App ID, its downloaded RSA private-key file, and webhook secret as different inputs. The private key authenticates the App; installation tokens are minted for bounded execution; the webhook secret verifies incoming bytes. Hosted adopters do not receive these operator credentials. Do not put any secret value in a command argument, repository file, public issue, screenshot, or documentation example.

The maintained Wrangler configuration is the adapter’s binding contract, not a provisioned production environment.

Resource Required consequence for the operator
Worker Serve signed webhook admission, health, queue consumption, and scheduled maintenance using the shared built engine
APP_QUEUE and its consumer Durably retain admitted minimal envelopes, retry bounded work, and route exhausted deliveries to the selected dead-letter queue
APP_DB D1 binding Own delivery deduplication, fenced PR execution, settings/consent, recovery, checks, history, and deletion state
Scheduled maintenance Actually run expiry, offboarding, and tombstone maintenance; a declared cron alone does not prove cleanup happened
HTTPS ingress and App registration Deliver events to this exact service identity, not another environment or a dashboard route

Provision resources in your explicitly selected Cloudflare account using its supported provider tooling. Record the actual resource identities and bindings in an untracked derived configuration. The checked-in all-zero database ID is a placeholder and must not be deployed. Preserve the source configuration’s entry point, engine aliases, migration directory, queue settings, and scheduled maintenance. Moving the configuration file changes relative path resolution; rebase those paths rather than copying it to another directory unchanged.

The current configuration keeps workers_dev disabled. A repository hostname, reserved domain, or selected configuration route is not an exposed service. Establish the actual ingress separately and verify it against the App’s webhook setting. Do not expose store/admission methods as unauthenticated HTTP endpoints merely to obtain a control surface.

Apply the repository’s complete ordered D1 migrations for the selected unit to the intended database before admitting work. The maintained sequence covers ledger/leases, consent and admission, named history and processing, authorization and management state, and offboarding protection. Do not treat an older list of migration topics as a complete current inventory. Skipping an intermediate migration can change whether a repository runs or whether a restore respects an opt-out; it is not only a missing-column problem.

Use the pinned Wrangler D1 migration commands and explicitly select local versus remote state. For a local development database, this command applies only the checked-in migrations to local state:

Terminal window
node node_modules/wrangler/bin/wrangler.js d1 migrations apply APP_DB --local --config apps/github-app/wrangler.jsonc

Then the maintained local adapter can be started with:

Terminal window
node tools/run-wrangler.mjs dev --local --config apps/github-app/wrangler.jsonc

These commands are not a public deployment or complete authenticated installation. Use local fixtures for qualification rather than a copied production credential. For live migration, review provider targeting, compatibility, backups, and recovery first; never replace --local with --remote casually in a shared production terminal. The complete migration files and their D1 qualification remain in the operator source.

GET /health/ping establishes that the Worker answers. GET /health/ready currently checks database reachability; it does not prove all migrations, queue delivery, GitHub credentials, or repository execution. An HTTP 200 on either is not your release acceptance test.

Install credentials without confusing version creation with deployment

Section titled “Install credentials without confusing version creation with deployment”

The source-owned credential helper requires an already identified Worker deployment, an actual account ID, and a derived configuration without the placeholder database ID. It reads back the account, deployment, and versions before reading the private key. It is not a bootstrap command for a nonexistent Worker.

For that established target, the operator command has this shape:

Terminal window
bash -p apps/github-app/operator-launch.sh operator check --artifact-file path/to/qualified-artifact.json --artifact-sha256 qualified-64-hex-sha256 --app-id 123 --private-key-file path/to/retained-app-key.pem --account-id account-id --config path/to/derived-qualification.jsonc --receipt-file path/to/new-credential-receipt.json
bash -p apps/github-app/operator-launch.sh operator install --artifact-file path/to/qualified-artifact.json --artifact-sha256 qualified-64-hex-sha256 --app-id 123 --private-key-file path/to/retained-app-key.pem --account-id account-id --config path/to/derived-qualification.jsonc --receipt-file path/to/new-credential-receipt.json

The identifiers and file paths are specimens, not values for a real environment. The config’s exact Worker name selects the target; an optional --worker must match it. Conflicting account/Worker names and inert or missing database IDs refuse before protected input. check reads provider target facts and file metadata, then probes the receipt directory without reading protected contents or writing a Worker version. The receipt directory must already exist and the new receipt must not exist.

install privately verifies the PEM/App ID with GitHub, matches the OAuth client ID for the signed-in App, and reads the bot identity against the exact config’s GITHUB_APP_BOT_LOGIN. check leaves this identity unverified because it does not read protected contents. Actual webhook/OAuth-secret use is qualified through the live consumer journey.

The helper accepts the downloaded unencrypted RSA PKCS#1 App key and obtains the webhook secret through a hidden interactive prompt after creating its non-secret checkpoint. --webhook-secret-file accepts an operator-controlled protected file. The non-interactive --webhook-secret-stdin route requires an already-protected, redirected channel; do not echo a secret into shell history. Native output stays contained, and values go through the upload child’s private stdin rather than a temporary file. No secret value belongs in a receipt.

For the signed-in App, also supply its OAuth client ID/secret file and retained seal key file/recovery reference; add the current Wirt signing file when Wirt is configured. When an existing hidden-child consumer provides a secret directly in its environment, --seal-key-env APP_SEAL_KEY and, when needed, --wirt-signing-key-env WIRT_SIGNING_KEY_CURRENT are explicit alternatives to those two files. Pass only the variable names on the command line, select one source per key, and establish custody separately; the helper does not search ambient variables or make an environment value recoverable. The operator source owns those options and the complete seven-input specimen. Use your own production config, Worker and custody paths. Keep the original seal key recoverable with the database: generating another key cannot open existing protected authorizations or reproduce opaque file locators.

Start both helpers through the clean launch entry shown in the operator source. The Bash specimens use bash -p; on Windows replace that prefix with pwsh -NoProfile -NonInteractive -File apps/github-app/operator-launch.ps1. Keep the operator or manifest selection and all helper arguments. The entry retains native authentication and explicit seal/Wirt sources while excluding ambient Node preloads and provider transport overrides before Node starts.

Select --base-version-id <qualified-bootstrap-version> for the joined App. This checks identity/seal continuity. Prepare and qualify the complete built target through operator prepare-artifact, then pass --artifact-file <retained-artifact> and --artifact-sha256 <qualified-digest> to check and install. The complete specimen shows the preparation and protected operation. Preparation freezes the pinned native modules, versioned settings, selected static assets, routing and headers. Installation stages those assets and creates one undeployed version with that artifact and the secret bindings; a concurrent latest upload cannot select its code. Source/composition qualification and effect authorization stay separate. Existing consumers add these two artifact inputs; missing or changed artifacts refuse before protected input. Legacy receipts remain read-only resumable. For a confirmed new/empty target, use --seal-key-initial; an established target uses --seal-key-previous-receipt <durable-successful-receipt> and the same retained key/custody reference. A foreign target or different seal refuses. Initial mode also refuses when the qualified uploaded version already names an APP_SEAL_KEY secret binding. Absence of that name does not prove an empty database or recoverable key custody. The receipt’s non-secret check proves key equality against that record; it does not establish off-workstation custody or retrieval.

Retain seal/Wirt material through the admitted recovery route. GitHub credential files are transient inputs, not seven plaintext files to retain forever. The operator source provides a random generate-seal file specimen for a custody destination already established; it never prints the key or overwrites an existing file. First actual rotation/rollover separately scopes subset and previous-key support. Keep operation receipts outside disposable checkouts, with the qualified source/configuration/composition evidence, before retiring those carriers.

This helper creates an undeployed Worker version using artifact-bound installation. It does not deploy that version, register the App, install it on a repository or enable execution. Its receipt separates command acceptance, created-version identity, operation/artifact lineage, observed script ETag and secret-binding-name readback, unchanged deployment, and partial/unknown results. These are request-bound source facts, not a byte download or secret-value readback. Reconcile an interrupted operation with the existing receipt and exact unchanged config:

Terminal window
bash -p apps/github-app/operator-launch.sh operator resume --account-id account-id --config path/to/derived-qualification.jsonc --receipt-file path/to/existing-credential-receipt.json

Resume reads provider state without reading input files or creating another version. It refuses missing, unusable or foreign receipts. A provider write with an unobserved acknowledgement remains partial even if its version appears; resolve that result before another admitted effect. A credential version receipt does not establish a working deployment or recoverable external key custody.

Deploying the selected version remains a deliberate provider operation under your own operating authority. Keep credentials out of diagnostics, use the narrow required permissions, and rotate/revoke them through the selected provider route after testing the replacement. Credential rotation and code rollback do not undo database migrations or repository effects.

Verify the actual deployed version, bindings, complete migration state, protected credential installation, webhook destination, queue consumption, and maintenance execution. Then use a deliberately selected test repository and the admitted authenticated administration route to establish access, effective policy, exclusive-writer confidence, and explicit execution consent. Keep history off unless that test separately selects it.

Current source provides createAdmissionService with a deny-by-default authorization callback and revision-checked updates, plus the Community analytical App and Manage console under apps/app/. Their build, runtime composition and local qualification are described in the App component. Source availability is not a deployed or publicly admitted service. Use the supported authenticated adapter; direct SQL edits or an unprotected wrapper are not substitutes.

Exercise a supported event through signature verification, durable admission, one execution, native check, selected label/comment readback, duplicate/no-op handling, and a fresh head. Distinguish a queued event from completed application. Verify disabled access and history opt-out as well as the happy path. A one-repository backend canary does not establish general account administration or complete data-lifecycle behavior.

Ingress accepts only selected signed events and queues minimized identifiers, not raw webhook bodies. 202 with enqueued means durable admission; 202 with ignored means no selected work was queued. An enqueue failure is not a completed delivery. Work that never reached the queue needs provider-delivery investigation, not a retry of an imaginary queue record.

Once admitted, delivery deduplication and per-PR fenced leases prevent overlapping attempts from owning the same work. A lost lease must be reacquired before effects. Do not delete a lease or mark a delivery complete to force a stuck operation through. Inspect its stable code, phase, current identity, and repair/readback standing first.

A dead-letter item or open repair remains unresolved work. Repair must reacquire current access, source, policy, and provider state. A stale or closed PR can be rejected without writing; an ambiguous comment creation needs readback before any repeat. If labels succeeded but check publication or history failed, repair that consequence without blindly replaying the effects. Troubleshooting preserves the relevant diagnostics.

Keep recovery logs as minimized and bounded as the database. Queue payloads, dead letters, traces, and exports must not become a parallel raw-patch archive. The seven-day recovery boundary applies to failures too; retries do not keep an event alive indefinitely.

Use three distinct artifacts: the resolved ordinary policy for portability; the versioned application configuration/state export for settings, consent, origins, and deletion meaning; and the versioned retained numeric-history export. A raw D1 export may contain protected account/configuration state and is not the user-facing history download.

The repository supplies exportState/importState, exportHistory/importHistory, and maintain on the storage adapter. These are operator integration seams, not invented public HTTP routes or a one-command backup service. Use them through the authorized operating adapter and qualify round trips with the D1 tests. Protect exported bytes according to their contents; “no private key” does not mean public data.

Pause conflicting writers and reconcile or drain admitted work before a restore or provider move. Do not import active leases as permission to replay old effects. Restore opt-outs, expiration, and deletion tombstones before serving history or processing jobs. When a Works commercial link is configured, run its restore reconciliation before serving again: restored commercial state is held until Works confirms it, through the restore seam the App source guide points to. Imported repositories return with unknown reach and pending enablement, not implicit installation access or renewed consent. Re-establish current authorization and obtain the required explicit re-consent before resuming.

A useful restore proves policy/configuration identities, history versions and evidence, deletion survival, no expired-data resurrection, and one newly authorized operation. D1 SQL import alone proves none of those application consequences. Define the longest supported backup-restore window: tombstones must outlive it by seven days. Disclose bounded provider-backup expiration instead of promising immediate erasure of every backup.

Before a code update, read the selected source and migrations, exercise local verification, record the current version/configuration, and test recovery in an isolated environment. A previous Worker version is not automatically compatible with an upgraded schema. Stop and reconcile on incompatibility rather than treating rollback as a database reversal.

Disable new execution and collection for the intended scope, reconcile in-flight work, preserve authorized exports, and apply the chosen offboarding/deletion schedule. A temporary access error is not sufficient evidence to destroy an installation’s history. Confirm real removal or an administrator’s selected action.

After the retained-data and recovery obligations are complete, remove the selected ingress, credentials, installation access, queues, database, and other resources through their provider controls. Do not destroy the database or maintenance route first and strand deletion work. Removing an App does not require a final GitHub cleanup write, and old App-owned labels/comments do not disappear merely because the service stopped.

The App architecture, privacy and history contract, security reporting, and licence map remain the complete specialist authorities. Your deployment must meet its own access, disclosure, retention, support, and AGPL obligations; the official service’s future public terms are not automatically yours.