# Teleton Rifa: Deploy Runbook (Neon + Vercel)

How to stand up the raffle app on Neon (Postgres) and Vercel (hosting). Written against the actual codebase as built (Next.js 16 App Router, Drizzle over postgres.js, the env vars in `.env.example`).

No em-dashes and no emojis in this document, because parts of it will be lifted into client and provider communications.

## Read this first

Deploying the app does NOT make the raffle legally live. Two separate things:

1. The software running on a URL. This runbook.
2. The legal right to sell paid raffle chances. The MININTER authorization chain in `docs/legal.md` and the go/no-go gates in `PLAN.md` section 14.1. The app gates the DRAW on a resolution reference, but YOU must not open paid SALES until the resolution is in hand (risk R19).

So there are two tracks below. Do Track A now to get a shareable demo in front of Rodrigo. Do Track B only when the commercial and legal gates have cleared and you have real payment credentials.

- Track A, Demo preview: Neon free tier, Vercel, seeded data, Turnstile and Culqi in test mode. No real money moves. About 30 minutes. This is the asset that helps close the deal.
- Track B, Production: everything in A plus real Culqi keys, a sandbox-tested money loop, the real domain, verified email, hardened admin auth, and the legal authorization.

---

## 0. Accounts and tools you need

- A Neon account (neon.tech). Free tier is enough for Track A.
- A Vercel account. Hobby works for the demo. Production spike likely wants Pro (longer function durations, more cron frequency).
- A GitHub account (Vercel deploys from a git repo).
- Local tools: Node 20 or 22, npm, git, the GitHub CLI `gh` (optional but used below), and `openssl` (for secrets).
- Track B only: a Culqi account on Teleton's RUC, a Cloudflare Turnstile widget, an Upstash Redis database, a Resend account with a verified Teleton domain, and DNS control for `teleton.pe`.

---

## 1. Make the app its own git repo (do NOT push the vault)

The app currently lives at `projects/teleton/` inside Sebastian's private bimbiOS vault. The vault holds gitignored personal data (identity, mentees, journal) and must never be pushed to a public host. Vercel deploys from a repo, so give the app its OWN repo.

```bash
cd /Users/sebasbimbi/sebastian-bimbi/projects/teleton

# The app already has a correct .gitignore (node_modules, .next, .env*, *.tsbuildinfo).
git init
git add .
git commit -m "Teleton rifa: initial app"

# Create a PRIVATE GitHub repo and push (charity money app, keep it private).
gh repo create teleton-rifa --private --source=. --remote=origin --push
```

Optional, to silence git's "embedded repository" warning in the vault: add `projects/teleton/` to the vault's top-level `.gitignore`. The app is a separate project; the vault does not need to track its source.

---

## 2. Neon: create the project and get two connection strings

1. In the Neon console, create a project (region close to Peru, for example AWS us-east-2 or us-east-1). Name it `teleton-rifa`.
2. Neon gives you a connection string. You need TWO shapes of it:
   - Pooled (runtime): the host contains `-pooler`. This is the transaction-mode pooler, the spike-survival path (architecture.md section 7.2). This is `DATABASE_URL`.
   - Direct (migrations): the plain host without `-pooler`. This is `DIRECT_URL` (drizzle-kit uses it; `drizzle.config.ts` prefers `DIRECT_URL`).
3. Always keep `?sslmode=require` on Neon URLs.

Example shapes (yours will differ):

```
DATABASE_URL="postgresql://USER:PASSWORD@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"
DIRECT_URL="postgresql://USER:PASSWORD@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require"
```

The code is provider-agnostic. Stay on `postgres.js` (already wired in `lib/db/index.ts`, `prepare:false`). Do NOT switch to `drizzle-orm/neon-http`: the webhook mint path runs a `SELECT ... FOR UPDATE` inside a transaction, which the HTTP driver cannot do. If you ever want a Neon-native driver, use `drizzle-orm/neon-serverless` (Pool), not `neon-http`.

---

## 3. Create the schema and seed data (run locally against Neon)

Do this once from your machine, pointed at the new Neon database.

```bash
cd /Users/sebasbimbi/sebastian-bimbi/projects/teleton
cp .env.example .env.local
# Edit .env.local: set DATABASE_URL and DIRECT_URL to the Neon strings from step 2.

# Create all tables from the schema (includes the 0001 draw-hardening columns).
npm run db:push

# Insert the two raffles, placeholder prizes, and the flat S/5 pricing tiers.
npm run db:seed
```

