Due colpevoli scoperti durante il deploy live:
- `NEXT_PUBLIC_APP_URL` mancava nel runbook ed in .env.example. Il client di Better Auth (lib/auth-client.ts) la usa come baseURL — senza, fallback a localhost:3000 → CORS error sul click di "Continua con Google". Va inlined al build (è NEXT_PUBLIC_*), quindi modificarla richiede rebuild.
- Setup Google OAuth: il bottone è SSG (prerendered al build), quindi aggiungere le env GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET non basta — serve rebuild perché `enabledSocialProviders` è valutato a build-time.
Aggiunta una sezione 4.3.bis dedicata al setup Google con i punti critici evidenziati.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guida step-by-step per deployare KetoPath sul server `lamiadieta.luzaonline.net`: preparazione VPS, PostgreSQL 15+ con backup, env, build, PM2, reverse proxy Apache + Let's Encrypt sui due sottodomini (web + api), webhook Stripe pubblico, operazioni quotidiane, troubleshooting. Riusabile come runbook per futuri deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chiude la decisione "payment provider" aperta in CLAUDE.md.
**Modello**: free 30gg post-signup (no carta richiesta) → Pro mensile €9,90 / annuale €89. Allinea il paywall al confine fra fase INTENSIVE e TRANSITION (PRD §5.1), quando l'utente ha già visto i primi risultati. Dopo la scadenza l'app non si "spegne": storico restano consultabili (sola lettura), ma generazione piani / nuove pesate / digiuni / foto / export richiedono abbonamento attivo.
**Schema**: nuova tabella `subscriptions` (1:1 con users) con stati TRIALING/ACTIVE/PAST_DUE/CANCEL_AT_PERIOD_END/CANCELED/EXPIRED + `billing_webhook_events` per idempotenza dei retry Stripe.
**Backend** (`apps/api/src/modules/billing/`):
- `GET /me/billing/status` — snapshot + derived (kind, isPro, trialDaysRemaining)
- `POST /me/billing/checkout` — crea Stripe Checkout Session (subscription mode + Stripe Tax + tax_id_collection)
- `POST /me/billing/portal` — Customer Portal Session
- `POST /webhooks/stripe` — raw body, firma HMAC, idempotenza per `event.id`, dispatch su `customer.subscription.*`, `checkout.session.completed`, `invoice.payment_failed`
- Plugin `requirePro()` (402 payment_required) applicato a 9 rotte: meal-plans CRUD, weight-entries POST, check-ins POST, fast-events POST/PATCH, fasting/pause POST, export.pdf (plan+tracking)
**Shared** (`@ketopath/shared/billing/pro-status`):
- `isProActive(snap)` — verifica live (gestisce anche TRIALING con `trialEndsAt` passato in caso di cron in ritardo)
- `deriveProStatus(snap)` — kind + isPro + trialDaysRemaining + accessEndsAt per l'UI
- `computeTrialEndsAt(signupAt, days=30)`
- 11 unit test
**Frontend** (`apps/web/src/app/[locale]/billing/`):
- Pagina `/billing` editoriale (capitolo VIII) con StatusBlock per ogni kind, BillingActionsBar client (transitions, redirect a Stripe), 3 benefits
- `<TrialBanner>` in SignedInDashboard (3 stati: trial in corso oro / scaduto pomodoro / past_due pomodoro)
- Nav item "Abbonamento" (chapter VI) nel grid asimmetrico
- i18n IT completo (`Billing` namespace)
**Soft-degradation**: env Stripe (`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID_*`, `BILLING_RETURN_URL`) sono tutte opzionali. Senza configurazione i route billing rispondono 503 e il banner trial mostra "pagamenti non ancora attivati" — utenti in trial continuano a usare l'app.
99/99 test verdi, lint pulito su tutto il monorepo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Font: Geist (Vercel) sostituisce Fraunces/Inter Tight; GeistMono al posto di JetBrains Mono. `--font-display`, `--font-sans`, `--font-mono` mappati tutti su Geist via alias CSS — vocabolario tipografico unificato sans-serif moderno.
- Layout: rimosso `mx-auto max-w-7xl` da 7 pagine app (root, onboarding, profile, plan, tracking, fasting, shopping, recipe detail). Sostituito con `w-full px-6 sm:px-10 lg:px-16 xl:px-24` — edge-to-edge, ma respiro su 4K. Welcome/sign-in/sign-up restano stretti (testo lungo / form login).
- Tailwind: fontFamily fallback display da `Georgia, serif` a `system-ui` sans. Rimosse le OpenType features di Fraunces (`cv11`, `dlig`) — Geist usa `ss01`/`ss03`.
- Cleanup: rimosso `docs/~\$D_KetoPath.docx` (Word lockfile residuo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tre leve aggiuntive che migliorano la personalizzazione del piano dopo
le tre del commit precedente (BF%, deficit, condizioni mediche).
1. Storia delle diete precedenti (adattamento metabolico)
- Profile.dietHistory ∈ {NONE,SOME,EXTENSIVE,SEVERE} cifrato at-rest
- bmrAdjustForDietHistory: 0.97 / 0.93 / 0.85 — aggiustamento BMR per
riflettere l'adattamento metabolico osservato in studi a lungo termine
(Fothergill 2016 Biggest Loser, Rosenbaum 2008)
- Select in ProfileForm con copy diretto sulla "questione yo-yo"
2. Schedule allenamento (kcal extra training-day)
- Preferences.trainingDays Int[] (0=Lun..6=Dom)
- Preferences.trainingType ∈ {CARDIO,STRENGTH,MIXED,SPORT}
- Preferences.sessionMinutes Int?
- extraKcalForSession: equazione MET (Ainsworth 2011) con MET prevalenti
8/5/6/7 a tipo. Output arrotondato a 25 kcal per evitare false-precisioni
- Plan generation aggiunge l'extra solo nei training days
- Chips Lun-Dom + select tipo + input durata in PreferencesForm
3. Frequenza pasti preferita
- Preferences.mealsPerDay Int? (1..4)
- mealShareForFrequency: 1=solo cena, 2=pranzo+cena, 3=no spuntino, 4=default
- Quando mealsPerDay è impostato e non c'è fastingProtocol attivo,
sostituisce la share del protocollo (il digiuno IF ha precedenza)
- Slot a share=0 saltati alla creazione (niente colazione/spuntino "vuoti")
Schema migration 20260429220XXX_add_lifestyle_fields.
Smoke verificato via API: con mealsPerDay=3 e nessun protocollo IF, ogni
giorno del piano ha esattamente 3 pasti (Colazione+Pranzo+Cena, niente
Spuntino). 54/54 unit test verdi (11 nuovi).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR 0003: VAPID self-hosted today, DeviceToken model agnostic to
platform so iOS/Android (Expo/APNs/FCM) plug in as new senders later.
Backend (apps/api/src/modules/notifications)
- sender.ts: NotificationSender interface, WebPushSender via VAPID
- notifications.routes.ts: GET /me/notifications/config, POST/DELETE
/me/device-tokens, PATCH /me/notifications/settings, POST /me/notifications/test
- scheduler.ts: node-cron Mon 09:00 Europe/Rome for weekly weigh-in
reminder; auto-cleanup of expired tokens on 404/410
- env: VAPID_PUBLIC_KEY/PRIVATE_KEY/SUBJECT (all optional → push gracefully off)
Frontend
- public/sw.js minimal (push + notificationclick)
- lib/notifications/push-client.ts: subscribe / unsubscribe / getCurrentSubscription
- profile/notifications-{actions,panel}.tsx: editorial panel with toggles,
device list, "send test", per-device removal
- pushReady requires both permission AND active subscription (covers the
case where the user revoked the SW but kept the browser permission)
Schema
- DeviceToken { userId, platform, endpoint, p256dh, auth, token, userAgent,
createdAt, lastSeenAt } with unique(userId, endpoint)
- ExtendedPrismaClient type exported from @ketopath/db
- NotificationSettings zod schema in @ketopath/shared
Tooling
- lint-staged: split .js out of eslint glob so service worker is only
formatted (it lives outside the TS project)
i18n
- Notifications namespace (it) with typed error keys
Smoke tested: POST /me/device-tokens 201, POST /me/notifications/test 200,
real push delivered to a macOS Chrome device.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks application-level field encryption via prisma-field-encryption
(AES-256-GCM, master key from KMS) as the primary defence, combined with
volume encryption on the production Postgres host as defence in depth.
Documents:
- which fields are sensitive now (Profile.weight*, Profile.targetDate) and
which arrive in V1 (WeightEntry, FastEvent, ProgressPhoto)
- which fields stay in clear text and why (email/login, age/gender for
aggregate analytics, height/activity for plan generation)
- alternatives rejected: pgcrypto (key in queries), volume-only (no app-level
protection), client-side E2E (kills BMR/TDEE server-side calculation)
- consequences and the implementation roadmap for a follow-up PR
Status: proposed — must be implemented before opening V1 to public users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Export enabledSocialProviders from @ketopath/auth: ['google'] when
GOOGLE_CLIENT_ID/SECRET are both set, [] otherwise
- sign-in and sign-up pages read enabledSocialProviders server-side and pass
googleEnabled to their forms; the divider and button disappear when false
- No changes to the Better Auth instance — the Google provider is still
conditionally registered, this just keeps the UI honest about it
docs/runbooks/google-oauth-setup.md walks through the Google Cloud Console
flow end-to-end (project, consent screen, credentials, redirect URIs, env
injection, verification, troubleshooting, prod considerations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reasons (full ADR in docs/decisions/0001-auth-provider.md):
- KetoPath targets EU users with Art. 9 GDPR data; Clerk on US infra
raises Schrems II concerns
- Free, self-hostable, stays in our Postgres
- TypeScript-first, fits Next.js + Fastify + Prisma stack
CLAUDE.md updated:
- Tech stack: Better Auth on EU Postgres
- "Cosa NON fare mai": no manual password hashing
- References: Better Auth docs link
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>