Docs /deployment/github-app

GitHub App: Staging vs Prod Split (Plan)

Status: plan only — this document captures the target state and migration path. No code or infrastructure changes are made by landing this doc.

Tracked by #230.

Why two apps

Izri ships a single GitHub App today (izri). Both staging and production point at the same App ID, private key, and webhook secret. That means:

  1. Webhook collisions. A push event from a customer install gets delivered to every webhook URL on the App. With one App, that's whatever URL was last saved — staging or prod, never both. If we change the URL to debug staging, prod stops receiving events until we change it back.
  2. Blast radius on secret rotation. Rotating the private key forces a simultaneous redeploy of staging and prod or one of them breaks.
  3. No Marketplace eligibility. GitHub Marketplace requires a single production App with stable identity, separate from dev / staging Apps.

The fix is the conventional GitHub-recommended pattern: one App per environment, each with its own webhook URL, private key, App ID, slug, and installation list.

Target state

Field izri-staging izri (prod)
App name Izri (Staging) Izri
Slug izri-staging izri
Homepage URL https://staging.izri.dev https://izri.dev
Webhook URL https://api.staging.izri.dev/api/webhooks/github https://api.izri.dev/api/webhooks/github
Webhook secret per-env random 32-byte hex per-env random 32-byte hex
Private key per-env PEM (downloaded once, stored in Railway) per-env PEM (downloaded once)
Permissions Identical (see below) Identical
Events subscribed Identical (pull_request, push, check_run, …) Identical
Installable on Owner only (legendify-dev org during dev) Public (after Marketplace listing)
Marketplace listing No Yes (deferred — separate work)

Permissions (read/write, identical across both):

  • Repository: Contents (read), Pull requests (read & write), Checks (read & write), Metadata (read), Issues (read & write).
  • Organization: Members (read).
  • Account: none.

Events: pull_request, pull_request_review, push, check_run, check_suite, installation, installation_repositories.

Env-var shape (no code change in this PR)

Today's flat scalars in env.config.ts (GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRET) stay as-is per environment. Staging Railway env gets the staging App's values; prod Railway env gets the prod App's values. No code change is required because each environment already has its own env-var bag.

A separate follow-up may introduce per-environment overrides (GITHUB_APP_ID_STAGING, GITHUB_APP_ID_PROD) consumed conditionally based on NODE_ENV, but that's only worth doing if we ever need both apps in a single deployed process — which we don't. Recommendation: keep scalar env vars; let Railway environment scope do the segregation.

Migration steps (production cutover)

Order matters — we want zero webhook downtime.

  1. Create izri-staging at https://github.com/settings/apps/new with the table above. Generate a private key, download the PEM, save it to Railway STAGING env as GITHUB_APP_PRIVATE_KEY (multi-line). Save the App ID and a freshly generated webhook secret to the same env.
  2. Install izri-staging on the dev org's test repos. Verify webhooks arrive at the staging API (/api/webhooks/github) and Check Runs render.
  3. Rename the existing App to izri (production-only). Update the webhook URL to point only at https://api.izri.dev/api/webhooks/github. The existing private key + webhook secret stay valid for production.
  4. Move the existing test installations off the prod App and onto the staging App. From this point, prod-customer installs target the prod App URL only; internal dogfood targets the staging App URL only.
  5. Rotate the prod private key as a cleanup (it was previously used from both envs). Update prod Railway env, redeploy.
  6. Document the recreation procedure at the bottom of this file so either App can be re-provisioned from scratch if the keys ever leak.

Rollback: each step is reversible. If staging webhooks fail to arrive, the prod App still works — staging is purely additive until step 4.

Marketplace listing (deferred sub-task)

Once the prod App is stable on izri.dev with paying customers, submit it to GitHub Marketplace.

Marketplace requirements:

  • App must be public, owned by a verified organization (legendify-dev).
  • Pricing plans defined (free + paid tiers).
  • Branding: 1024×1024 avatar, 200×200 badge, short + long descriptions, screenshots.
  • Stable webhook URL on a custom domain (no Heroku / Railway-default domains).
  • Privacy policy + terms of service URLs hosted under izri.dev.

Submission flow:

  1. Open https://github.com/marketplace/new and follow the listing wizard for the prod izri App.
  2. Attach pricing plans via github.com/settings/apps/izri/plans.
  3. Submit for review. GitHub's review SLA is 2–4 weeks.

This is intentionally not done in this PR — see #230 acceptance criteria. The work is gated on having a stable v1, billing wired up (Stripe), and approved brand assets.

Brand assets (deferred)

Both Apps need the same avatar / badge to look coherent in the GitHub UI. Upload via github.com/settings/apps/<slug>/edit once the design team delivers final 1024×1024 PNG + 200×200 PNG.

Local-dev tunnelling (deferred)

For local-dev work against a real GitHub App without exposing localhost, add a guide for one of:

  • smee.io (zero-setup, works out of the box, no install). Recommended for first-time contributors.
  • cloudflared (cloudflared tunnel) — more durable, requires a Cloudflare account.
  • ngrok — alternative; rate-limited free tier.

The guide will document:

  • Pointing a third dev-only App (izri-dev-<username>) at the tunnel.
  • How to keep the tunnel URL pinned in the App settings.
  • How to share the App among multiple developers (don't — each dev runs their own App; the App is the per-developer secret).

Open questions

  • Whether to add a GITHUB_APP_INSTALLATION_ID env var for tests, or keep resolving install IDs at runtime via /orgs/{org}/installation.
  • Whether the runner needs its own private key (currently uses the installation token via the API). Lean: no — the runner shouldn't have App-level credentials.

References

  • #200 – GitHub App phase 1 (creation + JWT minting)
  • #208 – GitHub App phase 5 (Check Runs integration)
  • #230 – this issue
  • packages/trpc/src/services/githubApp.ts – App auth implementation
  • apps/api/src/routes/githubApp.ts – webhook listener

Reading this with an agent? /docs/deployment/github-app.md serves the raw markdown.

All docs