Notes:
- `db:push` syncs the current `lib/db/schema.ts` directly. For a stricter, versioned audit trail you can instead apply the committed migrations (`drizzle/0000_*.sql` then `0001_draw_hardening.sql`) with `npx drizzle-kit migrate`; add a `db:migrate` script if you prefer that path. Either gives the same final schema.
- The seed is idempotent (upsert by raffle slug), so re-running is safe.
- After this, the landing page will show real seeded prizes and the countdown instead of placeholders.

Quick local check before deploying:

```bash
npm run dev
# open http://localhost:3000  -> landing renders with seeded data
# open http://localhost:3000/admin/login -> admin auth screen
```

---

## 4. Import the repo into Vercel

1. Vercel dashboard, Add New, Project, import `teleton-rifa` from GitHub.
2. Framework preset auto-detects Next.js. Leave Build Command and Output as default.
3. Set the Node version to 20 or 22 in Project Settings, Build and Deployment (or add `"engines": { "node": ">=20" }` to `package.json`).
4. Do NOT deploy yet. Add the environment variables (step 5) first, otherwise the first build has no database to reach at request time.

---

## 5. Environment variables in Vercel

Project Settings, Environment Variables. Add these. Set them for Production AND Preview (Preview is your Rodrigo demo surface). Use Neon branch URLs for Preview if you want isolation (step 12).

Generate the two app secrets first:

```bash
openssl rand -hex 32   # use for ADMIN_SESSION_SECRET
openssl rand -hex 32   # use for CRON_SECRET
```

### Required for the app to run at all

| Variable | Value | Notes |
|---|---|---|
| `DATABASE_URL` | Neon POOLED string | runtime, `-pooler` host, `?sslmode=require` |
| `DIRECT_URL` | Neon DIRECT string | migrations only; safe to include |
| `NEXT_PUBLIC_APP_URL` | `https://rifa.teleton.pe` (or the Vercel preview URL for Track A) | used for absolute links and OG |
| `ADMIN_SESSION_SECRET` | `openssl rand -hex 32` | signs the admin cookie |
| `ADMIN_ALLOWLIST` | `ops@teleton.pe:admin,sebastian@bimbi...:admin` | comma-separated `email:role`, roles `viewer|operator|draw_officer|admin` |

### Money path (Culqi primary)

| Variable | Value | Notes |
|---|---|---|
| `PAYMENT_PROVIDER` | `culqi` | flip to `mercadopago` to fail over |
| `CULQI_PUBLIC_KEY` | Culqi public key | TEST key for Track A, LIVE for Track B |
| `CULQI_SECRET_KEY` | Culqi secret key | server-side only |
| `CULQI_WEBHOOK_SECRET` | Culqi webhook signing secret | required to verify webhooks |

### Mercado Pago (fallback, optional until you need failover)

| Variable | Value |
|---|---|
| `MERCADOPAGO_ACCESS_TOKEN` | MP access token |
| `MERCADOPAGO_PUBLIC_KEY` | MP public key |
| `MERCADOPAGO_WEBHOOK_SECRET` | MP webhook secret |

### Abuse control (Cloudflare Turnstile + Upstash)

| Variable | Value | Notes |
|---|---|---|
| `TURNSTILE_SECRET_KEY` | Turnstile secret | server verification |
| `TURNSTILE_SITE_KEY` | Turnstile site key | read server-side and passed as a prop |
| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | the SAME site key | read by the client widget |
| `UPSTASH_REDIS_REST_URL` | Upstash REST URL | rate limiting; if absent, limiter fails open in dev |
| `UPSTASH_REDIS_REST_TOKEN` | Upstash REST token | |

IMPORTANT (known inconsistency to clean up later): the Turnstile site key is read under two names. The client component uses `NEXT_PUBLIC_TURNSTILE_SITE_KEY`; the server path uses `TURNSTILE_SITE_KEY`. Set BOTH to the same value so the captcha works. For Track A you can use Cloudflare's always-passing test keys (site `1x00000000000000000000AA`, secret `1x0000000000000000000000000000000AA`).

### Email (Resend)

