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:
- 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.
- Blast radius on secret rotation. Rotating the private key forces a simultaneous redeploy of staging and prod or one of them breaks.
- 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.
- Create
izri-stagingat https://github.com/settings/apps/new with the table above. Generate a private key, download the PEM, save it to RailwaySTAGINGenv asGITHUB_APP_PRIVATE_KEY(multi-line). Save the App ID and a freshly generated webhook secret to the same env. - Install
izri-stagingon the dev org's test repos. Verify webhooks arrive at the staging API (/api/webhooks/github) and Check Runs render. - Rename the existing App to
izri(production-only). Update the webhook URL to point only athttps://api.izri.dev/api/webhooks/github. The existing private key + webhook secret stay valid for production. - 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.
- Rotate the prod private key as a cleanup (it was previously used from both envs). Update prod Railway env, redeploy.
- 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:
- Open https://github.com/marketplace/new and follow the listing wizard
for the prod
izriApp. - Attach pricing plans via
github.com/settings/apps/izri/plans. - 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_IDenv 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.