# 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](https://github.com/legendify-dev/izri/issues/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](https://github.com/legendify-dev/izri/issues/200) – GitHub App
  phase 1 (creation + JWT minting)
- [#208](https://github.com/legendify-dev/izri/issues/208) – GitHub App
  phase 5 (Check Runs integration)
- [#230](https://github.com/legendify-dev/izri/issues/230) – this issue
- `packages/trpc/src/services/githubApp.ts` – App auth implementation
- `apps/api/src/routes/githubApp.ts` – webhook listener
