From f536631abe0573dea22ce087050cc07daef796ca Mon Sep 17 00:00:00 2001 From: Satelink Network Date: Tue, 10 Nov 2026 15:28:16 +0530 Subject: [PATCH] docs(launch): correct DNS/email runbook, document Preview login, PhonePe currency note, release packet - DNS_AND_EMAIL_ROUTING: Vercel records DNS-only (grey cloud), apex A + www CNAME from Vercel Settings > Domains, www -> apex redirect in Vercel; drop proxied/Full (Strict) steps. DMARC starts at p=none with rua, then quarantine, then reject. Email Routing is receive/forward only; "send as" options stated. Cites current Vercel and Cloudflare docs. - PREVIEW_LOGIN (new): test bot bound to the launch-mvp branch alias, production bot to nightlifez.com; exact env names the code reads. - UPI_PROVIDER_READINESS: note that the payload-currency rule makes PhonePe v1 confirmations impossible by design; revisit if onboarded. - PRODUCTION_RELEASE_PACKET (new): migrations 010-020 and which touch existing rows, Gate 3 procedure, Production env names, release and rollback steps. - PROJECT_STATE: consolidation SHAs, superseded PRs, migrations per environment, owner gates. Co-Authored-By: Claude Opus 5.5 --- docs/PROJECT_STATE.md | 103 ++++++------ docs/UPI_PROVIDER_READINESS.md | 11 ++ docs/launch/DNS_AND_EMAIL_ROUTING.md | 200 +++++++++++------------ docs/launch/PREVIEW_LOGIN.md | 45 +++++ docs/launch/PRODUCTION_RELEASE_PACKET.md | 86 ++++++++++ 5 files changed, 284 insertions(+), 161 deletions(-) create mode 100644 docs/launch/PREVIEW_LOGIN.md create mode 100644 docs/launch/PRODUCTION_RELEASE_PACKET.md diff --git a/docs/PROJECT_STATE.md b/docs/PROJECT_STATE.md index 68683b7..2365d13 100644 --- a/docs/PROJECT_STATE.md +++ b/docs/PROJECT_STATE.md @@ -2,7 +2,7 @@ **Single source of truth for where the code, environments and gates stand.** Each milestone PR updates this file. Other docs hold design and how-to detail; this file links to them instead of repeating them. Older point-in-time reports live in [`history/`](history/README.md). -Captured **2026-11-03 07:30 IST**, after `git fetch --all --prune`, from `git`, `gh` and Vercel deployment metadata. Every SHA, PR, run and build-log line below was read in that session. Anything not verified says so. +Captured **2026-11-10 15:30 IST**, after `git fetch --all --prune`, from `git`, `gh` and Vercel deployment metadata (§2–§4 and §7 refreshed; §5, §6 and §8 are from 2026-11-03 and not re-checked). Every SHA, PR, run and build-log line below was read in that session. Anything not verified says so. --- @@ -20,63 +20,57 @@ Repository: `Jakuraa/StrangersMeetup`. Vercel project: `strangersmeetup` (the te | Ref | SHA | What it is | |---|---|---| -| `origin/main` | `5c6652a` | Merge of PR #2 (2026-10-29). **Contains nothing that `launch-mvp` lacks**: both parents (`b13d927`, `7bc062b`) are ancestors of `launch-mvp`. 78 commits behind `launch-mvp`. | -| `origin/launch-mvp` | `c71ec8d` | Merge of PR #29 (M4.2 UPI Security Audit). Baseline for M4.3. | -| `origin/feature/p3-location-intelligence` | `d7836ff` | PR #23 feature branch head; merged into `launch-mvp` at `7c5079e`. | -| `origin/fix/preview-recovery-audit` | `bcb4989` | Commit holding Phase 5.6/5.7 reports; pushed to origin per owner decision. | - -Local `main` in the main clone is at `5693f7b`, an older Production commit that is behind `origin/main`. It is not checked out anywhere. Local `launch-mvp` matches `origin/launch-mvp` @ `c71ec8d`. - -## 3. Open pull requests and CI - -| PR | Base ← Head | Head SHA | Checks on head | Latest CI run | -|---|---|---|---|---| -| #17 Production release: free-only MVP | `main` ← `launch-mvp` | `83a30a7` | `lint, unit, e2e` ✅ (push + PR) · Vercel ✅ · Vercel Preview Comments ✅ | [41564501659](https://github.com/Jakuraa/StrangersMeetup/actions/runs/41564501659) (push) success | -| #30 M4.3 UPI Security Remediation | `launch-mvp` ← `feature/m4-3-upi-security-remediation` | `e4babe6` | `lint, unit, e2e` ✅ | PR Review Baseline for M4.3.1 | -| #31 M4.3.1 Review Remediation | `feature/m4-3-upi-security-remediation` ← `feature/m4-3-1-review-remediation` | `03bf518` | `lint, unit, e2e` ✅ | Independent Review Corrections for PR #30 | -| #32 M4.3.2 Financial Integrity Remediation | `feature/m4-3-1-review-remediation` ← `feature/m4-3-2-financial-integrity` | `62cb0a3` | `lint, unit, e2e` ✅ · Vercel ✅ | Fail-Closed Merchant Identity & Secure Manual Refund Reconciliation | -| #33 M4.3.3 Final Provider Verification Integrity | `feature/m4-3-2-financial-integrity` ← `feature/m4-3-3-provider-verification` | `4c54711` | `lint, unit, e2e` ✅ · Vercel ✅ | Removal of Currency Defaults, Elimination of Merchant Fallback, Authentic S2S Polling & Attestation | -| #34 M4.3.4 Provider Contract Decision & M4 Closure | `feature/m4-3-3-provider-verification` ← `feature/m4-3-4-contract-decision` | `1e5bd19` | `lint, unit, e2e` ✅ | PhonePe Contract Determination (Outcome B), Full Regression Suite, Audit & Milestone M4 Closure | -| #35 M5 Telegram & Operational Workflows | `feature/m4-3-4-contract-decision` ← `feature/m5-telegram-operational-workflows` | `262f4f0` | `lint, unit, e2e` ✅ | Telegram Identity & Account Linking, Event Private Group Isolation (No Shared Group), Operational Notifications with Quiet Hours, /report Workflow | -| #36 M6 Organiser Workspace, Event Staff & Secure QR Check-In | `feature/m5-telegram-operational-workflows` ← `feature/m6-organiser-workspace` | `66ad026` | `lint, unit, e2e` ✅ | Server-Authoritative QR Tickets, Event-Scoped Staff Roles, Check-In Validation, Duplicate Scan Prevention, Capacity & Inventory | -| #37 M7 Attendee Discovery & Experience Hub | `feature/m6-organiser-workspace` ← `feature/m7-attendee-experience` | `5453ab1` | `lint, unit, e2e` ✅ | Personalised Feed, Category Collections, Distance Bands, Mystery Venue Teasers, Bookmarking, Responsive Discovery | -| #38 M8 Real-time Notifications & Operational Comms | `feature/m7-attendee-experience` ← `feature/m8-realtime-notifications` | `0b30221` | `lint, unit, e2e` ✅ | Notification Outbox, Quiet Hours (IST), Attendee Self-Cancellation, Host Approval Webhooks, Admin Outbox Management | -| #39 M9 Multi-Role E2E, Security, Performance & Accessibility Review | `feature/m8-realtime-notifications` ← `feature/m9-testing-security-review` | `b8c82c5` | `lint, unit, e2e` ✅ | 7-Role E2E Browser Harness, IDOR/BOLA & Security Audit, 44px Mobile Tap Targets, Sub-Millisecond Queries, Blocker #6 Resolution | -| #40 M10 Beta Readiness, Operational Hardening & Final Engineering Gate | `feature/m9-testing-security-review` ← `feature/m10-beta-readiness-hardening` | `112361f` | `lint, unit, e2e` ✅ | Operational Hardening Runbook, 7-Role Acceptance Checklist, Venue/Partner Lifecycles, Blocker #6 Reconciliation, M1–M10 Gate Signoff | - -**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #40, PR #39, PR #38, PR #37, PR #36, PR #35, PR #34, PR #33, PR #32, PR #31, PR #30, and PR #17 must not be merged without separate owner approval. Keep `ENABLE_LIVE_PAYMENTS=false` everywhere. - -PR #23 is **merged** at `7c5079e` (P3 Location Intelligence + M2 Remediation). -PR #25 is **merged** at `83a30a7` (Owner Addendum Ops: dynamic DB block list, branch archiving, Phase 5.6/5.7 reports). -PR #26 is **merged** at `46b85c1` (M3 Deployment & Database Migration Architecture). -PR #27 is **merged** at `69cafae` (M4 Payment, Booking and Revenue Readiness Audit). -PR #28 is **merged** at `c3b4e83` (M4.1 UPI-First Checkout, Dynamic QR & Verified Payment Integration). -PR #29 is **merged** at `c71ec8d` (M4.2 Independent UPI Payment Security & Production Readiness Audit). -PR #30 is **open / preserved** (`feature/m4-3-upi-security-remediation`). -PR #31 is **open / preserved** (`feature/m4-3-1-review-remediation`). -PR #32 is **open / preserved** (`feature/m4-3-2-financial-integrity`). -PR #33 is **open / preserved** (`feature/m4-3-3-provider-verification`). -PR #34 is **open / preserved** (`feature/m4-3-4-contract-decision`). -PR #35 is **open / preserved** (`feature/m5-telegram-operational-workflows`). -PR #36 is **open / preserved** (`feature/m6-organiser-workspace`). -PR #37 is **open / preserved** (`feature/m7-attendee-experience`). -PR #38 is **open / preserved** (`feature/m8-realtime-notifications`). -PR #39 is **open / preserved** (`feature/m9-testing-security-review`). -PR #40 is **open / preserved** (`feature/m10-beta-readiness-hardening`). +| `origin/main` | `5c6652a` | Production. Merge of PR #2 (2026-10-29). Contains nothing that `launch-mvp` lacks. | +| `origin/launch-mvp` | `771f6e1` | Merge of **PR #41**, "Consolidate M4–M10 (supersedes #30–#40)", 2026-11-10. Head of the consolidated stack: `02bfdba`. | +| `archive/feature/m4-3-upi-security-remediation` … `archive/feature/m10-beta-readiness-hardening` | 11 tags | Heads of the superseded PRs #30–#40 (table in §3). Branches deleted after tagging. | + +## 3. Pull requests and CI + +**Open** + +| PR | Base ← Head | Purpose | +|---|---|---| +| #17 Production release | `main` ← `launch-mvp` | Owner gate 1. Must not be merged without the owner's separate approval. | +| #42 | `launch-mvp` ← `fix/s3a-upi-checkout-flag` | S3a: `UPI_CHECKOUT_ENABLED` separate from `ENABLE_LIVE_PAYMENTS` | + +**Consolidation (2026-11-10).** PR #41 merged `feature/m10-beta-readiness-hardening` @ `02bfdba` into `launch-mvp` as `771f6e1`. CI run [42791819075](https://github.com/Jakuraa/StrangersMeetup/actions/runs/42791819075) on the exact head: +- lint 153/153; +- `npm test` 695 tests (693 pass, 0 fail, 2 skipped); +- `npm run test:e2e` 182 tests (171 passed, 11 skipped, 0 failed; desktop 66 / mobile 66 / responsive 50). + +CI had been red on #37–#40 because one e2e test leaked state between projects; the fix is `02bfdba`. + +| Superseded PR | Branch (now tag `archive/`) | Head | State | +|---|---|---|---| +| #30 | feature/m4-3-upi-security-remediation | `e4babe6` | auto-marked merged by GitHub (head contained in #41) | +| #31 | feature/m4-3-1-review-remediation | `03bf518` | closed, superseded by #41 | +| #32 | feature/m4-3-2-financial-integrity | `62cb0a3` | closed, superseded by #41 | +| #33 | feature/m4-3-3-provider-verification | `4c54711` | closed, superseded by #41 | +| #34 | feature/m4-3-4-contract-decision | `2c930dd` | closed, superseded by #41 | +| #35 | feature/m5-telegram-operational-workflows | `43cf05a` | closed, superseded by #41 | +| #36 | feature/m6-organiser-workspace | `66ad026` | closed, superseded by #41 | +| #37 | feature/m7-attendee-experience | `5453ab1` | closed, superseded by #41 | +| #38 | feature/m8-realtime-notifications | `0b30221` | closed, superseded by #41 | +| #39 | feature/m9-testing-security-review | `b8c82c5` | closed, superseded by #41 | +| #40 | feature/m10-beta-readiness-hardening | `142cc62` (branch reached `02bfdba`) | closed, superseded by #41 | + +Earlier merges into `launch-mvp`: #23 `7c5079e`, #25 `83a30a7`, #26 `46b85c1`, #27 `69cafae`, #28 `c3b4e83`, #29 `c71ec8d`. + +Keep `ENABLE_LIVE_PAYMENTS=false` and `UPI_CHECKOUT_ENABLED=false` everywhere. ## 4. Migrations per environment -`npm run vercel-build` runs `scripts/migrate.js` on every Vercel build. `migrationPolicy()` (`lib/db.js:87`) only allows Preview builds to migrate when `MIGRATE_ON_PREVIEW=true` **and** the build is isolated (`ISOLATED_PREVIEW_DB=true` or `PREVIEW_DATABASE_URL` set). `migrate()` records applied files **by filename only** in `schema_migrations`, with no checksum, so an edited migration file is never re-applied. +`npm run vercel-build` runs `scripts/migrate.js` on every Vercel build. `migrationPolicy()` (`lib/db.js`) lets Preview builds migrate only when `MIGRATE_ON_PREVIEW=true` **and** isolation is proven. `migrate()` records applied files **by filename only** in `schema_migrations`. The tree on `launch-mvp` holds `003`–`020`; there is no `021`. | Environment | DB endpoint | Applied | Evidence | |---|---|---|---| -| **Local / CI** | ephemeral Postgres | whatever the checked-out tree has (`003`…`017` on `feature/m4-3-1-review-remediation`) | CI service container; fresh each run. | -| **Preview** (isolated) | `ep-green-hall-b47clnq0` (from earlier docs; the connection string was not read) | **`003` … `014`** (applied) | Applied via PR #23 push @ `d7836ff` (`dpl_AbjNDha7i298qjrh84FZZA3hHUQv`): `migration applied {"file":"014_location_remediation.sql"}`. | -| **Production** | `ep-dawn-glitter-b4mkwxkl` (from earlier docs; not re-verified) | **`003` … `009`** per its last build | Build log for `main` @ `5c6652a` (`dpl_3g7aUM3V…`, 2026-10-29): `[migrate] done (0 applied; vercel production)`; that tree holds `003`–`009`. | +| **Local / CI** | ephemeral Postgres | `003` … `020` | CI service container; fresh each run. | +| **Preview** (isolated) | `ep-green-hall-b47clnq0` (from earlier docs; the connection string was not read) | **`003` … `020`** | `launch-mvp` @ `771f6e1` build (`dpl_HRrw7oPWNomHhzUuszjpF96RC46s`, 2026-11-10): `[migrate] done (0 applied; vercel preview with MIGRATE_ON_PREVIEW=true and confirmed isolated database)`. That tree holds `003`–`020`, so all of them were already present (applied earlier by the stacked feature-branch Preview builds). | +| **Production** | **unknown**: `ep-dawn-glitter-b4mkwxkl` is unconfirmed (Gate 3) | **believed `003` … `009`** | Build log for `main` @ `5c6652a` (2026-10-29): `[migrate] done (0 applied; vercel production)`; that tree holds `003`–`009`. Not re-verified. The release will apply `010`–`020`: see [PRODUCTION_RELEASE_PACKET.md](launch/PRODUCTION_RELEASE_PACKET.md). | -> ℹ️ **Preview DB migrations.** Both `013_location_intelligence.sql` and `014_location_remediation.sql` are applied on the Preview database. `013` was preserved strictly unchanged; all fixes were applied via `014`. +> ⚠️ **Owner Gate 3 is still open.** See [PRODUCTION_DATABASE_VERIFICATION.md](launch/PRODUCTION_DATABASE_VERIFICATION.md). The block list (`DEFAULT_BLOCKED_PRODUCTION_ENDPOINTS` + `BLOCKED_PRODUCTION_ENDPOINTS` in `lib/config.js`) refuses listed endpoints for preview reviewer sign-in, seed/reset, preview writes and preview migrations. -> ⚠️ **Unreconciled — Production DB contents (Owner Gate 3).** See [PRODUCTION_DATABASE_VERIFICATION.md](launch/PRODUCTION_DATABASE_VERIFICATION.md) for full forensics. On 2026-10-29, build `dpl_3g7a...` applied migrations 003–009 to the configured database. The owner will provide the confirmed Production project ID and endpoint ID from Vercel → Storage before release. Until then, `ep-dawn-glitter-b4mkwxkl` is treated as unconfirmed/suspected. The codebase implements a dynamic production endpoint block list (via `DEFAULT_BLOCKED_PRODUCTION_ENDPOINTS` and `BLOCKED_PRODUCTION_ENDPOINTS` in `lib/config.js`), strictly refusing all listed endpoints across preview reviewer sign-in, seed/reset guards, preview writes, and preview migrations, verified by automated tests in `test/safety.test.js` and `test/migrations.test.js`. +> ⚠️ **Preview has never authenticated a user.** `SESSION_SECRET` (and the Telegram login vars) are missing on Preview, so every earlier "verified Preview" claim covered unauthenticated pages only. The owner steps are in [PREVIEW_LOGIN.md](launch/PREVIEW_LOGIN.md). ## 5. What is live vs Preview-only @@ -104,13 +98,14 @@ Rule: only the canonical workspace switches to `launch-mvp`. Milestone branches | # | Gate | Status | |---|---|---| | 1 | Approve merging PR #17 (`launch-mvp` → `main`) and the Production release | open | -| 2 | Production env vars. Missing as of P0 (2026-11-02, names only): `SESSION_SECRET`, `CRON_SECRET`, `TELEGRAM_BOT_USERNAME`, `TELEGRAM_WEBHOOK_SECRET`, `TG_CHAT_ID_EVENTS`, `ADMIN_BOOTSTRAP_TELEGRAM_ID`, `PUBLIC_SITE_URL`, `SUPPORT_EMAIL`, `LEGAL_ENTITY_NAME`, `GRIEVANCE_OFFICER_NAME`, `GRIEVANCE_EMAIL` (`ENABLE_LIVE_PAYMENTS` unset = off) | open, not re-checked in M1 | +| 2 | Production env vars. The full list (names only), including `ENABLE_LIVE_PAYMENTS=false` and `UPI_CHECKOUT_ENABLED=false`, is in [PRODUCTION_RELEASE_PACKET.md](launch/PRODUCTION_RELEASE_PACKET.md) §3 | open, not re-checked since P0 (2026-11-02) | | 3 | Reconcile which DB Production points at (§4) | open (owner will supply confirmed project ID & endpoint ID from Vercel → Storage; dynamic block list active) | -| 4 | Domain/DNS for `nightlifez.com` (Cloudflare, Vercel) | open (M8 prepares the steps in docs/launch/DNS_AND_EMAIL_ROUTING.md) | -| 5 | BotFather `/setdomain` and profile; Telegram groups and channel | open (M5 prepares the steps) | +| 4 | Domain/DNS for `nightlifez.com`: Vercel records **DNS-only (grey cloud)**, `www` → apex redirect in Vercel | open. Runbook corrected 2026-11-10: [DNS_AND_EMAIL_ROUTING.md](launch/DNS_AND_EMAIL_ROUTING.md) §2 | +| 5 | Telegram bots: a **test bot** for Preview (`/setdomain strangersmeetup-git-launch-mvp-jakuraa.vercel.app`) and the production bot (`/setdomain nightlifez.com`); groups and channel | open. Steps: [PREVIEW_LOGIN.md](launch/PREVIEW_LOGIN.md) | | 6 | Vercel Protection Bypass for Automation | reconciled in M10; pending owner token generation in Vercel and injection into GitHub Secrets (see docs/launch/REMAINING_OWNER_ACTIONS.md) | -| 7 | Email routing `contact@nightlifez.com` | open (M8 prepares the steps in docs/launch/DNS_AND_EMAIL_ROUTING.md) | +| 7 | Email routing `contact@nightlifez.com` (receive/forward only); DMARC starts at `p=none` | open. [DNS_AND_EMAIL_ROUTING.md](launch/DNS_AND_EMAIL_ROUTING.md) §3 | | 8 | Legal fields (entity, grievance officer) | open | +| 11 | **Preview env vars** so Preview can authenticate: `SESSION_SECRET`, `PREVIEW_TELEGRAM_BOT_TOKEN`, Preview-scoped `TELEGRAM_BOT_USERNAME` / `TELEGRAM_LOGIN_CLIENT_ID` / `TELEGRAM_LOGIN_CLIENT_SECRET`, `PREVIEW_PUBLIC_SITE_URL`, `ADMIN_BOOTSTRAP_TELEGRAM_ID` | open. Blocks the S4 authenticated Preview smoke. [PREVIEW_LOGIN.md](launch/PREVIEW_LOGIN.md) | | 9 | Approve the branch/worktree deletions proposed in §9 | **completed / approved** (19 fully merged remote branches archived to `archive/` tags and deleted from origin; `.claude/worktrees/launch-mvp` preserved as is) | | 10 | Decide on the unpushed Phase 5.6/5.7 reports (local commit `bcb4989`): push and archive, or drop | **completed / approved** (`bcb4989` pushed to `origin/fix/preview-recovery-audit`; reports imported into `docs/history/`) | diff --git a/docs/UPI_PROVIDER_READINESS.md b/docs/UPI_PROVIDER_READINESS.md index 7bd4ff9..16553b6 100644 --- a/docs/UPI_PROVIDER_READINESS.md +++ b/docs/UPI_PROVIDER_READINESS.md @@ -227,3 +227,14 @@ To transition payment processing from `ENABLE_LIVE_PAYMENTS=false` to live produ 2. **KYB & Commercial Contracting**: Complete merchant onboarding, bank nodal account binding, and production credential issuance. 3. **Webhook Whitelisting**: Register production webhook endpoints with HTTPS TLS termination. 4. **₹1 Financial Pilot**: Execute an end-to-end live ₹1 payment and subsequent refund under supervision on a dedicated staging branch before enabling production traffic. + +--- + +## 8. Note for any future PhonePe onboarding: the currency rule (2026-11-10, no code change) + +**Status:** UPI checkout stays off (`UPI_CHECKOUT_ENABLED=false`). This section changes no behaviour. It records a design consequence so it is not rediscovered later. + +- The verification engine requires the **provider payload** to state `currency === 'INR'` (§7.1.2). PhonePe's domestic v1 status and callback payloads never carry a `currency` field. So, **by design, no genuine PhonePe v1 payment can ever auto-confirm**: every one ends up `disputed` with `missing_currency`. This is not a bug in the adapter. It is the rule working exactly as written against a provider that cannot meet it. +- The rule was written to stop the adapter from *manufacturing* evidence (`currency || 'INR'`). That goal is right. But "the provider must echo the currency" is only one way to establish the INR invariant. For a domestic-only UPI merchant account, INR is fixed by the **signed merchant agreement and the merchant configuration**, not by each payload. +- **If PhonePe ever onboards the merchant**, revisit the rule before enabling anything. Derive the invariant from (a) the server-configured merchant ID matching the signed status response's merchant scope, (b) the merchant contract and configuration declaring domestic INR only, and (c) the exact-paise amount match. Write that derivation down explicitly in config, e.g. as a per-provider `currencyEvidence: 'contract'` rather than `'payload'`. Do not reintroduce a silent default. The change needs its own review, tests showing a missing payload currency is accepted **only** for a contract-INR provider, and an owner sign-off. +- Until then, Outcome B (§7.2) stands. diff --git a/docs/launch/DNS_AND_EMAIL_ROUTING.md b/docs/launch/DNS_AND_EMAIL_ROUTING.md index 9baa251..5878ecc 100644 --- a/docs/launch/DNS_AND_EMAIL_ROUTING.md +++ b/docs/launch/DNS_AND_EMAIL_ROUTING.md @@ -1,144 +1,130 @@ # NIGHTLIFEZ Launch Operations: DNS & Email Routing Runbook -**Target Milestone:** M8 Real-time Notifications & Operational Comms -**Scope:** Resolves Blocker #4 (Domain `nightlifez.com` DNS, CNAME flattening, SSL Full Strict) and Blocker #7 (Email routing `contact@nightlifez.com`). -**Status:** Operationally Prepared (Owner Execution Steps Gated for Production Window) ---- +**Scope:** domain `nightlifez.com` on Cloudflare DNS, served by Vercel; inbound mail for `contact@`, `safety@` and `grievance@`. +**Status:** owner-executed. The agent does not change DNS, Cloudflare or Vercel settings. +**Last checked against vendor docs:** 2026-11-10 (sources in §6). -## 1. Executive Summary +--- -This runbook provides the exact, production-ready specifications and manual execution checklist required to configure the apex domain `nightlifez.com`, subdomains, Cloudflare SSL/TLS encryption, and inbound email routing for `contact@nightlifez.com`. +## 1. Decisions (and why they changed) -All steps respect project boundaries: -- `ENABLE_LIVE_PAYMENTS=false` remains strictly enforced. -- Cloudflare credentials, DNS nameservers, and Production records are not modified automatically. -- This runbook serves as the authoritative, copy-paste ready blueprint for the domain administrator. +| Topic | Old instruction | Now | Reason | +|:---|:---|:---|:---| +| Vercel records | Proxied (orange cloud) + Cloudflare SSL `Full (Strict)` | **DNS only (grey cloud)**. No Cloudflare SSL/TLS, HSTS or redirect-rule setup for these hosts. | Vercel's guidance is "Use DNS-only mode for direct traffic": a proxy in front of Vercel reduces the connection signals its firewall and DDoS mitigation use, disables the Bot Protection managed ruleset, adds a second cache that Vercel deploys do not invalidate, and must keep certificate-validation paths open. The `Full (Strict)` steps only applied when proxying, so they are removed. [V1][V3] | +| Apex record | `CNAME @ → cname.vercel-dns.com` | **A `@` → the IP shown in Vercel → Project → Settings → Domains** | Vercel's documented apex record is an A record; values are project-specific, so copy them from the dashboard rather than this file. [V1] | +| `www` | CNAME `www → nightlifez.com` (proxied) + Cloudflare redirect rule | **CNAME `www` → the target shown in Settings → Domains**, and a redirect `www → apex` configured **in Vercel** | Vercel: if both apex and www are added, configure a redirect in project settings. [V1] | +| DMARC | `p=reject; sp=reject; pct=100` on day one | **`p=none` with `rua` reporting**, then `quarantine`, then `reject` | Start in monitor mode; tighten only once every real sender passes aligned SPF/DKIM. A day-one `reject` silently blocks the first legitimate sender added later (transactional mail, Google Workspace, a newsletter tool) before anyone notices. [C3][C4] | +| "Send as contact@" | Not addressed | **Not possible with Cloudflare Email Routing** (see §3.4) | Email Routing is receive/forward only. [C1] | --- -## 2. Blocker #4: Domain `nightlifez.com` & Cloudflare DNS Configuration - -### 2.1 Nameserver Delegation -Configure registrar (Namecheap / GoDaddy / Google Domains) nameservers to point to Cloudflare: -- Primary NS: `aria.ns.cloudflare.com` (or allocated Cloudflare account NS) -- Secondary NS: `dane.ns.cloudflare.com` (or allocated Cloudflare account NS) - -### 2.2 DNS Zone Records - -| Type | Name | Content / Target | Proxy Status | TTL | Purpose | -|:---|:---|:---|:---|:---|:---| -| **CNAME** | `@` (apex) | `cname.vercel-dns.com.` | Proxied (Orange Cloud) | Auto | Apex CNAME Flattening to Vercel production edge | -| **CNAME** | `www` | `nightlifez.com` | Proxied (Orange Cloud) | Auto | Subdomain redirect / canonical host | -| **CNAME** | `preview` | `cname.vercel-dns.com.` | DNS Only (Grey Cloud) | Auto | Dedicated Vercel preview staging endpoint | -| **A** | `_vercel` | `76.76.21.21` | DNS Only (Grey Cloud) | Auto | Vercel domain verification record (if requested) | - -### 2.3 Cloudflare SSL/TLS Settings -1. **Encryption Mode:** `Full (Strict)` - - Requires valid TLS certificate on origin (Vercel automatic Let's Encrypt). - - Mitigates man-in-the-middle attacks between Cloudflare Edge and Vercel Origin. -2. **Edge Certificates:** - - **Always Use HTTPS:** Enabled (`ON`). Automatically redirects `http://*` to `https://*` with 301. - - **HTTP Strict Transport Security (HSTS):** Enabled (`ON`). - - Max-Age: `31536000` (1 year). - - Include Subdomains: `ON`. - - Preload: `OFF` (enable only after 30 days of stable production traffic). - - **Minimum TLS Version:** `TLS 1.2` or `TLS 1.3`. - - **Opportunistic Encryption:** Enabled (`ON`). - - **TLS 1.3:** Enabled (`ON`). -3. **Page Rules / Redirect Rules (Apex Canonicalization):** - - URL Redirect Rule: `http*://www.nightlifez.com/*` -> `https://nightlifez.com/$1` (301 Permanent Redirect). +## 2. Web: Vercel records on Cloudflare DNS ---- +### 2.1 Nameservers +At the registrar, set the two nameservers Cloudflare assigned to the zone (shown on the zone's Overview page). -## 3. Blocker #7: Inbound Email Routing (`contact@nightlifez.com`) +### 2.2 Add the domains in Vercel first +Vercel → project `strangersmeetup` → Settings → Domains → add `nightlifez.com` and `www.nightlifez.com`. Choose **`nightlifez.com` as primary** and set `www.nightlifez.com` to **redirect (308) to `nightlifez.com`**. Vercel then shows the exact record values for each host. (Choosing www as primary instead is equally valid; just invert the redirect. Pick one and keep it.) -To receive platform inquiries, host applications, partnership requests, and safety grievance communications without running a dedicated mail server, Cloudflare Email Routing is utilized to forward inbound mail to the designated administrator inbox. +### 2.3 Cloudflare DNS records -### 3.1 Cloudflare Email Routing MX Records - -| Type | Name | Priority | Content / Mail Server | TTL | +| Type | Name | Content | Proxy status | Purpose | |:---|:---|:---|:---|:---| -| **MX** | `@` | `13` | `route1.mx.cloudflare.net` | Auto | -| **MX** | `@` | `57` | `route2.mx.cloudflare.net` | Auto | -| **MX** | `@` | `89` | `route3.mx.cloudflare.net` | Auto | +| **A** | `@` | value from Vercel → Settings → Domains (historically `76.76.21.21`; newer projects get a project-specific IP) | **DNS only (grey)** | apex → Vercel | +| **CNAME** | `www` | value from Vercel → Settings → Domains (e.g. `cname.vercel-dns.com` or a project-specific `*.vercel-dns-0xx.com`) | **DNS only (grey)** | www → Vercel, which then redirects to apex | +| **TXT** | `_vercel` | only if Vercel shows a verification TXT (domain already used by another Vercel account) | DNS only | ownership verification | -### 3.2 Sender Policy Framework (SPF) -Prevents email spoofing on the `nightlifez.com` domain. +Remove any pre-existing `AAAA` record on `@` or `www`. Do **not** add a CNAME at the apex. There is **no `preview.` subdomain**: Preview uses the stable branch alias `strangersmeetup-git-launch-mvp-jakuraa.vercel.app` (see `docs/launch/PREVIEW_LOGIN.md`). -| Type | Name | Content | TTL | -|:---|:---|:---|:---| -| **TXT** | `@` | `v=spf1 include:_spf.mx.cloudflare.net ~all` | Auto | +### 2.4 Cloudflare settings that must NOT be applied to these hosts +- No orange-cloud proxy, so no SSL/TLS mode, edge certificate, HSTS, "Always Use HTTPS" or Redirect Rules are needed for `@`/`www`. Vercel serves HTTPS and redirects HTTP→HTTPS itself. [V1] +- If the zone's SSL/TLS mode is ever changed for other proxied hosts, it does not affect grey-cloud records. -*Note:* If transactional sending (e.g., Postmark or Resend) is added in a subsequent phase, append `include:spf.postmarkapp.com` before `~all`. +### 2.5 Verify +```bash +dig +short A nightlifez.com # the Vercel IP from Settings → Domains, NOT 104.21.x / 172.67.x (those mean it is proxied) +dig +short CNAME www.nightlifez.com # the Vercel CNAME target +curl -sI http://nightlifez.com | head -3 # 308 → https://nightlifez.com/ +curl -sI https://www.nightlifez.com | head -3 # 308 → https://nightlifez.com/ +curl -s https://nightlifez.com/api/health # ok +vercel domains verify nightlifez.com --project strangersmeetup +``` +Vercel → Settings → Domains must show both hosts as **Valid Configuration**. -### 3.3 DomainKeys Identified Mail (DKIM) -Provided automatically by Cloudflare Email Routing when verification is active. +--- -| Type | Name | Content | TTL | -|:---|:---|:---|:---| -| **TXT** | `*._domainkey` | (As generated by Cloudflare Email Routing setup wizard) | Auto | +## 3. Email: inbound only, via Cloudflare Email Routing -### 3.4 Domain-based Message Authentication (DMARC) -Enforces rejection of fraudulent mail impersonating `nightlifez.com` and routes aggregate failure reports. +### 3.1 Enable +Cloudflare → Email Service → Email Routing → Onboard domain `nightlifez.com`. Cloudflare adds its own MX, SPF and DKIM records; **accept the values it generates** rather than typing them from this file. [C2] -| Type | Name | Content | TTL | -|:---|:---|:---|:---| -| **TXT** | `_dmarc` | `v=DMARC1; p=reject; sp=reject; pct=100; rua=mailto:contact@nightlifez.com` | Auto | +Expected shape (verify against what Cloudflare created): -### 3.5 Routing Rules -In Cloudflare Dashboard -> `nightlifez.com` -> `Email Routing` -> `Routing Rules`: +| Type | Name | Content | +|:---|:---|:---| +| MX | `@` | `route1/2/3.mx.cloudflare.net` (priorities as created) | +| TXT | `@` | `v=spf1 include:_spf.mx.cloudflare.net ~all` | +| TXT | `cf2024-1._domainkey` (or as created) | Cloudflare-generated DKIM key | -| Custom Address | Action | Destination Address | Status | -|:---|:---|:---|:---| -| `contact@nightlifez.com` | Send to | `@gmail.com` | Active | -| `safety@nightlifez.com` | Send to | `@gmail.com` | Active | -| `grievance@nightlifez.com` | Send to | `@gmail.com` | Active | -| `catch-all` | Drop | — | Disabled (Drop unexpected addresses) | +Only one SPF TXT record may exist on `@`. When a real sender is added later, edit that one record (e.g. add the provider's `include:`), never add a second. ---- +### 3.2 Destination and rules +1. Email Routing → Destination addresses → add the operations inbox → click the verification link. +2. Routing rules: `contact@`, `safety@`, `grievance@` → Send to → the verified inbox. Catch-all: Drop (or disabled). -## 4. Verification & Health Check Procedure +### 3.3 DMARC (start in monitor mode) -Run the following terminal commands to verify that DNS and Email records have propagated correctly: +| Type | Name | Content | +|:---|:---|:---| +| TXT | `_dmarc` | `v=DMARC1; p=none; rua=mailto:contact@nightlifez.com` | -### Check Apex Resolution -```bash -dig +short A nightlifez.com -# Should return Cloudflare Anycast IPs (e.g. 104.21.x.x, 172.67.x.x) -``` +Path to enforcement: +1. **Now:** `p=none`. Read the aggregate (`rua`) reports for 2–4 weeks. +2. **After every real sender (see §3.4) passes *aligned* SPF or DKIM in the reports:** `p=quarantine` (optionally `pct=25` → `100`). +3. **After a clean month at quarantine:** `p=reject`. -### Check CNAME Flattening & TLS -```bash -curl -svo /dev/null https://nightlifez.com -# Should complete TLS 1.3 handshake with HTTP 200 / 308 (Vercel) -``` +Note: forwarded mail can fail a *sender's* strict DMARC at the destination; Cloudflare mitigates with SRS and ARC but documents this limitation. [C1] -### Check Mail Exchanger (MX) Records +### 3.4 Can we send *as* `contact@nightlifez.com` for free? +- **Cloudflare Email Routing: no.** Cloudflare states that Email Routing "does not support sending or replying from your Cloudflare domain"; replies go out from the destination address (e.g. the Gmail inbox). [C1] +- **Cloudflare Email Sending** can send from the domain, but it is a **public beta on the Workers Paid plan**, not free for arbitrary recipients. [C2] +- **Gmail "Send mail as"** with Gmail's own servers: Gmail can label the From as `contact@nightlifez.com`, but the mail is signed by `gmail.com`, so DMARC for `nightlifez.com` will **not align**. Under `p=none` it is delivered, but deliverability is unreliable, and it becomes rejected once DMARC reaches `quarantine`/`reject`. Not recommended for anything important. +- **Recommended when sending is actually needed:** a provider that signs DKIM for `nightlifez.com` (e.g. Google Workspace mailbox, or a transactional service such as Resend/Postmark/Cloudflare Email Sending). Add its SPF include and DKIM records, confirm alignment in DMARC reports, then continue the §3.3 path. + +The product does not send email today; it notifies via Telegram. + +### 3.5 Verify ```bash dig +short MX nightlifez.com -# Expected: -# 13 route1.mx.cloudflare.net. -# 57 route2.mx.cloudflare.net. -# 89 route3.mx.cloudflare.net. +dig +short TXT nightlifez.com | grep v=spf1 # exactly one line +dig +short TXT _dmarc.nightlifez.com # "v=DMARC1; p=none; rua=mailto:contact@nightlifez.com" ``` +Then send a test mail from a different account to `contact@nightlifez.com` and confirm it arrives. -### Check SPF Record -```bash -dig +short TXT nightlifez.com | grep "v=spf1" -# Expected: "v=spf1 include:_spf.mx.cloudflare.net ~all" -``` +--- -### Check DMARC Policy -```bash -dig +short TXT _dmarc.nightlifez.com -# Expected: "v=DMARC1; p=reject; sp=reject; pct=100; rua=mailto:contact@nightlifez.com" -``` +## 4. Rollback +- Web: DNS changes are reversible by editing the records. Because records are grey-cloud, there is no proxy layer to roll back. +- Email: disabling Email Routing removes the MX records Cloudflare added; mail to the domain then bounces. +- DMARC: if legitimate mail is quarantined/rejected, step back to `p=none` immediately and fix alignment. --- -## 5. Rollback & Emergency Plan +## 5. Owner checklist +1. Vercel → Settings → Domains: add apex + www, apex primary, www → 308 apex. +2. Cloudflare DNS: A `@` and CNAME `www` with Vercel's values, **grey cloud**; remove conflicting AAAA/CNAME records. +3. Run §2.5 checks. +4. Cloudflare Email Routing: onboard, verify destination, add the three rules. +5. Add DMARC `p=none` with `rua`. +6. Run §3.5 checks. + +--- -If Cloudflare DNS proxying causes origin routing loops or header drops: -1. Switch apex `@` and `www` records from **Proxied** (Orange Cloud) to **DNS Only** (Grey Cloud) temporarily. -2. Verify direct resolution to Vercel edge. -3. Check Cloudflare SSL/TLS mode is set to **Full (Strict)**, not **Flexible** (which causes 301 redirection loops with HTTPS-enforcing origins like Vercel). +## 6. Sources +- [V1] Vercel — Set up a custom domain (A record for apex, CNAME for subdomains, redirect between apex and www): https://vercel.com/docs/domains/set-up-custom-domain +- [V2] Vercel — Troubleshooting domains (`dig` checks, HTTPS redirect): https://vercel.com/docs/domains/troubleshooting +- [V3] Vercel KB — Using Cloudflare and other reverse proxies with Vercel (updated 2026-10-19; "Why is another CDN or proxy in front of Vercel not recommended?", "Use DNS-only mode for direct traffic"): https://vercel.com/kb/guide/can-i-use-a-proxy-on-top-of-my-vercel-deployment +- [C1] Cloudflare — Email Service Postmaster, Known limitations ("Sending or replying to an email from your Cloudflare domain"; restrictive DMARC and forwarding; SRS): https://developers.cloudflare.com/email-service/reference/postmaster/ +- [C2] Cloudflare — Email Service overview (Email Sending beta on Workers Paid; Email Routing on Free/Paid; onboarding adds MX/SPF/DKIM): https://developers.cloudflare.com/email-service/ and https://developers.cloudflare.com/email-service/get-started/route-emails/ +- [C3] Cloudflare — Email authentication, DMARC deployment strategy (`p=none` → `quarantine` → `reject`): https://developers.cloudflare.com/email-service/concepts/email-authentication/ +- [C4] Cloudflare — DNS: Set up Google Workspace, DMARC ("Start with `p=none`"): https://developers.cloudflare.com/dns/manage-dns-records/how-to/set-up-google-workspace/ diff --git a/docs/launch/PREVIEW_LOGIN.md b/docs/launch/PREVIEW_LOGIN.md new file mode 100644 index 0000000..e3692bc --- /dev/null +++ b/docs/launch/PREVIEW_LOGIN.md @@ -0,0 +1,45 @@ +# Preview login: two Telegram bots, one login domain each + +**Status:** config only. No code change was needed; this records which env names the code reads and the exact owner steps. +**Why:** a Telegram bot's login is bound to the domain(s) registered with @BotFather. Preview and Production live on different hosts, so they need **two bots**. Sharing the production bot with Preview would also let Preview message real users. + +| | Production | Preview | +|---|---|---| +| Bot | production bot (e.g. `@NightlifezBot`) | **separate test bot** (e.g. `@NightlifezTestBot`) | +| Login domain (`/setdomain` + Web Login allowed URLs) | `nightlifez.com` | `strangersmeetup-git-launch-mvp-jakuraa.vercel.app` (the **stable branch alias** for `launch-mvp`) | +| Callback | `https://nightlifez.com/auth/telegram/callback` | `https://strangersmeetup-git-launch-mvp-jakuraa.vercel.app/auth/telegram/callback` | + +Use the branch alias, never a per-deployment URL (`strangersmeetup--jakuraa.vercel.app`): those change on every push and would break login after each deploy. + +## What the code reads (verified 2026-11-10) + +| Purpose | Env name(s) | Where | Preview behaviour | +|---|---|---|---| +| Bot token (sending, webhook) | `PREVIEW_TELEGRAM_BOT_TOKEN`, else `TELEGRAM_TEST_BOT_TOKEN` | `lib/config.js` `botToken()` | When `VERCEL_ENV=preview` **only** these are used. `TELEGRAM_BOT_TOKEN` is ignored unless `ALLOW_PREVIEW_TELEGRAM=true` (keep that unset). No valid token means Telegram sending is disabled on Preview (fail-closed). | +| Bot username (login button, deep links) | `TELEGRAM_BOT_USERNAME` | `lib/config.js` `botUsername()` | Same name in every environment, so set a **Preview-scoped value** = the test bot's username. | +| Login (OIDC) client | `TELEGRAM_LOGIN_CLIENT_ID`, `TELEGRAM_LOGIN_CLIENT_SECRET` | `lib/telegram/auth.js` | Same names in every environment, so set **Preview-scoped values** = the test bot's Web Login client ID/secret. The ID token `aud` must equal this client ID. | +| Callback base URL | `PREVIEW_PUBLIC_SITE_URL`, else `VERCEL_URL` | `lib/config.js` `siteUrl()`, `lib/handlers/auth.js` `redirectUri()` | **Must be set** to `https://strangersmeetup-git-launch-mvp-jakuraa.vercel.app`. Otherwise the callback uses the per-deployment `VERCEL_URL`, which never matches the bot's registered domain. | +| Sessions | `SESSION_SECRET` (≥ 32 chars) | `lib/session.js` | Without it every authenticated route returns 503. This is why Preview has never authenticated a user. Use a value different from Production. | +| First super_admin | `ADMIN_BOOTSTRAP_TELEGRAM_ID` | `lib/handlers/admin.js` | Owner's numeric Telegram user ID. The first visit to `/admin` by that user bootstraps `super_admin` and writes an audit row. | +| QR ticket signing | `TICKET_SECRET`, else `SESSION_SECRET` | `lib/services/tickets.js` | Optional on Preview, because `SESSION_SECRET` covers it. | + +Production reads `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_USERNAME`, `TELEGRAM_LOGIN_CLIENT_ID/SECRET` and `PUBLIC_SITE_URL=https://nightlifez.com`, all Production-scoped. + +## Owner steps (in order) + +1. **Create the test bot:** @BotFather → `/newbot` → e.g. `NightlifezTestBot`. Copy its token. +2. **Bind the test bot's login domain:** + - `/setdomain` → test bot → `strangersmeetup-git-launch-mvp-jakuraa.vercel.app` + - Bot Settings → Web Login → allowed URLs: `https://strangersmeetup-git-launch-mvp-jakuraa.vercel.app` and `https://strangersmeetup-git-launch-mvp-jakuraa.vercel.app/auth/telegram/callback`. Copy the **Client ID** and **Client Secret**. +3. **Vercel → Settings → Environment Variables, scope = Preview** (optionally restricted to branch `launch-mvp`): + - `PREVIEW_TELEGRAM_BOT_TOKEN` = test bot token + - `TELEGRAM_BOT_USERNAME` = test bot username (no `@`) + - `TELEGRAM_LOGIN_CLIENT_ID` / `TELEGRAM_LOGIN_CLIENT_SECRET` = the test bot's values + - `PREVIEW_PUBLIC_SITE_URL` = `https://strangersmeetup-git-launch-mvp-jakuraa.vercel.app` + - `SESSION_SECRET` = `openssl rand -hex 32` (a Preview-only value) + - `ADMIN_BOOTSTRAP_TELEGRAM_ID` = your numeric Telegram ID + - Confirm **unset** on Preview: `ALLOW_PREVIEW_TELEGRAM`, `ENABLE_LIVE_PAYMENTS`, `UPI_CHECKOUT_ENABLED`, `ALLOW_PREVIEW_PAID_CHECKOUT`. +4. **Production bot (at launch, not now):** `/setdomain` → production bot → `nightlifez.com`; Web Login allowed URLs → `https://nightlifez.com` and `https://nightlifez.com/auth/telegram/callback`. +5. **Redeploy** the `launch-mvp` Preview (env changes apply only to new deployments). +6. **Check:** `curl -s https://strangersmeetup-git-launch-mvp-jakuraa.vercel.app/api/health` (with the protection-bypass header if Deployment Protection is on). `session`, `telegramBot` and `telegramLogin` should read `configured`. +7. *(Optional, enables an agent-run smoke)* Vercel → Settings → Deployment Protection → **Protection Bypass for Automation**. Share the secret through a channel you trust; the agent sends it as `x-vercel-protection-bypass`. diff --git a/docs/launch/PRODUCTION_RELEASE_PACKET.md b/docs/launch/PRODUCTION_RELEASE_PACKET.md new file mode 100644 index 0000000..91d1f64 --- /dev/null +++ b/docs/launch/PRODUCTION_RELEASE_PACKET.md @@ -0,0 +1,86 @@ +# NIGHTLIFEZ Production Release Packet + +**Status:** prepared, not executed. The agent does not deploy to Production, touch the Production DB, or change Vercel env/DNS/BotFather. +**Prepared:** 2026-11-10 against `launch-mvp` @ `771f6e1` (+ PR #42, S3a payment flags, which must be merged before release). +**Production today:** running `5c6652a` (29 Oct). Its migration level is **believed** to be 009; unverified until Gate 3. + +--- + +## 1. Migrations that will run on Production + +The Vercel build runs `npm run vercel-build` → `scripts/migrate.js`. On `VERCEL_ENV=production` it applies every pending file in `migrations/` in filename order, once each, tracked in `schema_migrations` (`lib/db.js` `migrationPolicy`, `migrate`). Preview builds do not migrate unless `MIGRATE_ON_PREVIEW=true` and isolation is proven. + +If Production is at 009, the release applies **010 → 020 (11 files)**. There is no 021. + +| # | Purpose | Touches existing rows? | Pre-check (owner, read-only, before merge) | +|---|---|---|---| +| 010 | SuperProfile checkout: `external_orders`, booking source/payment-mode checks, user claim columns | **Yes: replaces CHECK `bookings_payment_mode`** with `payment_mode IS NULL OR IN ('free','provider','external')`; adds CHECK `bookings_source IN ('site','superprofile')` | `SELECT payment_mode, count(*) FROM bookings GROUP BY 1;` must show only NULL/free/provider/external. If `source` already exists: `SELECT source, count(*) FROM bookings GROUP BY 1;` | +| 011 | Safety reports: category, event/user refs; attendee blocking | No (columns with defaults) | — | +| 012 | Identities, organisations, memberships, capabilities, venues, event teams | **Yes: backfills** `user_identities` from `users`, a personal org + owner membership + `organise_events` capability for every organiser, and **UPDATEs `events.organiser_org_id`** | `SELECT count(*) FROM users;` `SELECT count(*) FROM events WHERE organiser_user_id IS NOT NULL;` (for after-checks) | +| 013 | LGD location directory (states/districts/cities) | Inserts reference data into new tables only | — | +| 014 | Coordinate provenance + location-resolution trigger on `events` | **Yes: UPDATE `events.coords_provenance='city'`** where lat/lng are set; the trigger fires on future event writes | `SELECT count(*) FROM events WHERE lat IS NOT NULL;` | +| 015 | UPI payment columns/enums, audit ledger | No (additive; enum values added) | — | +| 016 | `payment_audits` append-only trigger | No | — | +| 017 | Index for tier payment holds | No | — | +| 018 | Telegram link tokens, notification prefs, quiet hours | No | — | +| 019 | Event staff, `tickets`, QR check-in, announcements | **Yes: inserts one `tickets` row per confirmed/checked-in booking** that has none | `SELECT status, count(*) FROM bookings GROUP BY 1;` | +| 020 | Saved events, discovery indexes | No | — | + +Notes: +- Every file is written to be idempotent (`IF NOT EXISTS`, `ON CONFLICT DO NOTHING`, guarded `DO` blocks). A failed migration fails the build, so the previous Production deployment keeps serving. +- Migrations run inside the build, **before** the new code is live. Old code runs against the new schema briefly; all changes are additive, so that is safe. +- 019's backfill sets `tickets.security_hash = md5('nlz_seed_sig_'||booking_id)`. That column is **not** used for check-in: QR codes are verified by HMAC-SHA256 keyed by `TICKET_SECRET`/`SESSION_SECRET` (`lib/services/tickets.js`). It is harmless, but it is dead data. + +## 2. Gate 3: confirm the Production database endpoint + +Why: Preview tooling refuses any DB URL whose Neon endpoint is on the blocklist. Today the list holds only `ep-dawn-glitter-b4mkwxkl`, the historical, *unconfirmed* endpoint (`lib/config.js` `DEFAULT_BLOCKED_PRODUCTION_ENDPOINTS`). + +1. **Owner:** Vercel → project → Storage → the Production database → note the Neon **project ID** and **endpoint ID** (`ep-…`, also visible in the host of the Production `DATABASE_URL`). Send **only the IDs**, never the URL or password. +2. **Agent:** add the endpoint ID to `DEFAULT_BLOCKED_PRODUCTION_ENDPOINTS` in `lib/config.js`, with a test asserting a URL with that host is refused by `isBlockedProductionDatabaseUrl`, `migrationPolicy` (Preview), the seed guard and `previewReviewerEnabled`. Open a PR into `launch-mvp`. (Optionally the owner also sets `BLOCKED_PRODUCTION_ENDPOINTS=` on Preview; the code merges both lists.) +3. **Owner:** confirm the Preview `DATABASE_URL`/`PREVIEW_DATABASE_URL` host is a *different* endpoint. +4. **Owner, read-only:** `SELECT name FROM schema_migrations ORDER BY name;` on Production, to confirm the level is 009 before the release. + +## 3. Production environment variables (names only; scope = Production) + +| Name | Value / rule | +|---|---| +| `DATABASE_URL` | Production Neon URL (Gate 3 endpoint) | +| `SESSION_SECRET` | ≥ 32 chars, Production-only. **Required:** without it sessions return 503, and ticket QR signing falls back to a hard-coded dev secret | +| `TICKET_SECRET` | recommended, ≥ 32 chars; separates QR signing from sessions | +| `CRON_SECRET` | ≥ 16 chars | +| `PUBLIC_SITE_URL` | `https://nightlifez.com` | +| `TELEGRAM_BOT_TOKEN` | production bot | +| `TELEGRAM_BOT_USERNAME` | production bot username, no `@` | +| `TELEGRAM_WEBHOOK_SECRET` | `[A-Za-z0-9_-]{16,256}` | +| `TELEGRAM_LOGIN_CLIENT_ID` / `TELEGRAM_LOGIN_CLIENT_SECRET` | production bot's Web Login client | +| `TG_CHAT_ID_EVENTS` | events channel/group ID | +| `ADMIN_BOOTSTRAP_TELEGRAM_ID` | owner's numeric Telegram ID | +| `SUPERPROFILE_WEBHOOK_SECRET` | ≥ 24 chars | +| `ENABLE_LIVE_PAYMENTS` | **`false`** (gates the SuperProfile link-out only) | +| `UPI_CHECKOUT_ENABLED` | **`false`** (gates every UPI/PhonePe route; see S3a) | +| `SUPPORT_EMAIL`, `GRIEVANCE_EMAIL`, `GRIEVANCE_OFFICER_NAME`, `LEGAL_ENTITY_NAME` | legal/contact copy | +| optional | `SOCIAL_PROOF_MIN` (≥ 5), `PG_POOL_MAX`, `DISABLE_WEB_ANALYTICS` | + +Must be **absent** in Production: `MIGRATE_ON_PREVIEW`, `ISOLATED_PREVIEW_DB`, `PREVIEW_*`, `TELEGRAM_TEST_*`, `ALLOW_PREVIEW_*`, `IS_PREVIEW`, `UPI_ADAPTER`, `FORCE_PHONEPE`, `PHONEPE_*` (UPI stays off), `Telegram_HTTP_API` (legacy name). + +Check after deploy: `GET /api/health` → every `config` entry `configured`, `livePayments: false`. + +## 4. Release steps (owner, in order) + +1. Gates done: DNS (`DNS_AND_EMAIL_ROUTING.md` §5), Gate 3 (§2 above), Production env (§3), the production bot's `/setdomain nightlifez.com` + Web Login URLs (`PREVIEW_LOGIN.md` step 4). +2. Run the §1 pre-checks (read-only) and keep the outputs. +3. Merge **PR #17** (`launch-mvp` → `main`) with CI green on its head SHA. +4. Watch the Production build log: expect `[migrate] done (11 applied; vercel production)` (or fewer if Production is past 009). Any `[migrate] failed` stops the release; the old deployment keeps serving. +5. `curl -s https://nightlifez.com/api/health` → DB ok, config all `configured`, `version` = the first 7 chars of the merge SHA. +6. Register the webhook: `curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" -d url=https://nightlifez.com/api/telegram/webhook -d secret_token=$TELEGRAM_WEBHOOK_SECRET`, then `getWebhookInfo` (no `last_error_message`). +7. Production smoke: owner Telegram login → `/admin` bootstraps `super_admin` (audit row) → publish a free test event → book from a second account → QR pass → staff scan → cancel → seat released → unpublish the test event. +8. After-checks for the §1 backfills: identity count = user count; every booking with status confirmed/checked_in has a ticket. + +## 5. Rollback + +- **Code:** Vercel → Deployments → previous Production deployment (`5c6652a`) → *Instant Rollback*; or revert the merge commit on `main` and redeploy. +- **Schema:** do **not** roll migrations back. 010–020 are additive, and the old code ignores the new tables/columns/indexes/triggers. Two exceptions worth knowing: + - 010's stricter `bookings_payment_mode` CHECK also applies to old code. Old code writes only free/provider values, which the CHECK allows. + - 016's append-only trigger blocks UPDATE/DELETE on `payment_audits`. Old code never writes that table. +- **Data:** if a backfill is wrong, fix forward with a new migration. Never hand-edit Production. +- **Webhook:** re-run `setWebhook` with the previous URL if the domain changes back.