1 line
70 KiB
JSON
1 line
70 KiB
JSON
{"repo": "alessandrobertelli/ibm-bob-hackathon", "files": {"DEPLOYMENT_GUIDE.md": {"884e54cf365b4105f2ae62d8d2138d5469b96c39": {"t": 1785746530.2394595, "n": 13464, "h": "311aeb2c9e0d3539", "c": "# atavola — Deployment guide\n\nEnd-to-end walkthrough for getting atavola live on **a single Vercel project** with Supabase. ~30 minutes once you have the API keys.\n\n## Cost\n\n- Supabase free tier: 500 MB DB, 1 GB Storage, 50k MAU.\n- Vercel Hobby: 100 GB bandwidth/month, 100k function invocations/month, 60 s function `maxDuration`. Plenty.\n- OpenRouter: free models cost nothing per call (each is rate-limited; the rotation list handles that).\n- Pollinations.ai: free, no account.\n- Hugging Face Inference: free with a token; rate-limited.\n- Cloudflare Workers AI: free tier, ~3,300 FLUX-1-schnell images/day.\n- Resend: 3k emails/month free.\n\nTotal: **$0/month** at hackathon scale.\n\n---\n\n## Prerequisites\n\n- A GitHub repo with this code.\n- Free accounts: Supabase, OpenRouter, Resend, Vercel. Optional but recommended: Hugging Face, Cloudflare (image-gen fallbacks).\n\n---\n\n## 1. Supabase\n\n### 1.1 Create the project\n\n1. https://supabase.com/dashboard → \"New project\".\n2. Pick a region close to your users.\n3. Wait ~2 min for provisioning.\n\n### 1.2 Apply the schema\n\n1. Open **SQL Editor** in the dashboard.\n2. Paste and run [supabase/migrations/0001_init.sql](supabase/migrations/0001_init.sql). Single file, end-to-end:\n - tables (`profiles`, `saved_meals`, `sessions`, `session_meals`, `guests`, `votes`, `rate_log`, `service_status`, `events`, `aggregated_stats`, `error_log`),\n - RLS policies on every one of them,\n - RPCs (`cast_vote`, `ensure_guest`, `check_rate`, `record_service_outcome`, `record_event`, `record_error`, `get_weekly_digest_data`, `cleanup_after_digest`, `list_my_sessions`),\n - the `supabase_realtime` publication for live results,\n - the public-read `meal-images` Storage bucket,\n - and the daily pgcron `atavola-cleanup-expired-sessions` job at 03:15 UTC.\n\nThe file is idempotent — running it twice is safe.\n\n### 1.3 Configure Auth\n\n**Authentication → URL Configuration**\n- Site URL: your future Vercel URL (you'll update once Vercel gives you the domain).\n- Redirect URLs: add `${SITE_URL}/auth/verify`.\n\n**Authentication → Email Templates → Magic Link**\n- Default English copy is fine. Keep `{{ .ConfirmationURL }}` as the link placeholder.\n\n**Authentication → SMTP Settings**\n- Enable \"Custom SMTP\" and point it at Resend.\n ```\n Host: smtp.resend.com\n Port: 465\n User: resend\n Password: <Resend API key>\n Sender: <verified domain> (or onboarding@resend.dev for testing)\n Sender name: atavola\n ```\n\n### 1.4 Grab the keys\n\n**Settings → API**, copy:\n\n- Project URL → `SUPABASE_URL` and `VITE_SUPABASE_URL`.\n- `anon` public key → `VITE_SUPABASE_ANON_KEY`.\n- `service_role` secret → `SUPABASE_SERVICE_ROLE_KEY` (server-only, never expose).\n\n---\n\n## 2. OpenRouter\n\n1. https://openrouter.ai → sign up.\n2. **Keys** → \"Create Key\". Copy the `sk-or-v1-...` value into `OPENROUTER_API_KEY`.\n3. Browse https://openrouter.ai/models?max_price=0 and pick 3–5 free models. A solid starting set:\n ```\n OPENROUTER_MODELS=google/gemini-2.0-flash-exp:free,deepseek/deepseek-chat:free,meta-llama/llama-3.3-70b-instruct:free,mistralai/mistral-7b-instruct:free\n ```\n\n---\n\n## 3. Image-gen providers (optional but recommended)\n\n`IMAGE_PROVIDERS` is comma-separated, tried in order. Pollinations alone works; adding the others gives resilience.\n\n### 3.1 Pollinations.ai (no setup)\n\nAlready free, no account. Add `pollinations` to `IMAGE_PROVIDERS`.\n\n### 3.2 Hugging Face Inference\n\n1. https://huggingface.co/join → sign up (or use existing account).\n2. https://huggingface.co/settings/tokens → \"New token\", role \"Read\".\n3. Save as `HUGGINGFACE_API_TOKEN`.\n4. Add `huggingface` to `IMAGE_PROVIDERS`.\n\n### 3.3 Cloudflare Workers AI\n\n1. https://dash.cloudflare.com → sign up.\n2. **Account home** → copy the Account ID from the right rail → `CLOUDFLARE_ACCOUNT_ID`.\n3. **My Profile → API Tokens → Create Token → Custom token**:\n - Permissions: `Account → Workers AI → Read`.\n - Account Resources: include your account.\n4. Save the token as `CLOUDFLARE_API_TOKEN`.\n5. Add `cloudflare` to `IMAGE_PROVIDERS`.\n\nRecommended order: `pollinations,huggingface,cloudflare`. Pollinations is fastest; HF and CF are sturdier when Pollinations slows down.\n\n---\n\n## 4. Vercel\n\n### 4.1 Import the project\n\n1. https://vercel.com → \"Add New\" → \"Project\".\n2. Import the GitHub repo.\n3. Configure:\n - **Framework Preset**: Other (auto-detected from `vercel.json`).\n - **Root Directory**: `.` (repo root — leave default).\n - **Build / Output / Install** commands: leave at defaults; `vercel.json` overrides them.\n4. Don't deploy yet — set env vars first.\n\n### 4.2 Environment variables\n\nVercel → Project → Settings → Environment Variables. Add each, mark for Production + Preview + Development:\n\n```\nSERVICE_MODE=production\n\nSUPABASE_URL=\nSUPABASE_SERVICE_ROLE_KEY=\nVITE_SUPABASE_URL=\nVITE_SUPABASE_ANON_KEY=\n\nOPENROUTER_API_KEY=\nOPENROUTER_MODELS=\n\nIMAGE_PROVIDERS=pollinations,huggingface,cloudflare\nHUGGINGFACE_API_TOKEN=\nCLOUDFLARE_ACCOUNT_ID=\nCLOUDFLARE_API_TOKEN=\n\n# Weekly digest email (Vercel Cron → /api/cron/weekly-stats)\nRESEND_API_KEY=\nEMAIL_FROM=atavola <no-reply@atavola.ch>\nCRON_SECRET=\nSTATS_EMAIL_RECIPIENTS=\n```\n\nNotes:\n\n- `EMAIL_FROM` is the sender for direct Resend HTTP calls (currently just the weekly digest). It's separate from the Supabase Auth → SMTP settings; magic-link mail still uses whatever you wired up there. The domain in `EMAIL_FROM` must be verified in Resend.\n- `CRON_SECRET` is what Vercel signs cron requests with as `Authorization: Bearer …`. Set it both in the env-vars block above **and** in **Settings → Cron Jobs** for the project. Mismatch → the cron 401s.\n- `STATS_EMAIL_RECIPIENTS` is a comma-separated list. Empty = the cron runs but skips sending (handy in staging).\n- The schedule itself lives in [`vercel.json`](vercel.json) (`crons` block). Default is every Monday 09:00 UTC; change it there and redeploy.\n- `VITE_API_URL` is intentionally absent — same-origin defaults to `/api` and that's correct here.\n\n### 4.3 Deploy\n\nClick \"Deploy\". ~1–2 minutes. Note the URL (e.g. `https://atavola.vercel.app`).\n\n### 4.4 Custom Domain & DNS\n\nIf you have a domain like `atavola.ch`:\n1. **Vercel → Settings → Domains**: Add `atavola.ch`.\n2. **DNS Records**: Vercel will provide an **A record** (for `@`) and a **CNAME record** (for `www`). Set these in your registrar's dashboard (e.g., Cloudflare, Namecheap).\n3. **Verify**: Once the SSL certificate is issued by Vercel, the domain status will turn green.\n\n### 4.5 Loop the URL back to Supabase\n\nGo back to **Supabase → Authentication → URL Configuration**: ensure the **Site URL** and **Redirect URLs** use `https://atavola.ch`. If you don't do this, users will be redirected to the default `vercel.app` domain instead of your custom one.\n\n---\n\n## 5. Smoke test\n\n```bash\ncurl https://your-app.vercel.app/api/system/health\n# → {\"status\":\"ok\",\"mode\":\"production\",...}\n```\n\nIn a private window:\n\n1. Open the Vercel URL.\n2. Sign in with magic link (check your inbox — see \"Resend domain\" gotcha below).\n3. Create a session: vibe + headcount + no pre-selected meals.\n4. Wait for the 4 cards to appear with images.\n5. Click \"Share voting link\". Try the **native share** button (mobile only) and the **copy link** button. Tapping the URL pill itself also copies. Try the **QR code**.\n6. In another private window, open the link, swipe through the 4 cards.\n7. In the host window, watch the live results re-rank.\n8. Heart a card → it appears under \"My Food\" in your profile dropdown.\n9. Reload the profile, drag to reorder, swipe a row left to delete.\n\nIf any step fails, check Vercel function logs (Project → Logs → click a function) and the browser console.\n\n---\n\n## 6. Custom domain (optional)\n\n1. Buy at any registrar (Cloudflare Registrar is at-cost).\n2. Vercel → Project → Settings → Domains → Add. Follow Vercel's DNS instructions.\n3. Once active, update **Supabase → Authentication → URL Configuration** to use the new domain.\n4. Update **Resend → Domains** to verify the new sending domain so magic-link emails work for everyone, not just yourself.\n\nNo code changes needed — the app reads URLs from env / runtime, never hardcoded.\n\n---\n\n## 7. Email deliverability (SPF / DKIM / DMARC)\n\nMagic-link delivery is the silent growth lever — if mail lands in spam, sign-ups die without you noticing. Resend's dashboard walks you through the DNS records; here's what to set on the apex domain (`atavola.ch`):\n\n1. **SPF** (`TXT @`) — tells receivers which servers may send mail \"as\" your domain.\n ```\n v=spf1 include:_spf.resend.com ~all\n ```\n `~all` (soft fail) is the right choice while you stabilise; switch to `-all` (hard fail) once you're confident no other systems send mail from atavola.ch.\n\n2. **DKIM** — Resend auto-generates a CNAME pair when you verify the domain. Copy them into DNS verbatim. They look like:\n ```\n resend._domainkey CNAME resend._domainkey.<region>.amazonses.com\n ```\n Wait for Resend to flip the domain status to \"verified\" (usually <10 min).\n\n3. **DMARC** (`TXT _dmarc`) — tells receivers what to do with mail that fails SPF / DKIM and where to report aggregate failures.\n ```\n v=DMARC1; p=none; rua=mailto:no-reply@atavola.ch; pct=100; aspf=r; adkim=r\n ```\n Start with `p=none` so you collect reports without rejecting legitimate mail. After a week of clean reports, tighten to `p=quarantine`, then `p=reject`.\n\n4. **Reverse DNS** is handled by Resend on their sending IPs — nothing for you to configure.\n\n5. **Verify**: send a magic-link to a Gmail address and inspect the headers. You want `dkim=pass`, `spf=pass`, `dmarc=pass`. [https://www.mail-tester.com/](https://www.mail-tester.com/) gives a 0–10 score; aim for 9+.\n\nThe same Resend domain powers the weekly digest (`/api/cron/weekly-stats`). `EMAIL_FROM` must use this verified domain or sends bounce silently.\n\n---\n\n## 8. SEO submission\n\nThe static landing page, `/about`, `/privacy`, and `/terms` ship with full meta and JSON-LD; `robots.txt`, `sitemap.xml`, `llms.txt`, and `humans.txt` live in `frontend/public/`. After first deploy:\n\n1. **Google Search Console** — [https://search.google.com/search-console](https://search.google.com/search-console). Add the property (use DNS verification on atavola.ch so all subdomains are covered). Submit `https://atavola.ch/sitemap.xml`. Indexing usually starts within hours.\n2. **Bing Webmaster Tools** — [https://www.bing.com/webmasters](https://www.bing.com/webmasters). Same flow; Bing also feeds DuckDuckGo results.\n3. **Yandex Webmaster** (optional, only if you care about RU/CIS traffic).\n4. **Sitemap auto-discovery** — `robots.txt` already references the sitemap. Most crawlers find it automatically; the manual submission above just shortens the discovery window.\n5. **Validate**:\n - [Google Rich Results Test](https://search.google.com/test/rich-results) — paste each of `/`, `/about`, `/privacy`, `/terms` and confirm the JSON-LD blocks parse.\n - [Twitter / X Card Validator](https://cards-dev.twitter.com/validator) — paste the home URL, confirm the OG image renders.\n - [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/) — same.\n - [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) — same. Click \"Scrape Again\" if cached.\n - [securityheaders.com](https://securityheaders.com/) and [Mozilla Observatory](https://observatory.mozilla.org/) — both should score A or A+.\n - [PageSpeed Insights](https://pagespeed.web.dev/) — Core Web Vitals must be green for ranking.\n\n---\n\n## Common pitfalls\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Function logs say \"Refusing to boot: SERVICE_MODE=…\" | Vercel env var unset or misspelled | Set `SERVICE_MODE=production` exactly. The hard fail is intentional — see [SECURITY.md](SECURITY.md) #C1. |\n| Magic-link email never arrives for anyone but you | Resend domain not verified | Verify the domain in Resend, then point Supabase SMTP at it. Until verified, only your own address receives mail. |\n| 401 on every call | RLS denies because policies didn't run | Re-apply both migrations — verify policies under `Authentication → Policies`. |\n| `cast_vote` errors with `session_expired` or `session_not_open` | Session is older than 30 days or status moved off `voting` | Hosts create a new session; expiry is enforced server-side and the daily pgcron purges past it anyway. |\n| `cast_vote` errors with `invalid_guest_token` | Guest hasn't been minted | Frontend should call `POST /api/votes/guest` once before voting (it does — clear localStorage and reload if stale). |\n| 429 on rate-limited endpoints | The Postgres rate-limit RPC threw | Check function logs for `[checkRate] RPC failed` warnings. The limiter fails open to avoid locking real users out, but a sustained warning means you're effectively unprotected — fix the underlying DB error. |\n| Generated images broken | Every image provider failed magic-byte sniff or quota | Inspect the function log — provider names + reasons are logged. Most common: wrong CF/HF key, or HF model warming up (just retry). |\n| Meal generation 503s | All OpenRouter free models hit their daily quota | Add more models to `OPENROUTER_MODELS`; rotation will skip past empty ones. |\n| Vercel function times out | LLM + 4 image fetches > 60 s | Drop a slow provider from `IMAGE_PROVIDERS` (HF when its servers are loading). Long-term: Vercel Pro = 300 s, or split generation into per-meal endpoints (out of scope today). |\n\n<!-- Made with Bob -->\n"}, "8ca7e9c34d34e903d7cd4efc841240463543d671": {"t": 1785765360.8299544, "n": 13464, "h": "311aeb2c9e0d3539", "c": "# atavola — Deployment guide\n\nEnd-to-end walkthrough for getting atavola live on **a single Vercel project** with Supabase. ~30 minutes once you have the API keys.\n\n## Cost\n\n- Supabase free tier: 500 MB DB, 1 GB Storage, 50k MAU.\n- Vercel Hobby: 100 GB bandwidth/month, 100k function invocations/month, 60 s function `maxDuration`. Plenty.\n- OpenRouter: free models cost nothing per call (each is rate-limited; the rotation list handles that).\n- Pollinations.ai: free, no account.\n- Hugging Face Inference: free with a token; rate-limited.\n- Cloudflare Workers AI: free tier, ~3,300 FLUX-1-schnell images/day.\n- Resend: 3k emails/month free.\n\nTotal: **$0/month** at hackathon scale.\n\n---\n\n## Prerequisites\n\n- A GitHub repo with this code.\n- Free accounts: Supabase, OpenRouter, Resend, Vercel. Optional but recommended: Hugging Face, Cloudflare (image-gen fallbacks).\n\n---\n\n## 1. Supabase\n\n### 1.1 Create the project\n\n1. https://supabase.com/dashboard → \"New project\".\n2. Pick a region close to your users.\n3. Wait ~2 min for provisioning.\n\n### 1.2 Apply the schema\n\n1. Open **SQL Editor** in the dashboard.\n2. Paste and run [supabase/migrations/0001_init.sql](supabase/migrations/0001_init.sql). Single file, end-to-end:\n - tables (`profiles`, `saved_meals`, `sessions`, `session_meals`, `guests`, `votes`, `rate_log`, `service_status`, `events`, `aggregated_stats`, `error_log`),\n - RLS policies on every one of them,\n - RPCs (`cast_vote`, `ensure_guest`, `check_rate`, `record_service_outcome`, `record_event`, `record_error`, `get_weekly_digest_data`, `cleanup_after_digest`, `list_my_sessions`),\n - the `supabase_realtime` publication for live results,\n - the public-read `meal-images` Storage bucket,\n - and the daily pgcron `atavola-cleanup-expired-sessions` job at 03:15 UTC.\n\nThe file is idempotent — running it twice is safe.\n\n### 1.3 Configure Auth\n\n**Authentication → URL Configuration**\n- Site URL: your future Vercel URL (you'll update once Vercel gives you the domain).\n- Redirect URLs: add `${SITE_URL}/auth/verify`.\n\n**Authentication → Email Templates → Magic Link**\n- Default English copy is fine. Keep `{{ .ConfirmationURL }}` as the link placeholder.\n\n**Authentication → SMTP Settings**\n- Enable \"Custom SMTP\" and point it at Resend.\n ```\n Host: smtp.resend.com\n Port: 465\n User: resend\n Password: <Resend API key>\n Sender: <verified domain> (or onboarding@resend.dev for testing)\n Sender name: atavola\n ```\n\n### 1.4 Grab the keys\n\n**Settings → API**, copy:\n\n- Project URL → `SUPABASE_URL` and `VITE_SUPABASE_URL`.\n- `anon` public key → `VITE_SUPABASE_ANON_KEY`.\n- `service_role` secret → `SUPABASE_SERVICE_ROLE_KEY` (server-only, never expose).\n\n---\n\n## 2. OpenRouter\n\n1. https://openrouter.ai → sign up.\n2. **Keys** → \"Create Key\". Copy the `sk-or-v1-...` value into `OPENROUTER_API_KEY`.\n3. Browse https://openrouter.ai/models?max_price=0 and pick 3–5 free models. A solid starting set:\n ```\n OPENROUTER_MODELS=google/gemini-2.0-flash-exp:free,deepseek/deepseek-chat:free,meta-llama/llama-3.3-70b-instruct:free,mistralai/mistral-7b-instruct:free\n ```\n\n---\n\n## 3. Image-gen providers (optional but recommended)\n\n`IMAGE_PROVIDERS` is comma-separated, tried in order. Pollinations alone works; adding the others gives resilience.\n\n### 3.1 Pollinations.ai (no setup)\n\nAlready free, no account. Add `pollinations` to `IMAGE_PROVIDERS`.\n\n### 3.2 Hugging Face Inference\n\n1. https://huggingface.co/join → sign up (or use existing account).\n2. https://huggingface.co/settings/tokens → \"New token\", role \"Read\".\n3. Save as `HUGGINGFACE_API_TOKEN`.\n4. Add `huggingface` to `IMAGE_PROVIDERS`.\n\n### 3.3 Cloudflare Workers AI\n\n1. https://dash.cloudflare.com → sign up.\n2. **Account home** → copy the Account ID from the right rail → `CLOUDFLARE_ACCOUNT_ID`.\n3. **My Profile → API Tokens → Create Token → Custom token**:\n - Permissions: `Account → Workers AI → Read`.\n - Account Resources: include your account.\n4. Save the token as `CLOUDFLARE_API_TOKEN`.\n5. Add `cloudflare` to `IMAGE_PROVIDERS`.\n\nRecommended order: `pollinations,huggingface,cloudflare`. Pollinations is fastest; HF and CF are sturdier when Pollinations slows down.\n\n---\n\n## 4. Vercel\n\n### 4.1 Import the project\n\n1. https://vercel.com → \"Add New\" → \"Project\".\n2. Import the GitHub repo.\n3. Configure:\n - **Framework Preset**: Other (auto-detected from `vercel.json`).\n - **Root Directory**: `.` (repo root — leave default).\n - **Build / Output / Install** commands: leave at defaults; `vercel.json` overrides them.\n4. Don't deploy yet — set env vars first.\n\n### 4.2 Environment variables\n\nVercel → Project → Settings → Environment Variables. Add each, mark for Production + Preview + Development:\n\n```\nSERVICE_MODE=production\n\nSUPABASE_URL=\nSUPABASE_SERVICE_ROLE_KEY=\nVITE_SUPABASE_URL=\nVITE_SUPABASE_ANON_KEY=\n\nOPENROUTER_API_KEY=\nOPENROUTER_MODELS=\n\nIMAGE_PROVIDERS=pollinations,huggingface,cloudflare\nHUGGINGFACE_API_TOKEN=\nCLOUDFLARE_ACCOUNT_ID=\nCLOUDFLARE_API_TOKEN=\n\n# Weekly digest email (Vercel Cron → /api/cron/weekly-stats)\nRESEND_API_KEY=\nEMAIL_FROM=atavola <no-reply@atavola.ch>\nCRON_SECRET=\nSTATS_EMAIL_RECIPIENTS=\n```\n\nNotes:\n\n- `EMAIL_FROM` is the sender for direct Resend HTTP calls (currently just the weekly digest). It's separate from the Supabase Auth → SMTP settings; magic-link mail still uses whatever you wired up there. The domain in `EMAIL_FROM` must be verified in Resend.\n- `CRON_SECRET` is what Vercel signs cron requests with as `Authorization: Bearer …`. Set it both in the env-vars block above **and** in **Settings → Cron Jobs** for the project. Mismatch → the cron 401s.\n- `STATS_EMAIL_RECIPIENTS` is a comma-separated list. Empty = the cron runs but skips sending (handy in staging).\n- The schedule itself lives in [`vercel.json`](vercel.json) (`crons` block). Default is every Monday 09:00 UTC; change it there and redeploy.\n- `VITE_API_URL` is intentionally absent — same-origin defaults to `/api` and that's correct here.\n\n### 4.3 Deploy\n\nClick \"Deploy\". ~1–2 minutes. Note the URL (e.g. `https://atavola.vercel.app`).\n\n### 4.4 Custom Domain & DNS\n\nIf you have a domain like `atavola.ch`:\n1. **Vercel → Settings → Domains**: Add `atavola.ch`.\n2. **DNS Records**: Vercel will provide an **A record** (for `@`) and a **CNAME record** (for `www`). Set these in your registrar's dashboard (e.g., Cloudflare, Namecheap).\n3. **Verify**: Once the SSL certificate is issued by Vercel, the domain status will turn green.\n\n### 4.5 Loop the URL back to Supabase\n\nGo back to **Supabase → Authentication → URL Configuration**: ensure the **Site URL** and **Redirect URLs** use `https://atavola.ch`. If you don't do this, users will be redirected to the default `vercel.app` domain instead of your custom one.\n\n---\n\n## 5. Smoke test\n\n```bash\ncurl https://your-app.vercel.app/api/system/health\n# → {\"status\":\"ok\",\"mode\":\"production\",...}\n```\n\nIn a private window:\n\n1. Open the Vercel URL.\n2. Sign in with magic link (check your inbox — see \"Resend domain\" gotcha below).\n3. Create a session: vibe + headcount + no pre-selected meals.\n4. Wait for the 4 cards to appear with images.\n5. Click \"Share voting link\". Try the **native share** button (mobile only) and the **copy link** button. Tapping the URL pill itself also copies. Try the **QR code**.\n6. In another private window, open the link, swipe through the 4 cards.\n7. In the host window, watch the live results re-rank.\n8. Heart a card → it appears under \"My Food\" in your profile dropdown.\n9. Reload the profile, drag to reorder, swipe a row left to delete.\n\nIf any step fails, check Vercel function logs (Project → Logs → click a function) and the browser console.\n\n---\n\n## 6. Custom domain (optional)\n\n1. Buy at any registrar (Cloudflare Registrar is at-cost).\n2. Vercel → Project → Settings → Domains → Add. Follow Vercel's DNS instructions.\n3. Once active, update **Supabase → Authentication → URL Configuration** to use the new domain.\n4. Update **Resend → Domains** to verify the new sending domain so magic-link emails work for everyone, not just yourself.\n\nNo code changes needed — the app reads URLs from env / runtime, never hardcoded.\n\n---\n\n## 7. Email deliverability (SPF / DKIM / DMARC)\n\nMagic-link delivery is the silent growth lever — if mail lands in spam, sign-ups die without you noticing. Resend's dashboard walks you through the DNS records; here's what to set on the apex domain (`atavola.ch`):\n\n1. **SPF** (`TXT @`) — tells receivers which servers may send mail \"as\" your domain.\n ```\n v=spf1 include:_spf.resend.com ~all\n ```\n `~all` (soft fail) is the right choice while you stabilise; switch to `-all` (hard fail) once you're confident no other systems send mail from atavola.ch.\n\n2. **DKIM** — Resend auto-generates a CNAME pair when you verify the domain. Copy them into DNS verbatim. They look like:\n ```\n resend._domainkey CNAME resend._domainkey.<region>.amazonses.com\n ```\n Wait for Resend to flip the domain status to \"verified\" (usually <10 min).\n\n3. **DMARC** (`TXT _dmarc`) — tells receivers what to do with mail that fails SPF / DKIM and where to report aggregate failures.\n ```\n v=DMARC1; p=none; rua=mailto:no-reply@atavola.ch; pct=100; aspf=r; adkim=r\n ```\n Start with `p=none` so you collect reports without rejecting legitimate mail. After a week of clean reports, tighten to `p=quarantine`, then `p=reject`.\n\n4. **Reverse DNS** is handled by Resend on their sending IPs — nothing for you to configure.\n\n5. **Verify**: send a magic-link to a Gmail address and inspect the headers. You want `dkim=pass`, `spf=pass`, `dmarc=pass`. [https://www.mail-tester.com/](https://www.mail-tester.com/) gives a 0–10 score; aim for 9+.\n\nThe same Resend domain powers the weekly digest (`/api/cron/weekly-stats`). `EMAIL_FROM` must use this verified domain or sends bounce silently.\n\n---\n\n## 8. SEO submission\n\nThe static landing page, `/about`, `/privacy`, and `/terms` ship with full meta and JSON-LD; `robots.txt`, `sitemap.xml`, `llms.txt`, and `humans.txt` live in `frontend/public/`. After first deploy:\n\n1. **Google Search Console** — [https://search.google.com/search-console](https://search.google.com/search-console). Add the property (use DNS verification on atavola.ch so all subdomains are covered). Submit `https://atavola.ch/sitemap.xml`. Indexing usually starts within hours.\n2. **Bing Webmaster Tools** — [https://www.bing.com/webmasters](https://www.bing.com/webmasters). Same flow; Bing also feeds DuckDuckGo results.\n3. **Yandex Webmaster** (optional, only if you care about RU/CIS traffic).\n4. **Sitemap auto-discovery** — `robots.txt` already references the sitemap. Most crawlers find it automatically; the manual submission above just shortens the discovery window.\n5. **Validate**:\n - [Google Rich Results Test](https://search.google.com/test/rich-results) — paste each of `/`, `/about`, `/privacy`, `/terms` and confirm the JSON-LD blocks parse.\n - [Twitter / X Card Validator](https://cards-dev.twitter.com/validator) — paste the home URL, confirm the OG image renders.\n - [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/) — same.\n - [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) — same. Click \"Scrape Again\" if cached.\n - [securityheaders.com](https://securityheaders.com/) and [Mozilla Observatory](https://observatory.mozilla.org/) — both should score A or A+.\n - [PageSpeed Insights](https://pagespeed.web.dev/) — Core Web Vitals must be green for ranking.\n\n---\n\n## Common pitfalls\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Function logs say \"Refusing to boot: SERVICE_MODE=…\" | Vercel env var unset or misspelled | Set `SERVICE_MODE=production` exactly. The hard fail is intentional — see [SECURITY.md](SECURITY.md) #C1. |\n| Magic-link email never arrives for anyone but you | Resend domain not verified | Verify the domain in Resend, then point Supabase SMTP at it. Until verified, only your own address receives mail. |\n| 401 on every call | RLS denies because policies didn't run | Re-apply both migrations — verify policies under `Authentication → Policies`. |\n| `cast_vote` errors with `session_expired` or `session_not_open` | Session is older than 30 days or status moved off `voting` | Hosts create a new session; expiry is enforced server-side and the daily pgcron purges past it anyway. |\n| `cast_vote` errors with `invalid_guest_token` | Guest hasn't been minted | Frontend should call `POST /api/votes/guest` once before voting (it does — clear localStorage and reload if stale). |\n| 429 on rate-limited endpoints | The Postgres rate-limit RPC threw | Check function logs for `[checkRate] RPC failed` warnings. The limiter fails open to avoid locking real users out, but a sustained warning means you're effectively unprotected — fix the underlying DB error. |\n| Generated images broken | Every image provider failed magic-byte sniff or quota | Inspect the function log — provider names + reasons are logged. Most common: wrong CF/HF key, or HF model warming up (just retry). |\n| Meal generation 503s | All OpenRouter free models hit their daily quota | Add more models to `OPENROUTER_MODELS`; rotation will skip past empty ones. |\n| Vercel function times out | LLM + 4 image fetches > 60 s | Drop a slow provider from `IMAGE_PROVIDERS` (HF when its servers are loading). Long-term: Vercel Pro = 300 s, or split generation into per-meal endpoints (out of scope today). |\n\n<!-- Made with Bob -->\n"}, "11a7a4736772593ff85676c37351199bc754cec2": {"t": 1785765363.1181133, "n": 13468, "h": "9cbe42111db0c5b3", "c": "# atavola — Deployment guide\n\nEnd-to-end walkthrough for getting atavola live on **a single Vercel project** with Supabase. ~30 minutes once you have the API keys.\n\n## Cost\n\n- Supabase free tier: 500 MB DB, 1 GB Storage, 50k MAU.\n- Vercel Hobby: 100 GB bandwidth/month, 100k function invocations/month, 60 s function `maxDuration`. Plenty.\n- OpenRouter: free models cost nothing per call (each is rate-limited; the rotation list handles that).\n- Pollinations.ai: free, no account.\n- Hugging Face Inference: free with a token; rate-limited.\n- Cloudflare Workers AI: free tier, ~3,300 FLUX-1-schnell images/day.\n- Resend: 3k emails/month free.\n\nTotal: **$0/month** at hackathon scale.\n\n---\n\n## Prerequisites\n\n- A GitHub repo with this code.\n- Free accounts: Supabase, OpenRouter, Resend, Vercel. Optional but recommended: Hugging Face, Cloudflare (image-gen fallbacks).\n\n---\n\n## 1. Supabase\n\n### 1.1 Create the project\n\n1. https://supabase.com/dashboard → \"New project\".\n2. Pick a region close to your users.\n3. Wait ~2 min for provisioning.\n\n### 1.2 Apply the schema\n\n1. Open **SQL Editor** in the dashboard.\n2. Paste and run [supabase/migrations/0001_init.sql](supabase/migrations/0001_init.sql). Single file, end-to-end:\n - tables (`profiles`, `saved_meals`, `sessions`, `session_meals`, `guests`, `votes`, `rate_log`, `service_status`, `events`, `aggregated_stats`, `error_log`),\n - RLS policies on every one of them,\n - RPCs (`cast_vote`, `ensure_guest`, `check_rate`, `record_service_outcome`, `record_event`, `record_error`, `get_weekly_digest_data`, `cleanup_after_digest`, `list_my_sessions`),\n - the `supabase_realtime` publication for live results,\n - the public-read `meal-images` Storage bucket,\n - and the daily pg_cron `atavola-cleanup-expired-sessions` job at 03:15 UTC.\n\nThe file is idempotent — running it twice is safe.\n\n### 1.3 Configure Auth\n\n**Authentication → URL Configuration**\n- Site URL: your future Vercel URL (you'll update once Vercel gives you the domain).\n- Redirect URLs: add `${SITE_URL}/auth/verify`.\n\n**Authentication → Email Templates → Magic Link**\n- Default English copy is fine. Keep `{{ .ConfirmationURL }}` as the link placeholder.\n\n**Authentication → SMTP Settings**\n- Enable \"Custom SMTP\" and point it at Resend.\n ```\n Host: smtp.resend.com\n Port: 465\n User: resend\n Password: <Resend API key>\n Sender: <verified domain> (or onboarding@resend.dev for testing)\n Sender name: atavola\n ```\n\n### 1.4 Grab the keys\n\n**Settings → API**, copy:\n\n- Project URL → `SUPABASE_URL` and `VITE_SUPABASE_URL`.\n- `anon` public key → `VITE_SUPABASE_ANON_KEY`.\n- `service_role` secret → `SUPABASE_SERVICE_ROLE_KEY` (server-only, never expose).\n\n---\n\n## 2. OpenRouter\n\n1. https://openrouter.ai → sign up.\n2. **Keys** → \"Create Key\". Copy the `sk-or-v1-...` value into `OPENROUTER_API_KEY`.\n3. Browse https://openrouter.ai/models?max_price=0 and pick 3–5 free models. A solid starting set:\n ```\n OPENROUTER_MODELS=google/gemini-2.0-flash-exp:free,deepseek/deepseek-chat:free,meta-llama/llama-3.3-70b-instruct:free,mistralai/mistral-7b-instruct:free\n ```\n\n---\n\n## 3. Image-gen providers (optional but recommended)\n\n`IMAGE_PROVIDERS` is comma-separated, tried in order. Pollinations alone works; adding the others gives resilience.\n\n### 3.1 Pollinations.ai (no setup)\n\nAlready free, no account. Add `pollinations` to `IMAGE_PROVIDERS`.\n\n### 3.2 Hugging Face Inference\n\n1. https://huggingface.co/join → sign up (or use existing account).\n2. https://huggingface.co/settings/tokens → \"New token\", role \"Read\".\n3. Save as `HUGGINGFACE_API_TOKEN`.\n4. Add `huggingface` to `IMAGE_PROVIDERS`.\n\n### 3.3 Cloudflare Workers AI\n\n1. https://dash.cloudflare.com → sign up.\n2. **Account home** → copy the Account ID from the right rail → `CLOUDFLARE_ACCOUNT_ID`.\n3. **My Profile → API Tokens → Create Token → Custom token**:\n - Permissions: `Account → Workers AI → Read`.\n - Account Resources: include your account.\n4. Save the token as `CLOUDFLARE_API_TOKEN`.\n5. Add `cloudflare` to `IMAGE_PROVIDERS`.\n\nRecommended order: `pollinations,huggingface,cloudflare`. Pollinations is fastest; HF and CF are sturdier when Pollinations slows down.\n\n---\n\n## 4. Vercel\n\n### 4.1 Import the project\n\n1. https://vercel.com → \"Add New\" → \"Project\".\n2. Import the GitHub repo.\n3. Configure:\n - **Framework Preset**: Other (auto-detected from `vercel.json`).\n - **Root Directory**: `.` (repo root — leave default).\n - **Build / Output / Install** commands: leave at defaults; `vercel.json` overrides them.\n4. Don't deploy yet — set env vars first.\n\n### 4.2 Environment variables\n\nVercel → Project → Settings → Environment Variables. Add each, mark for Production + Preview + Development:\n\n```\nSERVICE_MODE=production\n\nSUPABASE_URL=\nSUPABASE_SERVICE_ROLE_KEY=\nVITE_SUPABASE_URL=\nVITE_SUPABASE_ANON_KEY=\n\nOPENROUTER_API_KEY=\nOPENROUTER_MODELS=\n\nIMAGE_PROVIDERS=pollinations,huggingface,cloudflare\nHUGGINGFACE_API_TOKEN=\nCLOUDFLARE_ACCOUNT_ID=\nCLOUDFLARE_API_TOKEN=\n\n# Weekly digest email (Vercel Cron → /api/_cron/weekly-stats)\nRESEND_API_KEY=\nEMAIL_FROM=atavola <no-reply@atavola.ch>\nCRON_SECRET=\nSTATS_EMAIL_RECIPIENTS=\n```\n\nNotes:\n\n- `EMAIL_FROM` is the sender for direct Resend HTTP calls (currently just the weekly digest). It's separate from the Supabase Auth → SMTP settings; magic-link mail still uses whatever you wired up there. The domain in `EMAIL_FROM` must be verified in Resend.\n- `CRON_SECRET` is what Vercel signs cron requests with as `Authorization: Bearer …`. Set it both in the env-vars block above **and** in **Settings → Cron Jobs** for the project. Mismatch → the cron 401s.\n- `STATS_EMAIL_RECIPIENTS` is a comma-separated list. Empty = the cron runs but skips sending (handy in staging).\n- The schedule itself lives in [`vercel.json`](vercel.json) (`crons` block). Default is every Monday 09:00 UTC; change it there and redeploy.\n- `VITE_API_URL` is intentionally absent — same-origin defaults to `/api` and that's correct here.\n\n### 4.3 Deploy\n\nClick \"Deploy\". ~1–2 minutes. Note the URL (e.g. `https://atavola.vercel.app`).\n\n### 4.4 Custom Domain & DNS\n\nIf you have a domain like `atavola.ch`:\n1. **Vercel → Settings → Domains**: Add `atavola.ch`.\n2. **DNS Records**: Vercel will provide an **A record** (for `@`) and a **CNAME record** (for `www`). Set these in your registrar's dashboard (e.g., Cloudflare, Namecheap).\n3. **Verify**: Once the SSL certificate is issued by Vercel, the domain status will turn green.\n\n### 4.5 Loop the URL back to Supabase\n\nGo back to **Supabase → Authentication → URL Configuration**: ensure the **Site URL** and **Redirect URLs** use `https://atavola.ch`. If you don't do this, users will be redirected to the default `vercel.app` domain instead of your custom one.\n\n---\n\n## 5. Smoke test\n\n```bash\ncurl https://your-app.vercel.app/api/system/health\n# → {\"status\":\"ok\",\"mode\":\"production\",...}\n```\n\nIn a private window:\n\n1. Open the Vercel URL.\n2. Sign in with magic link (check your inbox — see \"Resend domain\" gotcha below).\n3. Create a session: vibe + headcount + no pre-selected meals.\n4. Wait for the 4 cards to appear with images.\n5. Click \"Share voting link\". Try the **native share** button (mobile only) and the **copy link** button. Tapping the URL pill itself also copies. Try the **QR code**.\n6. In another private window, open the link, swipe through the 4 cards.\n7. In the host window, watch the live results re-rank.\n8. Heart a card → it appears under \"My Food\" in your profile dropdown.\n9. Reload the profile, drag to reorder, swipe a row left to delete.\n\nIf any step fails, check Vercel function logs (Project → Logs → click a function) and the browser console.\n\n---\n\n## 6. Custom domain (optional)\n\n1. Buy at any registrar (Cloudflare Registrar is at-cost).\n2. Vercel → Project → Settings → Domains → Add. Follow Vercel's DNS instructions.\n3. Once active, update **Supabase → Authentication → URL Configuration** to use the new domain.\n4. Update **Resend → Domains** to verify the new sending domain so magic-link emails work for everyone, not just yourself.\n\nNo code changes needed — the app reads URLs from env / runtime, never hardcoded.\n\n---\n\n## 7. Email deliverability (SPF / DKIM / DMARC)\n\nMagic-link delivery is the silent growth lever — if mail lands in spam, sign-ups die without you noticing. Resend's dashboard walks you through the DNS records; here's what to set on the apex domain (`atavola.ch`):\n\n1. **SPF** (`TXT @`) — tells receivers which servers may send mail \"as\" your domain.\n ```\n v=spf1 include:_spf.resend.com ~all\n ```\n `~all` (soft fail) is the right choice while you stabilise; switch to `-all` (hard fail) once you're confident no other systems send mail from atavola.ch.\n\n2. **DKIM** — Resend auto-generates a CNAME pair when you verify the domain. Copy them into DNS verbatim. They look like:\n ```\n resend._domainkey CNAME resend._domainkey.<region>.amazonses.com\n ```\n Wait for Resend to flip the domain status to \"verified\" (usually <10 min).\n\n3. **DMARC** (`TXT _dmarc`) — tells receivers what to do with mail that fails SPF / DKIM and where to report aggregate failures.\n ```\n v=DMARC1; p=none; rua=mailto:no-reply@atavola.ch; pct=100; aspf=r; adkim=r\n ```\n Start with `p=none` so you collect reports without rejecting legitimate mail. After a week of clean reports, tighten to `p=quarantine`, then `p=reject`.\n\n4. **Reverse DNS** is handled by Resend on their sending IPs — nothing for you to configure.\n\n5. **Verify**: send a magic-link to a Gmail address and inspect the headers. You want `dkim=pass`, `spf=pass`, `dmarc=pass`. [https://www.mail-tester.com/](https://www.mail-tester.com/) gives a 0–10 score; aim for 9+.\n\nThe same Resend domain powers the weekly digest (`/api/_cron/weekly-stats`). `EMAIL_FROM` must use this verified domain or sends bounce silently.\n\n---\n\n## 8. SEO submission\n\nThe static landing page, `/about`, `/privacy`, and `/terms` ship with full meta and JSON-LD; `robots.txt`, `sitemap.xml`, `llms.txt`, and `humans.txt` live in `frontend/public/`. After first deploy:\n\n1. **Google Search Console** — [https://search.google.com/search-console](https://search.google.com/search-console). Add the property (use DNS verification on atavola.ch so all subdomains are covered). Submit `https://atavola.ch/sitemap.xml`. Indexing usually starts within hours.\n2. **Bing Webmaster Tools** — [https://www.bing.com/webmasters](https://www.bing.com/webmasters). Same flow; Bing also feeds DuckDuckGo results.\n3. **Yandex Webmaster** (optional, only if you care about RU/CIS traffic).\n4. **Sitemap auto-discovery** — `robots.txt` already references the sitemap. Most crawlers find it automatically; the manual submission above just shortens the discovery window.\n5. **Validate**:\n - [Google Rich Results Test](https://search.google.com/test/rich-results) — paste each of `/`, `/about`, `/privacy`, `/terms` and confirm the JSON-LD blocks parse.\n - [Twitter / X Card Validator](https://cards-dev.twitter.com/validator) — paste the home URL, confirm the OG image renders.\n - [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/) — same.\n - [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) — same. Click \"Scrape Again\" if cached.\n - [securityheaders.com](https://securityheaders.com/) and [Mozilla Observatory](https://observatory.mozilla.org/) — both should score A or A+.\n - [PageSpeed Insights](https://pagespeed.web.dev/) — Core Web Vitals must be green for ranking.\n\n---\n\n## Common pitfalls\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Function logs say \"Refusing to boot: SERVICE_MODE=…\" | Vercel env var unset or misspelled | Set `SERVICE_MODE=production` exactly. The hard fail is intentional — see [SECURITY.md](SECURITY.md) #C1. |\n| Magic-link email never arrives for anyone but you | Resend domain not verified | Verify the domain in Resend, then point Supabase SMTP at it. Until verified, only your own address receives mail. |\n| 401 on every call | RLS denies because policies didn't run | Re-apply both migrations — verify policies under `Authentication → Policies`. |\n| `cast_vote` errors with `session_expired` or `session_not_open` | Session is older than 30 days or status moved off `voting` | Hosts create a new session; expiry is enforced server-side and the daily pg_cron purges past it anyway. |\n| `cast_vote` errors with `invalid_guest_token` | Guest hasn't been minted | Frontend should call `POST /api/votes/guest` once before voting (it does — clear localStorage and reload if stale). |\n| 429 on rate-limited endpoints | The Postgres rate-limit RPC threw | Check function logs for `[checkRate] RPC failed` warnings. The limiter fails open to avoid locking real users out, but a sustained warning means you're effectively unprotected — fix the underlying DB error. |\n| Generated images broken | Every image provider failed magic-byte sniff or quota | Inspect the function log — provider names + reasons are logged. Most common: wrong CF/HF key, or HF model warming up (just retry). |\n| Meal generation 503s | All OpenRouter free models hit their daily quota | Add more models to `OPENROUTER_MODELS`; rotation will skip past empty ones. |\n| Vercel function times out | LLM + 4 image fetches > 60 s | Drop a slow provider from `IMAGE_PROVIDERS` (HF when its servers are loading). Long-term: Vercel Pro = 300 s, or split generation into per-meal endpoints (out of scope today). |\n\n<!-- Made with Bob -->\n"}, "d99c0d4e01462a8d640c7417a4b3ce038adecdda": {"t": 1785765364.3181355, "n": 13008, "h": "2e17ab36100c2769", "c": "# atavola — Deployment guide\n\nEnd-to-end walkthrough for getting atavola live on **a single Vercel project** with Supabase. ~30 minutes once you have the API keys.\n\n## Cost\n\n- Supabase free tier: 500 MB DB, 1 GB Storage, 50k MAU.\n- Vercel Hobby: 100 GB bandwidth/month, 100k function invocations/month, 60 s function `maxDuration`. Plenty.\n- OpenRouter: free models cost nothing per call (each is rate-limited; the rotation list handles that).\n- Pollinations.ai: free, no account.\n- Hugging Face Inference: free with a token; rate-limited.\n- Cloudflare Workers AI: free tier, ~3,300 FLUX-1-schnell images/day.\n- Resend: 3k emails/month free.\n\nTotal: **$0/month** at hackathon scale.\n\n---\n\n## Prerequisites\n\n- A GitHub repo with this code.\n- Free accounts: Supabase, OpenRouter, Resend, Vercel. Optional but recommended: Hugging Face, Cloudflare (image-gen fallbacks).\n\n---\n\n## 1. Supabase\n\n### 1.1 Create the project\n\n1. https://supabase.com/dashboard → \"New project\".\n2. Pick a region close to your users.\n3. Wait ~2 min for provisioning.\n\n### 1.2 Apply the schema\n\n1. Open **SQL Editor** in the dashboard.\n2. Paste and run [supabase/migrations/0001_init.sql](supabase/migrations/0001_init.sql). Single file, end-to-end:\n - tables (`profiles`, `saved_meals`, `sessions`, `session_meals`, `guests`, `votes`, `rate_log`, `service_status`, `events`, `aggregated_stats`, `error_log`),\n - RLS policies on every one of them,\n - RPCs (`cast_vote`, `ensure_guest`, `check_rate`, `record_service_outcome`, `record_event`, `record_error`, `get_weekly_digest_data`, `cleanup_after_digest`, `list_my_sessions`),\n - the `supabase_realtime` publication for live results,\n - the public-read `meal-images` Storage bucket,\n - and the daily pg_cron `atavola-cleanup-expired-sessions` job at 03:15 UTC.\n\nThe file is idempotent — running it twice is safe.\n\n### 1.3 Configure Auth\n\n**Authentication → URL Configuration**\n- Site URL: your future Vercel URL (you'll update once Vercel gives you the domain).\n- Redirect URLs: add `${SITE_URL}/auth/verify`.\n\n**Authentication → Email Templates → Magic Link**\n- Default English copy is fine. Keep `{{ .ConfirmationURL }}` as the link placeholder.\n\n**Authentication → SMTP Settings**\n- Enable \"Custom SMTP\" and point it at Resend.\n ```\n Host: smtp.resend.com\n Port: 465\n User: resend\n Password: <Resend API key>\n Sender: <verified domain> (or onboarding@resend.dev for testing)\n Sender name: atavola\n ```\n\n### 1.4 Grab the keys\n\n**Settings → API**, copy:\n\n- Project URL → `SUPABASE_URL` and `VITE_SUPABASE_URL`.\n- `anon` public key → `VITE_SUPABASE_ANON_KEY`.\n- `service_role` secret → `SUPABASE_SERVICE_ROLE_KEY` (server-only, never expose).\n\n---\n\n## 2. OpenRouter\n\n1. https://openrouter.ai → sign up.\n2. **Keys** → \"Create Key\". Copy the `sk-or-v1-...` value into `OPENROUTER_API_KEY`.\n3. Browse https://openrouter.ai/models?max_price=0 and pick 3–5 free models. A solid starting set:\n ```\n OPENROUTER_MODELS=google/gemini-2.0-flash-exp:free,deepseek/deepseek-chat:free,meta-llama/llama-3.3-70b-instruct:free,mistralai/mistral-7b-instruct:free\n ```\n\n---\n\n## 3. Image-gen providers (optional but recommended)\n\n`IMAGE_PROVIDERS` is comma-separated, tried in order. Pollinations alone works; adding the others gives resilience.\n\n### 3.1 Pollinations.ai (no setup)\n\nAlready free, no account. Add `pollinations` to `IMAGE_PROVIDERS`.\n\n### 3.2 Hugging Face Inference\n\n1. https://huggingface.co/join → sign up (or use existing account).\n2. https://huggingface.co/settings/tokens → \"New token\", role \"Read\".\n3. Save as `HUGGINGFACE_API_TOKEN`.\n4. Add `huggingface` to `IMAGE_PROVIDERS`.\n\n### 3.3 Cloudflare Workers AI\n\n1. https://dash.cloudflare.com → sign up.\n2. **Account home** → copy the Account ID from the right rail → `CLOUDFLARE_ACCOUNT_ID`.\n3. **My Profile → API Tokens → Create Token → Custom token**:\n - Permissions: `Account → Workers AI → Read`.\n - Account Resources: include your account.\n4. Save the token as `CLOUDFLARE_API_TOKEN`.\n5. Add `cloudflare` to `IMAGE_PROVIDERS`.\n\nRecommended order: `pollinations,huggingface,cloudflare`. Pollinations is fastest; HF and CF are sturdier when Pollinations slows down.\n\n---\n\n## 4. Vercel\n\n### 4.1 Import the project\n\n1. https://vercel.com → \"Add New\" → \"Project\".\n2. Import the GitHub repo.\n3. Configure:\n - **Framework Preset**: Other (auto-detected from `vercel.json`).\n - **Root Directory**: `.` (repo root — leave default).\n - **Build / Output / Install** commands: leave at defaults; `vercel.json` overrides them.\n4. Don't deploy yet — set env vars first.\n\n### 4.2 Environment variables\n\nVercel → Project → Settings → Environment Variables. Add each, mark for Production + Preview + Development:\n\n```\nSERVICE_MODE=production\n\nSUPABASE_URL=\nSUPABASE_SERVICE_ROLE_KEY=\nVITE_SUPABASE_URL=\nVITE_SUPABASE_ANON_KEY=\n\nOPENROUTER_API_KEY=\nOPENROUTER_MODELS=\n\nIMAGE_PROVIDERS=pollinations,huggingface,cloudflare\nHUGGINGFACE_API_TOKEN=\nCLOUDFLARE_ACCOUNT_ID=\nCLOUDFLARE_API_TOKEN=\n\n# Weekly digest email (Vercel Cron → /api/_cron/weekly-stats)\nRESEND_API_KEY=\nEMAIL_FROM=atavola <no-reply@atavola.ch>\nCRON_SECRET=\nSTATS_EMAIL_RECIPIENTS=\n```\n\nNotes:\n\n- `EMAIL_FROM` is the sender for direct Resend HTTP calls (currently just the weekly digest). It's separate from the Supabase Auth → SMTP settings; magic-link mail still uses whatever you wired up there. The domain in `EMAIL_FROM` must be verified in Resend.\n- `CRON_SECRET` is what Vercel signs cron requests with as `Authorization: Bearer …`. Set it both in the env-vars block above **and** in **Settings → Cron Jobs** for the project. Mismatch → the cron 401s.\n- `STATS_EMAIL_RECIPIENTS` is a comma-separated list. Empty = the cron runs but skips sending (handy in staging).\n- The schedule itself lives in [`vercel.json`](vercel.json) (`crons` block). Default is every Monday 09:00 UTC; change it there and redeploy.\n- `VITE_API_URL` is intentionally absent — same-origin defaults to `/api` and that's correct here.\n\n### 4.3 Deploy\n\nClick \"Deploy\". ~1–2 minutes. Note the URL (e.g. `https://atavola.vercel.app`).\n\n### 4.4 Loop the URL back to Supabase\n\nGo back to **Supabase → Authentication → URL Configuration**: set Site URL and Redirect URL to the Vercel URL.\n\nIf you later add a custom domain, repeat this step with the new domain.\n\n---\n\n## 5. Smoke test\n\n```bash\ncurl https://your-app.vercel.app/api/health\n# → {\"status\":\"ok\",\"mode\":\"production\",...}\n```\n\nIn a private window:\n\n1. Open the Vercel URL.\n2. Sign in with magic link (check your inbox — see \"Resend domain\" gotcha below).\n3. Create a session: vibe + headcount + no pre-selected meals.\n4. Wait for the 4 cards to appear with images.\n5. Click \"Share voting link\". Try the **native share** button (mobile only) and the **copy link** button. Tapping the URL pill itself also copies. Try the **QR code**.\n6. In another private window, open the link, swipe through the 4 cards.\n7. In the host window, watch the live results re-rank.\n8. Heart a card → it appears under \"My Food\" in your profile dropdown.\n9. Reload the profile, drag to reorder, swipe a row left to delete.\n\nIf any step fails, check Vercel function logs (Project → Logs → click a function) and the browser console.\n\n---\n\n## 6. Custom domain (optional)\n\n1. Buy at any registrar (Cloudflare Registrar is at-cost).\n2. Vercel → Project → Settings → Domains → Add. Follow Vercel's DNS instructions.\n3. Once active, update **Supabase → Authentication → URL Configuration** to use the new domain.\n4. Update **Resend → Domains** to verify the new sending domain so magic-link emails work for everyone, not just yourself.\n\nNo code changes needed — the app reads URLs from env / runtime, never hardcoded.\n\n---\n\n## 7. Email deliverability (SPF / DKIM / DMARC)\n\nMagic-link delivery is the silent growth lever — if mail lands in spam, sign-ups die without you noticing. Resend's dashboard walks you through the DNS records; here's what to set on the apex domain (`atavola.ch`):\n\n1. **SPF** (`TXT @`) — tells receivers which servers may send mail \"as\" your domain.\n ```\n v=spf1 include:_spf.resend.com ~all\n ```\n `~all` (soft fail) is the right choice while you stabilise; switch to `-all` (hard fail) once you're confident no other systems send mail from atavola.ch.\n\n2. **DKIM** — Resend auto-generates a CNAME pair when you verify the domain. Copy them into DNS verbatim. They look like:\n ```\n resend._domainkey CNAME resend._domainkey.<region>.amazonses.com\n ```\n Wait for Resend to flip the domain status to \"verified\" (usually <10 min).\n\n3. **DMARC** (`TXT _dmarc`) — tells receivers what to do with mail that fails SPF / DKIM and where to report aggregate failures.\n ```\n v=DMARC1; p=none; rua=mailto:no-reply@atavola.ch; pct=100; aspf=r; adkim=r\n ```\n Start with `p=none` so you collect reports without rejecting legitimate mail. After a week of clean reports, tighten to `p=quarantine`, then `p=reject`.\n\n4. **Reverse DNS** is handled by Resend on their sending IPs — nothing for you to configure.\n\n5. **Verify**: send a magic-link to a Gmail address and inspect the headers. You want `dkim=pass`, `spf=pass`, `dmarc=pass`. [https://www.mail-tester.com/](https://www.mail-tester.com/) gives a 0–10 score; aim for 9+.\n\nThe same Resend domain powers the weekly digest (`/api/_cron/weekly-stats`). `EMAIL_FROM` must use this verified domain or sends bounce silently.\n\n---\n\n## 8. SEO submission\n\nThe static landing page, `/about`, `/privacy`, and `/terms` ship with full meta and JSON-LD; `robots.txt`, `sitemap.xml`, `llms.txt`, and `humans.txt` live in `frontend/public/`. After first deploy:\n\n1. **Google Search Console** — [https://search.google.com/search-console](https://search.google.com/search-console). Add the property (use DNS verification on atavola.ch so all subdomains are covered). Submit `https://atavola.ch/sitemap.xml`. Indexing usually starts within hours.\n2. **Bing Webmaster Tools** — [https://www.bing.com/webmasters](https://www.bing.com/webmasters). Same flow; Bing also feeds DuckDuckGo results.\n3. **Yandex Webmaster** (optional, only if you care about RU/CIS traffic).\n4. **Sitemap auto-discovery** — `robots.txt` already references the sitemap. Most crawlers find it automatically; the manual submission above just shortens the discovery window.\n5. **Validate**:\n - [Google Rich Results Test](https://search.google.com/test/rich-results) — paste each of `/`, `/about`, `/privacy`, `/terms` and confirm the JSON-LD blocks parse.\n - [Twitter / X Card Validator](https://cards-dev.twitter.com/validator) — paste the home URL, confirm the OG image renders.\n - [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/) — same.\n - [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) — same. Click \"Scrape Again\" if cached.\n - [securityheaders.com](https://securityheaders.com/) and [Mozilla Observatory](https://observatory.mozilla.org/) — both should score A or A+.\n - [PageSpeed Insights](https://pagespeed.web.dev/) — Core Web Vitals must be green for ranking.\n\n---\n\n## Common pitfalls\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Function logs say \"Refusing to boot: SERVICE_MODE=…\" | Vercel env var unset or misspelled | Set `SERVICE_MODE=production` exactly. The hard fail is intentional — see [SECURITY.md](SECURITY.md) #C1. |\n| Magic-link email never arrives for anyone but you | Resend domain not verified | Verify the domain in Resend, then point Supabase SMTP at it. Until verified, only your own address receives mail. |\n| 401 on every call | RLS denies because policies didn't run | Re-apply both migrations — verify policies under `Authentication → Policies`. |\n| `cast_vote` errors with `session_expired` or `session_not_open` | Session is older than 30 days or status moved off `voting` | Hosts create a new session; expiry is enforced server-side and the daily pg_cron purges past it anyway. |\n| `cast_vote` errors with `invalid_guest_token` | Guest hasn't been minted | Frontend should call `POST /api/votes/guest` once before voting (it does — clear localStorage and reload if stale). |\n| 429 on rate-limited endpoints | The Postgres rate-limit RPC threw | Check function logs for `[checkRate] RPC failed` warnings. The limiter fails open to avoid locking real users out, but a sustained warning means you're effectively unprotected — fix the underlying DB error. |\n| Generated images broken | Every image provider failed magic-byte sniff or quota | Inspect the function log — provider names + reasons are logged. Most common: wrong CF/HF key, or HF model warming up (just retry). |\n| Meal generation 503s | All OpenRouter free models hit their daily quota | Add more models to `OPENROUTER_MODELS`; rotation will skip past empty ones. |\n| Vercel function times out | LLM + 4 image fetches > 60 s | Drop a slow provider from `IMAGE_PROVIDERS` (HF when its servers are loading). Long-term: Vercel Pro = 300 s, or split generation into per-meal endpoints (out of scope today). |\n\n<!-- Made with Bob -->\n"}, "48641413b9fa4ad8d3aa40554a368ab1656e6828": {"t": 1785765365.6674185, "n": 14416, "h": "3ca9e88e056919f9", "c": "# 🚀 Group Food Tinder - Complete Deployment Guide\n\n## 📋 Overview\n\nThis guide will help you deploy your Group Food Tinder app online using **free services**. Yes, you can use Render's free tier!\n\n### What You'll Deploy\n- **Frontend**: React app (Vercel Free)\n- **Backend**: Node.js API (Render Free)\n- **Database**: Firebase Realtime Database (Free tier)\n- **AI**: OpenAI API (Pay-as-you-go, ~$0.002 per request)\n- **Email**: Gmail SMTP (Free)\n\n### Total Cost\n- **Development/Testing**: $0 (using mock mode)\n- **Production**: ~$5-10/month (mostly OpenAI usage)\n\n---\n\n## 🎯 Deployment Strategy\n\n### Option 1: Quick Demo (Recommended for Hackathon)\n**Time: 30 minutes**\n- Deploy with mock mode (no API keys needed)\n- Perfect for demos and testing\n- Zero cost\n\n### Option 2: Full Production\n**Time: 2 hours**\n- All services configured\n- Real AI generation\n- Email notifications\n- Best for actual use\n\n---\n\n## 📝 Step-by-Step Deployment Plan\n\n### Phase 1: Get Your API Keys (1 hour)\n\n#### 1.1 Firebase Setup (15 min)\n**What it does**: Database for sessions, votes, and user data\n\n**Steps:**\n1. Go to [Firebase Console](https://console.firebase.google.com)\n2. Click \"Add project\"\n3. Name it: `group-food-tinder`\n4. Disable Google Analytics (optional)\n5. Click \"Create project\"\n\n**Enable Realtime Database:**\n1. In left sidebar → Build → Realtime Database\n2. Click \"Create Database\"\n3. Choose location: `us-central1` (or closest to you)\n4. Start in **test mode** (we'll secure it later)\n5. Click \"Enable\"\n\n**Get Service Account Credentials:**\n1. Click gear icon → Project settings\n2. Go to \"Service accounts\" tab\n3. Click \"Generate new private key\"\n4. Download the JSON file\n5. Open it and copy these values:\n - `project_id` → FIREBASE_PROJECT_ID\n - `private_key` → FIREBASE_PRIVATE_KEY\n - `client_email` → FIREBASE_CLIENT_EMAIL\n\n**Security Rules (Important!):**\n```json\n{\n \"rules\": {\n \"sessions\": {\n \"$sessionId\": {\n \".read\": true,\n \".write\": \"auth != null || data.exists()\"\n }\n },\n \"votes\": {\n \"$sessionId\": {\n \".read\": true,\n \".write\": true\n }\n }\n }\n}\n```\n\n**Cost**: Free up to 1GB storage, 10GB/month downloads\n\n---\n\n#### 1.2 OpenAI API Setup (10 min)\n**What it does**: Generates creative meal suggestions\n\n**Steps:**\n1. Go to [OpenAI Platform](https://platform.openai.com)\n2. Sign up or log in\n3. Click your profile → \"View API keys\"\n4. Click \"Create new secret key\"\n5. Name it: `group-food-tinder`\n6. Copy the key (starts with `sk-`)\n7. **IMPORTANT**: Save it immediately (you can't see it again!)\n\n**Add Credits:**\n1. Go to Settings → Billing\n2. Add payment method\n3. Set usage limit: $5/month (recommended for testing)\n\n**Cost**: \n- GPT-3.5-turbo: ~$0.002 per meal generation\n- 100 sessions = ~$0.20\n- Recommended budget: $5-10/month\n\n---\n\n#### 1.3 Gmail SMTP Setup (10 min)\n**What it does**: Sends magic link emails for authentication\n\n**Steps:**\n1. Go to [Google Account](https://myaccount.google.com)\n2. Enable 2-Factor Authentication:\n - Security → 2-Step Verification → Get Started\n3. Generate App Password:\n - Security → 2-Step Verification → App passwords\n - Select app: \"Mail\"\n - Select device: \"Other\" → Name it \"Group Food Tinder\"\n - Click \"Generate\"\n - Copy the 16-character password\n\n**Alternative Email Services:**\n- **SendGrid**: 100 emails/day free\n- **Mailgun**: 5,000 emails/month free\n- **AWS SES**: 62,000 emails/month free (if on AWS)\n\n**Cost**: Free (Gmail has daily limits ~500 emails/day)\n\n---\n\n#### 1.4 JWT Secrets (5 min)\n**What it does**: Secure token generation\n\n**Generate Random Secrets:**\n```bash\n# Run these commands in your terminal\nnode -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"\n# Copy output for JWT_SECRET\n\nnode -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"\n# Copy output for MAGIC_LINK_SECRET\n```\n\n---\n\n### Phase 2: Deploy Backend to Render (30 min)\n\n#### 2.1 Prepare Backend for Deployment (10 min)\n\n**1. Create `backend/.env.production` file:**\n```env\n# Server Configuration\nPORT=3000\nNODE_ENV=production\nFRONTEND_URL=https://your-app.vercel.app\n\n# Service Mode\nSERVICE_MODE=production\n\n# Firebase Admin SDK\nFIREBASE_PROJECT_ID=your_project_id\nFIREBASE_PRIVATE_KEY=\"-----BEGIN PRIVATE KEY-----\\nYour private key here\\n-----END PRIVATE KEY-----\\n\"\nFIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your_project_id.iam.gserviceaccount.com\n\n# OpenAI API\nOPENAI_API_KEY=sk-your_openai_api_key_here\n\n# Email Configuration\nSMTP_HOST=smtp.gmail.com\nSMTP_PORT=587\nSMTP_SECURE=false\nSMTP_USER=your_email@gmail.com\nSMTP_PASS=your_16_char_app_password\nEMAIL_FROM=noreply@groupfoodtinder.com\n\n# JWT Configuration\nJWT_SECRET=your_generated_jwt_secret_here\nJWT_EXPIRES_IN=7d\n\n# Magic Link Configuration\nMAGIC_LINK_SECRET=your_generated_magic_link_secret_here\nMAGIC_LINK_EXPIRES_IN=15m\n\n# Logging\nLOG_LEVEL=info\n```\n\n**2. Verify `backend/package.json` has these scripts:**\n```json\n{\n \"scripts\": {\n \"start\": \"node dist/index.js\",\n \"build\": \"tsc\",\n \"dev\": \"nodemon src/index.ts\"\n }\n}\n```\n\n---\n\n#### 2.2 Deploy to Render (20 min)\n\n**Why Render?**\n- ✅ Free tier available\n- ✅ Automatic deployments from GitHub\n- ✅ Easy environment variable management\n- ✅ Built-in SSL certificates\n\n**Steps:**\n\n1. **Push to GitHub** (if not already done):\n ```bash\n git add .\n git commit -m \"Prepare for deployment\"\n git push origin neon\n ```\n\n2. **Sign up for Render**:\n - Go to [Render.com](https://render.com)\n - Sign up with GitHub\n - Authorize Render to access your repositories\n\n3. **Create New Web Service**:\n - Click \"New +\" → \"Web Service\"\n - Connect your GitHub repository\n - Select `ibm-bob-hackathon` repo\n - Click \"Connect\"\n\n4. **Configure Service**:\n ```\n Name: group-food-tinder-api\n Region: Oregon (US West) or closest to you\n Branch: neon\n Root Directory: backend\n Runtime: Node\n Build Command: npm install && npm run build\n Start Command: npm start\n Instance Type: Free\n ```\n\n5. **Add Environment Variables**:\n - Click \"Environment\" tab\n - Click \"Add Environment Variable\"\n - Add ALL variables from your `.env.production` file\n - **IMPORTANT**: For `FIREBASE_PRIVATE_KEY`, paste the entire key including `\\n` characters\n\n6. **Deploy**:\n - Click \"Create Web Service\"\n - Wait 5-10 minutes for deployment\n - Your API will be at: `https://group-food-tinder-api.onrender.com`\n\n**Render Free Tier Limitations:**\n- ⚠️ Spins down after 15 minutes of inactivity\n- ⚠️ First request after spin-down takes 30-60 seconds\n- ⚠️ 750 hours/month free (enough for one service)\n- ✅ Perfect for demos and low-traffic apps\n\n**Pro Tip**: Keep your service alive with a cron job:\n```bash\n# Use cron-job.org to ping your API every 14 minutes\ncurl https://group-food-tinder-api.onrender.com/api/health\n```\n\n---\n\n### Phase 3: Deploy Frontend to Vercel (20 min)\n\n#### 3.1 Prepare Frontend (5 min)\n\n**1. Create `frontend/.env.production`:**\n```env\nVITE_API_URL=https://group-food-tinder-api.onrender.com/api\n```\n\n**2. Update `frontend/vite.config.ts` (if needed):**\n```typescript\nimport { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n plugins: [react()],\n server: {\n port: 5173\n },\n build: {\n outDir: 'dist',\n sourcemap: false\n }\n})\n```\n\n---\n\n#### 3.2 Deploy to Vercel (15 min)\n\n**Why Vercel?**\n- ✅ Free tier for personal projects\n- ✅ Automatic HTTPS\n- ✅ Global CDN\n- ✅ Instant deployments\n- ✅ Perfect for React apps\n\n**Steps:**\n\n1. **Sign up for Vercel**:\n - Go to [Vercel.com](https://vercel.com)\n - Sign up with GitHub\n - Authorize Vercel\n\n2. **Import Project**:\n - Click \"Add New...\" → \"Project\"\n - Import `ibm-bob-hackathon` repository\n - Click \"Import\"\n\n3. **Configure Project**:\n ```\n Framework Preset: Vite\n Root Directory: frontend\n Build Command: npm run build\n Output Directory: dist\n Install Command: npm install\n ```\n\n4. **Add Environment Variables**:\n - Click \"Environment Variables\"\n - Add: `VITE_API_URL` = `https://group-food-tinder-api.onrender.com/api`\n - Click \"Add\"\n\n5. **Deploy**:\n - Click \"Deploy\"\n - Wait 2-3 minutes\n - Your app will be at: `https://your-app.vercel.app`\n\n6. **Update Backend CORS**:\n - Go back to Render dashboard\n - Update `FRONTEND_URL` environment variable\n - Set to: `https://your-app.vercel.app`\n - Redeploy backend\n\n---\n\n### Phase 4: Testing & Verification (20 min)\n\n#### 4.1 Test Checklist\n\n**Backend Health Check:**\n```bash\ncurl https://group-food-tinder-api.onrender.com/api/health\n# Should return: {\"status\":\"ok\",\"mode\":\"production\"}\n```\n\n**Frontend Access:**\n1. Open `https://your-app.vercel.app`\n2. Should see landing page\n3. Check browser console for errors\n\n**Full Flow Test:**\n1. ✅ Request magic link (check email)\n2. ✅ Click magic link (should authenticate)\n3. ✅ Create session with vibe and headcount\n4. ✅ AI generates meals (wait 30-60 seconds)\n5. ✅ Share link works\n6. ✅ Voting interface loads\n7. ✅ Swipe cards work\n8. ✅ Winner displays correctly\n\n---\n\n#### 4.2 Common Issues & Fixes\n\n**Issue: Backend takes 30+ seconds to respond**\n- **Cause**: Render free tier cold start\n- **Fix**: Normal behavior, or upgrade to paid tier ($7/month)\n\n**Issue: CORS errors in browser**\n- **Cause**: FRONTEND_URL mismatch\n- **Fix**: Update FRONTEND_URL in Render to match Vercel URL exactly\n\n**Issue: Firebase permission denied**\n- **Cause**: Security rules too strict\n- **Fix**: Update Firebase rules (see Phase 1.1)\n\n**Issue: OpenAI rate limit errors**\n- **Cause**: Too many requests\n- **Fix**: Add rate limiting or upgrade OpenAI plan\n\n**Issue: Email not sending**\n- **Cause**: Gmail app password incorrect\n- **Fix**: Regenerate app password, update SMTP_PASS\n\n---\n\n### Phase 5: Custom Domain (Optional, 10 min)\n\n#### 5.1 Add Custom Domain to Vercel\n\n**Steps:**\n1. Buy domain from Namecheap, GoDaddy, etc. (~$10/year)\n2. In Vercel dashboard → Settings → Domains\n3. Add your domain: `groupfoodtinder.com`\n4. Follow DNS configuration instructions\n5. Wait 24-48 hours for DNS propagation\n\n**Free Domain Options:**\n- Use Vercel's free subdomain: `your-app.vercel.app`\n- Use Render's free subdomain: `your-api.onrender.com`\n\n---\n\n## 🎭 Deployment Modes Comparison\n\n### Mock Mode (Free, Instant)\n```bash\n# Deploy with mock mode for demos\nSERVICE_MODE=mock\n```\n**Pros:**\n- ✅ Zero cost\n- ✅ No API keys needed\n- ✅ Instant deployment\n- ✅ Perfect for demos\n\n**Cons:**\n- ❌ No real AI generation\n- ❌ No email sending\n- ❌ Data resets on restart\n\n---\n\n### Production Mode (Paid, Full Features)\n```bash\nSERVICE_MODE=production\n```\n**Pros:**\n- ✅ Real AI meal generation\n- ✅ Email notifications\n- ✅ Persistent data\n- ✅ Production-ready\n\n**Cons:**\n- ❌ Requires API keys\n- ❌ ~$5-10/month cost\n- ❌ More setup time\n\n---\n\n## 💰 Cost Breakdown\n\n### Free Tier (Mock Mode)\n| Service | Cost | Limits |\n|---------|------|--------|\n| Render | $0 | 750 hours/month, cold starts |\n| Vercel | $0 | 100GB bandwidth/month |\n| **Total** | **$0/month** | Perfect for demos |\n\n### Production Tier\n| Service | Cost | Usage |\n|---------|------|-------|\n| Render | $0 | Free tier |\n| Vercel | $0 | Free tier |\n| Firebase | $0 | Free tier (1GB storage) |\n| OpenAI | ~$5-10 | ~2,500 meal generations |\n| Gmail | $0 | Free (500 emails/day limit) |\n| **Total** | **~$5-10/month** | Light usage |\n\n### Scaling Costs\n| Users/Month | OpenAI Cost | Render Cost | Total |\n|-------------|-------------|-------------|-------|\n| 100 | ~$2 | $0 | ~$2 |\n| 1,000 | ~$20 | $7 | ~$27 |\n| 10,000 | ~$200 | $25 | ~$225 |\n\n---\n\n## 🚀 Quick Deploy Commands\n\n### Deploy Everything (Mock Mode)\n```bash\n# 1. Push to GitHub\ngit add .\ngit commit -m \"Deploy to production\"\ngit push origin neon\n\n# 2. Deploy backend to Render\n# (Use Render dashboard - see Phase 2)\n\n# 3. Deploy frontend to Vercel\n# (Use Vercel dashboard - see Phase 3)\n```\n\n### Update Deployment\n```bash\n# Just push to GitHub - auto-deploys!\ngit add .\ngit commit -m \"Update feature\"\ngit push origin neon\n```\n\n---\n\n## 📊 Monitoring & Maintenance\n\n### Check Backend Logs\n1. Go to Render dashboard\n2. Click your service\n3. Click \"Logs\" tab\n4. Monitor for errors\n\n### Check Frontend Logs\n1. Go to Vercel dashboard\n2. Click your project\n3. Click \"Deployments\"\n4. Click latest deployment → \"View Function Logs\"\n\n### Set Up Alerts\n**Render:**\n- Settings → Notifications\n- Add email for deployment failures\n\n**Vercel:**\n- Settings → Notifications\n- Enable deployment notifications\n\n---\n\n## 🔒 Security Checklist\n\n- [ ] All API keys in environment variables (not in code)\n- [ ] Firebase security rules configured\n- [ ] CORS properly configured\n- [ ] JWT secrets are random and secure\n- [ ] HTTPS enabled (automatic on Vercel/Render)\n- [ ] Rate limiting enabled\n- [ ] Input validation on all endpoints\n- [ ] No sensitive data in logs\n\n---\n\n## 🎯 Recommended Deployment Path\n\n### For Hackathon Demo (30 min)\n1. ✅ Deploy backend in **mock mode** to Render\n2. ✅ Deploy frontend to Vercel\n3. ✅ Test basic flow\n4. ✅ Present demo\n\n### For Production (2 hours)\n1. ✅ Get all API keys (Phase 1)\n2. ✅ Deploy backend in **production mode** to Render\n3. ✅ Deploy frontend to Vercel\n4. ✅ Test full flow with real services\n5. ✅ Monitor and optimize\n\n---\n\n## 📞 Support & Resources\n\n### Documentation\n- [Render Docs](https://render.com/docs)\n- [Vercel Docs](https://vercel.com/docs)\n- [Firebase Docs](https://firebase.google.com/docs)\n- [OpenAI Docs](https://platform.openai.com/docs)\n\n### Troubleshooting\n- Check backend logs in Render\n- Check frontend logs in Vercel\n- Check browser console for errors\n- Test API endpoints with Postman/curl\n\n### Community\n- GitHub Issues\n- Stack Overflow\n- Discord/Slack channels\n\n---\n\n## ✅ Final Checklist\n\n### Before Demo\n- [ ] Backend deployed and responding\n- [ ] Frontend deployed and accessible\n- [ ] Test magic link authentication\n- [ ] Test session creation\n- [ ] Test voting flow\n- [ ] Test winner determination\n- [ ] Prepare backup screenshots\n- [ ] Practice demo script\n\n### After Demo\n- [ ] Monitor usage and costs\n- [ ] Set up error tracking (Sentry)\n- [ ] Add analytics (Google Analytics)\n- [ ] Collect user feedback\n- [ ] Plan improvements\n\n---\n\n## 🎉 You're Ready to Deploy!\n\n**Next Steps:**\n1. Choose your deployment mode (mock or production)\n2. Follow the step-by-step guide above\n3. Test thoroughly\n4. Share your app with the world!\n\n**Questions?** Check the troubleshooting section or open an issue on GitHub.\n\n**Good luck with your deployment! 🚀**"}}}} |