-
Notifications
You must be signed in to change notification settings - Fork 0
chore(ci,docs): retire staging — dev→beta→main ladder + beta-first self-host plan #670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ with **Cloudflare** in front and the database staying on **managed Neon**. | |
| Cloudflare ── DNS + CDN + DDoS + WAF (proxied, Full-strict TLS) | ||
| │ (origin reachable ONLY from Cloudflare IPs) | ||
| ┌────────────┴─────────────────────────────┐ | ||
| │ One VPS (Hetzner CX42 / Hostinger KVM4) │ | ||
| │ One VPS (Contabo VPS 8 / Hetzner CX43) │ | ||
| │ │ | ||
| │ Openship control plane + OpenResty edge │ :80/:443 (firewalled to CF) | ||
| │ ├─ web (Next.js, apps/web) │ loopback only | ||
|
|
@@ -45,13 +45,29 @@ money-critical data + backups are never our responsibility on the box. | |
| | Database | **Neon** (managed) — the box only runs **stateless** app containers | | ||
| | Ingress | **Openship edge + Cloudflare proxied**, origin firewalled to Cloudflare IPs | | ||
| | Payments at launch | **Paystack only** — `MONNIFY_*_ENABLED=false` | | ||
| | Rollout | **Staging first**, then production | | ||
| | Rollout | **Beta first** (disposable rehearsal box), then a **separate dedicated** production box | | ||
| | Environments on the box | **Beta only** to start; prod gets its **own** box once beta proves the stack | | ||
|
|
||
| **The three guardrails that make a young platform safe:** | ||
| 1. **DB on Neon** — if Openship ever breaks, no customer data was on it. | ||
| 2. **Pin Openship to a tagged release** (`OPENSHIP_VERSION`), not `main`. | ||
| 3. **Stand up on `staging` first** and run real flows (auth, Paystack **test** | ||
| checkout, feed, product pages) before production exists. | ||
| 3. **Stand up `beta` first** — a **disposable** infra rehearsal (Paystack **test** | ||
| keys, throwaway data) — and run real flows (auth, test checkout, feed, product | ||
| pages) before production exists on its own box. | ||
|
|
||
| **Environment model (post-staging-drop):** | ||
|
|
||
| | Tier | Who | Where | Data | | ||
| | --- | --- | --- | --- | | ||
| | **dev + per-PR previews** | Team — integration & final QA (this replaces staging) | Vercel previews / ephemeral Neon | throwaway | | ||
| | **beta** | Selected testers — infra rehearsal of the self-host stack | The VPS (this box) | **disposable**, Paystack test | | ||
| | **main / prod** | Real users; early access via **allowlist** | A **separate dedicated** box (later) | real, Paystack live | | ||
|
|
||
| **No beta→prod data migration.** Beta and prod are separate Neon databases and | ||
| nothing crosses automatically; beta runs sandbox payments, so its financial state | ||
| can never become real money. Real early-access users therefore live on **prod | ||
| behind an allowlist** from day one — "exiting beta" just opens the gate, and no | ||
| data is ever migrated. The beta box stays a disposable rehearsal. | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -62,14 +78,28 @@ runtime; 3 frontend instances; `socket.io` WebSocket growth): | |
|
|
||
| | Target | Spec | Provider option | ~Cost/mo | | ||
| | --- | --- | --- | --- | | ||
| | **Recommended (holds long)** | **8 vCPU / 16 GB / 160 GB NVMe** | **Hetzner CX42** (EU) | ~€17 (~$18) | | ||
| | Vendor-consolidated | 4 vCPU / 16 GB / 200 GB NVMe | **Hostinger KVM 4** (check renewal price) | ~$13 | | ||
| | Budget floor | 4 vCPU / 8 GB / 80 GB NVMe | Hetzner CX32 | ~€7 | | ||
|
|
||
| | **Beta rehearsal (current pick)** | **8 vCPU / 24 GB / 300 GB SSD** | **Contabo Cloud VPS 8** (EU) | ~€14 **month-to-month** (no prepay) | | ||
| | Recommended for prod (consistent) | 8 vCPU / 16 GB / 160 GB NVMe | **Hetzner CX43** (EU) | ~€16 incl. IPv4 (~$17) | | ||
| | Budget floor | 4 vCPU / 8 GB / 80 GB NVMe | Hetzner CX33 | ~€8 | | ||
|
|
||
| - **Two-box end state:** the current box runs **beta only** (rehearsal); when the | ||
| stack is proven, **prod gets its own dedicated box** — either promote this box | ||
| to prod-only (fresh OS reinstall) and move beta to a cheap box, or buy a fresh | ||
| prod box. Never run prod cohabiting with beta long-term (blast radius, noisy | ||
| neighbor, wider attack surface). | ||
| - **Contabo notes:** cheap because oversubscribed → variable vCPU/disk performance | ||
| and slow provisioning/support. Fine for **beta**. Order **month-to-month, 1 | ||
| IPv4** — the beta box needs only **one origin IP**; all its surfaces | ||
| (`beta`, `app.beta`, `admin.beta`, `api.beta`) sit behind that single IP via the | ||
| edge's host-based routing. (The separate **prod** box later gets its **own** | ||
| origin IP.) **Do not prepay 24 months** on a box you may rebuild. If prod later | ||
| wants consistent performance, Hetzner is the pick — but then Contabo's numbers | ||
| won't perfectly predict prod's. | ||
| - **Region:** EU (no budget provider has African DCs; Cloudflare edge serves | ||
| static close to Nigerian users; API latency is EU-ish, which is fine). | ||
| - **Add the provider's automated snapshots (~$1–2/mo)** — the box is a single | ||
| point of failure; snapshots + Neon data + Openship config = fast rebuild. | ||
| - **Add the provider's automated snapshots / Auto Backup (~€1–5/mo)** — the box is | ||
| a single point of failure; snapshots + Neon data + Openship config = fast | ||
| rebuild. (Data itself lives on Neon; box backups only protect config/state.) | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -78,7 +108,7 @@ runtime; 3 frontend instances; `socket.io` WebSocket growth): | |
| - [ ] VPS purchased (spec above), **Ubuntu 24.04 LTS**, SSH key added (no password login). | ||
| - [ ] Cloudflare zone `twizrr.com` active (nameservers already moved — see the | ||
| Cloudflare setup done separately). | ||
| - [ ] Neon connection strings ready for **staging** (`ep-mute-glitter`) and later | ||
| - [ ] Neon connection strings ready for **beta** (`ep-winter-hall`) and later | ||
| **production** (`ep-morning-mountain`). Pull with | ||
| `npx neonctl connection-string <branch> --project-id cold-star-22806085`. | ||
| - [ ] A pinned Openship version chosen (latest tagged release, e.g. `v0.3.0`). | ||
|
|
@@ -254,18 +284,18 @@ backend reaches it by service name via `REDIS_URL`. | |
| ### 8.4 Environment variables (per app) | ||
| Set these in each Openship app (secrets stay in Openship, never in git). | ||
|
|
||
| **Environment-specific values differ between staging and production** — do not | ||
| reuse production domains/keys on staging or the staging smoke test (Phase 5) | ||
| **Environment-specific values differ between beta and production** — do not | ||
| reuse production domains/keys on beta or the beta smoke test (Phase 5) | ||
| will fail (CORS blocks, live charges). Use this matrix for the vars that change: | ||
|
|
||
| | Var | Staging | Production | | ||
| | Var | Beta | Production | | ||
| | --- | --- | --- | | ||
| | `DATABASE_URL` / `DIRECT_URL` | Neon **staging** (`ep-mute-glitter`) | Neon **production** (`ep-morning-mountain`) | | ||
| | `CORS_ORIGINS` | `https://staging.twizrr.com,https://app.staging.twizrr.com,https://admin.staging.twizrr.com` | `https://twizrr.com,https://app.twizrr.com,https://admin.twizrr.com` | | ||
| | `WEB_URL` / `APP_URL` / `FRONTEND_URL` | `*.staging.twizrr.com` | prod hosts | | ||
| | `AUTH_COOKIE_DOMAIN` | `staging.twizrr.com` | `twizrr.com` | | ||
| | `DATABASE_URL` / `DIRECT_URL` | Neon **beta** (`ep-winter-hall`) | Neon **production** (`ep-morning-mountain`) | | ||
| | `CORS_ORIGINS` | `https://beta.twizrr.com,https://app.beta.twizrr.com,https://admin.beta.twizrr.com` | `https://twizrr.com,https://app.twizrr.com,https://admin.twizrr.com` | | ||
| | `WEB_URL` / `APP_URL` / `FRONTEND_URL` | `https://beta.twizrr.com` / `https://app.beta.twizrr.com` (each var → its surface's full origin) | `https://twizrr.com` / `https://app.twizrr.com` | | ||
| | `AUTH_COOKIE_DOMAIN` | `beta.twizrr.com` | `twizrr.com` | | ||
| | `PAYSTACK_*` / `NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY` | **test** keys | **live** keys | | ||
| | `NEXT_PUBLIC_API_URL` | `https://api.staging.twizrr.com` | `https://api.twizrr.com` | | ||
| | `NEXT_PUBLIC_API_URL` | `https://api.beta.twizrr.com` | `https://api.twizrr.com` | | ||
|
Comment on lines
297
to
+298
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Files matching deployment docs:"
git ls-files | rg -n 'TWIZRR_SELF_HOST_DEPLOYMENT\.md|deploy|deployment|paystack|PAYSTACK' || true
echo
echo "Relevant lines in TWIZRR_SELF_HOST_DEPLOYMENT.md around 260-320:"
if [ -f TWIZRR_SELF_HOST_DEPLOYMENT.md ]; then
sed -n '260,320p' TWIZRR_SELF_HOST_DEPLOYMENT.md | cat -n -v
fi
echo
echo "Search for PAYSTACK_WEBHOOK_SECRET in repository:"
rg -n "PAYSTACK_WEBHOOK_SECRET|paystack|PAYSTACK|webhook" TWIZRR_SELF_HOST_DEPLOYMENT.md . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200Repository: coded-devs/twizrr Length of output: 28248 🌐 Web query:
💡 Result: Yes, Paystack uses different secret keys for its sandbox (test) and live environments [1][2]. Because the webhook signature is generated using your secret key [3][4], the key used to verify the signature must match the environment from which the webhook was sent [1][2]. When configuring your webhook handler, ensure that your application uses the corresponding secret key based on the environment: 1. Test Mode: Use your Test Secret Key (prefixed with sk_test_) to verify signatures for webhooks received in the test environment [1][2]. 2. Live Mode: Use your Live Secret Key (prefixed with sk_live_) to verify signatures for webhooks received in the production environment [1][2]. You can find both sets of keys in your Paystack Dashboard under Settings > API Keys & Webhooks [1][2][4]. It is recommended to use environment variables to manage these keys securely and prevent the accidental use of test keys in production [5][6]. Failure to use the correct key for the respective environment will result in signature verification errors [5]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Search Paystack secret/env var usage:"
rg -n "PAYSTACK_SECRET_KEY|PAYSTACK_WEBHOOK_SECRET|secretKey|webhook" apps backend packages apps/backend/src --glob '!node_modules' --glob '!dist' --glob '!build' 2>/dev/null | head -250 || true
echo
echo "Relevant deployment webhook instructions around line 343:"
sed -n '330,350p' TWIZRR_SELF_HOST_DEPLOYMENT.md | cat -n -vRepository: coded-devs/twizrr Length of output: 34443 Make The backend falls back to 🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
| **Backend (`twizrr-backend`)** — everything above, plus (same on both envs): | ||
| - `REDIS_URL` → the internal Openship Redis service | ||
|
|
@@ -280,10 +310,10 @@ matrix, plus the surface flags that differ per app: | |
|
|
||
| --- | ||
|
|
||
| ## 9. Phase 5 — Deploy on STAGING first | ||
| ## 9. Phase 5 — Deploy on BETA first | ||
|
|
||
| 1. Point the backend's `DATABASE_URL`/`DIRECT_URL` at the **staging Neon branch** (`ep-mute-glitter`). | ||
| 2. Track the **`staging`** git branch in each Openship app (push-to-deploy). | ||
| 1. Point the backend's `DATABASE_URL`/`DIRECT_URL` at the **beta Neon branch** (`ep-winter-hall`). | ||
| 2. Track the **`beta`** git branch in each Openship app (push-to-deploy). | ||
| 3. Deploy: | ||
|
|
||
| ```bash | ||
|
|
@@ -292,10 +322,10 @@ openship init # link directory → project (confirm per-app flow in doc | |
| openship deploy | ||
| ``` | ||
|
|
||
| 4. Attach **staging domains** in Cloudflare (proxied): `staging.twizrr.com`, | ||
| `app.staging.twizrr.com`, `admin.staging.twizrr.com`, `api.staging.twizrr.com` → VPS IP. | ||
| 5. **Smoke test on staging:** | ||
| - [ ] `api.staging.twizrr.com/health` OK (Redis reachable) | ||
| 4. Attach **beta domains** in Cloudflare (proxied): `beta.twizrr.com`, | ||
| `app.beta.twizrr.com`, `admin.beta.twizrr.com`, `api.beta.twizrr.com` → VPS IP. | ||
| 5. **Smoke test on beta:** | ||
| - [ ] `api.beta.twizrr.com/health` OK (Redis reachable) | ||
| - [ ] Create/edit a product; publish + post-to-feed | ||
| - [ ] Auth (login/signup), feed renders, product detail renders | ||
| - [ ] Paystack **test** checkout end-to-end | ||
|
|
@@ -305,17 +335,25 @@ openship deploy | |
|
|
||
| ## 10. Phase 6 — Production go-live | ||
|
|
||
| Only after staging is signed off. | ||
| Only after beta is signed off. **Production runs on its own dedicated box** (§3 | ||
| two-box end state) — provision it and run Phases 1–3 on it, or promote the beta | ||
| box to prod-only via a fresh OS reinstall (moving beta to a cheap box first). | ||
|
|
||
| 1. **Neon:** confirm the production branch (`ep-morning-mountain`) is migrated to | ||
| the current schema and seeded (reference data + `bootstrap:admin`). | ||
| 2. **Backend env → production Neon**; confirm **Paystack live** keys + | ||
| `MONNIFY_*_ENABLED=false`; Paystack dashboard webhook → `api.twizrr.com`. | ||
| 3. Track the **`main`** branch in the production Openship apps; deploy. | ||
| 4. **Cut the domains:** in Cloudflare, repoint `twizrr.com`, `www`, `app`, | ||
| `admin`, `api` from the old Vercel/Railway targets to the **VPS IP** (proxied). | ||
| 5. **Verify:** `/health`, all three surfaces, a real login, a live checkout. | ||
| 6. Keep the old Vercel/Railway deploys **parked** (not deleted) for the soak window. | ||
| 4. **Early access via allowlist, not migration.** Real selected users sign up | ||
| **directly on prod** behind an allowlist/invite flag — their data is prod data | ||
| from day one. **Nothing migrates from beta** (separate DB, sandbox payments). | ||
| "Exiting beta" = opening the gate; there is no cutover of user data. | ||
| 5. **Cut the domains:** in Cloudflare, repoint `twizrr.com`, `www`, `app`, | ||
| `admin`, `api` from the old Vercel/Railway targets to the **prod VPS IP** | ||
| (proxied). Give `dev` its own host (e.g. `dev.twizrr.com`, team-gated) so it no | ||
| longer squats on the apex. | ||
| 6. **Verify:** `/health`, all three surfaces, a real login, a live checkout. | ||
| 7. Keep the old Vercel/Railway deploys **parked** (not deleted) for the soak window. | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -332,18 +370,22 @@ Only after staging is signed off. | |
|
|
||
| ## 12. Promotion pipeline mapping | ||
|
|
||
| The `dev → staging → beta → main` ladder is unchanged — only the deployer swaps | ||
| from Vercel/Railway to Openship: | ||
| The ladder is now `dev → beta → main` (staging dropped). The deployer swaps from | ||
| Vercel/Railway to Openship: | ||
|
|
||
| | Git branch | Neon branch | Openship target | | ||
| | --- | --- | --- | | ||
| | `dev` | dev (`ep-round-silence`) | local/preview (optional) | | ||
| | `staging` | staging (`ep-mute-glitter`) | staging apps | | ||
| | `beta` | beta (`ep-winter-hall`) | beta apps (optional box/env) | | ||
| | `main` | production (`ep-morning-mountain`) | production apps | | ||
| | `dev` | dev (`ep-round-silence`) | Vercel previews / local (team) | | ||
| | `beta` | beta (`ep-winter-hall`) | **beta apps on this VPS** (disposable rehearsal) | | ||
| | `main` | production (`ep-morning-mountain`) | **production apps on a dedicated box** (later) | | ||
|
|
||
| > **`staging` is retired.** Its git branch and Neon branch (`ep-mute-glitter`) go | ||
| > dormant — keep them parked (Neon scales to zero) or delete once you're sure. | ||
| > Per-PR preview envs + `dev` cover pre-beta testing. | ||
|
|
||
| Each Openship app tracks one branch; a push re-runs the pipeline for that | ||
| environment. The GitHub **promotion-guard** workflow still enforces the ladder. | ||
| environment. The GitHub **promotion-guard** workflow enforces the `dev → beta → | ||
| main` ladder. | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -383,7 +425,7 @@ accumulates on the new stack. | |
|
|
||
| - **First limits you'll hit:** WebSocket concurrency (RAM + FDs — already raised) | ||
| and build-vs-runtime contention. | ||
| - **Vertical:** bump the VPS (CX42 → 16 vCPU / 32 GB) — a resize + reboot. | ||
| - **Vertical:** bump the VPS (CX43 → CX53: 16 vCPU / 32 GB) — a resize + reboot. | ||
| - **Then split concerns:** dedicated build, or move Redis to managed (Upstash), | ||
| or run frontend and backend on separate boxes. | ||
| - **Consolidate frontends** (store+app one runtime, admin separate) to reclaim RAM. | ||
|
|
@@ -395,7 +437,7 @@ accumulates on the new stack. | |
|
|
||
| The docs were being filled out at time of writing. Confirm before relying on: | ||
| - Exact `openship.json` schema (monorepo subdir, build/start, port, per-app env). | ||
| - Per-branch / multi-environment mapping (staging vs prod apps). | ||
| - Per-branch / multi-environment mapping (beta vs prod apps). | ||
| - Version pinning mechanism (`OPENSHIP_VERSION`) and upgrade/rollback commands. | ||
| - Certificate issuance flow with Cloudflare proxied (HTTP-01 timing). | ||
| - Whether to use Openship-managed Redis vs an external managed Redis. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the stale staging reference.
The mapping is correct, but the nearby rationale still mentions
staging <- dev. Change it tobeta <- devso the workflow documentation matches the enforced ladder.🤖 Prompt for AI Agents