| Variable | Value | Notes |
|---|---|---|
| `RESEND_API_KEY` | Resend API key | |
| `RESEND_FROM_EMAIL` | `rifa@teleton.pe` | the domain must be verified in Resend (step 9) |

### Draw randomness (drand) and seed

| Variable | Value | Notes |
|---|---|---|
| `DRAND_CHAIN_HASH` | leave blank, or pin `52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` | defaults to League of Entropy quicknet; pinning it in prod is good hygiene |
| `DRAND_API_URL` | leave blank, or `https://api.drand.sh` | defaults correctly; relays api2/api3 are built in |
| `DRAW_SERVER_SEED` | leave blank | declared but NOT used; the real per-draw seed is generated at commit time. Do not rely on this. |

### Cron protection

| Variable | Value | Notes |
|---|---|---|
| `CRON_SECRET` | `openssl rand -hex 32` | Vercel automatically sends `Authorization: Bearer $CRON_SECRET` to cron routes; the reconcile route checks it |

`NODE_ENV` is set by Vercel automatically. Do not add it.

---

## 6. Add `vercel.json` (cron + function durations)

The nightly reconciliation needs a Vercel Cron entry, and the money and draw routes want a longer timeout than the default. Create `vercel.json` at the repo root:

```json
{
  "crons": [
    { "path": "/api/cron/reconcile", "schedule": "0 6 * * *" }
  ],
  "functions": {
    "app/api/webhooks/[provider]/route.ts": { "maxDuration": 30 },
    "app/api/cron/reconcile/route.ts": { "maxDuration": 60 },
    "app/api/admin/raffles/[id]/draw/reveal/route.ts": { "maxDuration": 60 }
  }
}
```

Notes:
- `0 6 * * *` is 06:00 UTC daily (01:00 in Peru). Adjust to taste.
- Hobby plan allows one cron run per day and caps function duration. The nightly reconcile fits Hobby. The minutes-fresh reconciliation used on draw day is triggered from the admin draw stepper (`reconcileNow`), not from cron, so it does not depend on cron frequency.
- For the September spike and longer durations, move to Pro.

Commit and push; Vercel will pick it up on the next deploy.

---

## 7. First deploy and the custom domain

1. Trigger a deploy (push to the default branch, or Deploy in the Vercel UI).
2. Confirm the build succeeds and the preview URL renders the landing with seeded data.
3. Custom domain (Track B): in Vercel, Project Settings, Domains, add `rifa.teleton.pe`. Vercel shows a CNAME target. Teleton (or its web vendor) must create the CNAME on `teleton.pe` DNS pointing `rifa` at Vercel. This is the standalone first-party origin the architecture requires (architecture.md section 5); do not serve the checkout from a cross-origin iframe. Update `NEXT_PUBLIC_APP_URL` to `https://rifa.teleton.pe` once the domain resolves.

---

## 8. Register the payment webhooks (Track B)

The webhook is the single source of truth for minting tickets. After the domain is live:

1. Culqi dashboard, Webhooks, add an endpoint: `https://rifa.teleton.pe/api/webhooks/culqi`. Copy the signing secret into `CULQI_WEBHOOK_SECRET`.
2. Mercado Pago (if used), add: `https://rifa.teleton.pe/api/webhooks/mercadopago`. Set `MERCADOPAGO_WEBHOOK_SECRET`.
3. Subscribe to the charge succeeded and refund events for each provider.
4. Redeploy so the new secrets load.

There are `// SANDBOX:` markers in `lib/payments/culqi.ts` and `lib/payments/mercadopago.ts`. The provider calls are written to the documented APIs but have NEVER been tested against a live gateway. Step 11 is where you prove the loop end to end before opening sales.

---

## 9. Verify the email domain (Track B)

1. In Resend, add and verify `teleton.pe` (or a subdomain like `rifa.teleton.pe`).
2. Teleton DNS adds the SPF, DKIM, and DMARC records Resend provides.
3. Set `RESEND_FROM_EMAIL` to an address on the verified domain.
4. Send a test confirmation and confirm it lands in inbox, not spam.

---

## 10. Create the Turnstile widget (Track B)

1. Cloudflare, Turnstile, add a widget for `rifa.teleton.pe`.
2. Put the site key in BOTH `TURNSTILE_SITE_KEY` and `NEXT_PUBLIC_TURNSTILE_SITE_KEY`, and the secret in `TURNSTILE_SECRET_KEY`.
3. Redeploy.

---

## 11. Post-deploy smoke test (do this before opening any sales)

Run the full money loop in a sandbox first. This is the single most important pre-launch test.

1. Open the deployed URL on a real phone. Register (name, phone, email), accept the unticked consent.
2. Choose an upsell tier. Confirm totals and odds are visible and the decline is equal weight.
3. Pay with a Culqi TEST card or test Yape. Confirm the Culqi popup opens.
4. After paying, confirm the `/gracias` page polls and then shows confirmed (driven by the webhook, not the browser return).
5. In the admin panel, confirm exactly N tickets minted, the purchase is `paid`, and the audit log has the entries.
6. Retry the webhook (resend the event from Culqi) and confirm NO double mint (idempotency).
7. Trigger a refund from admin and confirm the tickets are voided.
8. Run the loop again INSIDE the Instagram in-app browser and the TikTok in-app browser. This is where mobile payment flows break; the "abrir en tu navegador" escape hatch is there for this.
9. Hit `/api/cron/reconcile` with the `Authorization: Bearer <CRON_SECRET>` header and confirm it reports clean.

Only after all nine pass should you consider opening real sales, and only once the MININTER resolution reference is attached in admin.

---

## 12. Preview workflow (your Rodrigo demo surface)

- Every push to a non-default branch gets a Vercel Preview URL. Use Neon branching to give each preview its own database: create a Neon branch, set the Preview `DATABASE_URL` and `DIRECT_URL` to the branch URLs. Seed the branch once. Now `git push` to a branch gives you a fully working, isolated demo link to send Rodrigo, with zero risk to production data.
- Neon scale-to-zero means these branches cost almost nothing while idle, which fits this project's long dormant periods between the freeze, the August warm-up, and September 12.

---

## 13. Known blockers before this touches a real sol

These are not deploy steps; they are gates the deploy cannot close for you.

1. Real Culqi credentials on Teleton's RUC, and the step 11 loop tested against the live gateway (the provider calls are sandbox-marked, never gateway-tested).
2. Confirm in writing whether Yape via Culqi is subject to the S/3.50 minimum fee. It changes the viable price and the hero copy (PLAN.md section 5.1, risk R17). Do not freeze pricing until this is answered.
3. Harden admin auth. The current session is a signed cookie plus an env allowlist; review it before it guards real money and PII.
4. The MININTER authorization and matching bases (docs/legal.md). Gate SALES, not just the draw, on the resolution (risk R19).
5. Cosmetic follow-ups: rename `middleware.ts` to `proxy.ts` (Next 16 deprecation, still works), and regenerate the drizzle meta snapshot after the 0001 columns.

---

## 14. Draw-day notes (September 12)

- The draw is commit then reveal, bound to drand quicknet. The admin draw stepper hard-gates commit and reveal on: a MININTER resolution reference attached, a government representative present confirmation, and a minutes-fresh reconciliation that is green.
- Pre-cache and verify the target drand round before going live; the stepper carries an on-camera holding script for a late round. A half-revealed draw is voided, not patched.
- After the draw, the verification bundle (server seed, drand round, randomness, signature, frozen pool hash, prize snapshot) lets any third party re-run `verifyDraw` and reproduce the exact winners. Export it and publish it.

---

## Quick reference: the fastest path to a demo link

```bash
# 1. own repo
cd /Users/sebasbimbi/sebastian-bimbi/projects/teleton
git init && git add . && git commit -m "Teleton rifa: initial app"
gh repo create teleton-rifa --private --source=. --remote=origin --push

# 2. Neon: create project, copy pooled + direct URLs

# 3. schema + seed against Neon
cp .env.example .env.local   # set DATABASE_URL + DIRECT_URL
npm run db:push && npm run db:seed

# 4. Vercel: import repo, set env (DATABASE_URL, DIRECT_URL, NEXT_PUBLIC_APP_URL,
#    ADMIN_SESSION_SECRET, ADMIN_ALLOWLIST, CRON_SECRET, Turnstile TEST keys,
#    Culqi TEST keys), add vercel.json, deploy.

# 5. share the preview URL with Rodrigo
```
