@ketopath/shared — new module profile/schema:
- profileInputSchema (Zod): age 18-110, gender, height 120-230 cm, weights
35-300 kg, activityLevel, optional targetDate
- GENDERS / ACTIVITY_LEVELS string-literal arrays for UI iteration
- Re-exported from @ketopath/shared
apps/api — new module modules/profile:
- PUT /me/profile: requireAuth, validates body via profileInputSchema, upserts
the row, returns the saved profile + derived { bmr, tdee, activityMultiplier }
(BMR/TDEE computed via @ketopath/shared)
- GET /me/profile: requireAuth, returns the same shape, 404 when missing
- Decimal columns serialised back as numbers
apps/web — new /profile page:
- src/app/[locale]/profile/page.tsx (server): redirects unauthenticated users
to /sign-in, calls fetchProfile to hydrate the form
- profile-form.tsx (client): react-hook-form + zodResolver bound to the same
shared schema, native styled selects for gender/activityLevel until shadcn
Select arrives, post-submit panel showing BMR/TDEE/multiplier
- actions.ts: server actions saveProfile / fetchProfile that proxy the call
to API_URL via cookie passthrough (no CORS, no exposed token)
- Home page gains a "Completa il tuo profilo" CTA when signed in
- API_URL env var added to .env.example
- Italian copy in messages/it.json under Profile
Verified end-to-end with the "Michele" PRD persona (49, M, 170cm, 76kg, SEDENTARY):
BMR = 1583 kcal, TDEE = 1899 kcal, multiplier 1.2 — matches the unit tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add shadcn/ui plumbing:
- components.json (style: new-york, baseColor: slate, primary: green ~142 71%)
- src/lib/utils.ts with cn() helper (clsx + tailwind-merge)
- Tailwind config switched to CSS variables theming with primary/destructive/
muted/card etc. tokens, dark-mode class, tailwindcss-animate plugin
- src/styles/globals.css declares :root and .dark variable palettes
Initial component set under src/components/ui:
- Button (cva variants: default/destructive/outline/secondary/ghost/link, asChild)
- Input
- Label (Radix Label)
- Card + CardHeader/Title/Description/Content/Footer
Refactor existing auth surface to the new primitives:
- sign-in / sign-up pages wrap form in a Card with Title/Description (+ footer
for sign-up disclaimer)
- sign-in-form / sign-up-form use Input, Label, Button; tokens replace bespoke
emerald/slate classes; divider and "Continua con Google" use the same Button
- Home page CTAs become Button asChild around Link; SignOutButton uses Button
- Hide-Google logic still works: enabledSocialProviders is now
ReadonlyArray<SocialProvider> for ergonomic .includes() checks
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>
Unit tests on @ketopath/auth (added to the Vitest workspace):
- readAuthEnv accepts a valid configuration
- rejects secret < 32 chars and non-URL BETTER_AUTH_URL
- preserves Google credentials when both vars are set
Playwright e2e/auth.spec.ts:
- happy path: sign-up creates a user, redirects home with welcome message,
sign-out clears session, sign-in with the same credentials restores it
- error path: invalid credentials surface a role="alert" message
- each test uses a unique generated email to avoid DB collisions
home.spec.ts: links instead of buttons (CTAs are now next/link).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- authPlugin runs preHandler that turns Fastify request headers into a Headers
object, calls auth.api.getSession, and decorates request.user / request.session
- requireAuth() preHandler short-circuits with 401 when not signed in
- New module modules/me with GET /me returning the authenticated user/session
- env validates BETTER_AUTH_SECRET (≥32) and BETTER_AUTH_URL — must match web
- Restored .js extensions in shared packages so NodeNext-resolution consumers
(api) typecheck cleanly; Next webpack now uses extensionAlias to map .js → .ts
- Re-enabled NodeNext for packages/auth and packages/db tsconfigs
Verified end-to-end:
- POST /api/auth/sign-in/email on web returns session cookie
- GET /me on api with the cookie returns 200 + user/session
- GET /me without the cookie returns 401
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web app integration:
- /api/auth/[...all] route exposes Better Auth handler
- src/lib/auth.ts: server helper getServerSession() reading cookies via headers()
- src/lib/auth-client.ts: browser auth client built on shared makeAuthClient
- (auth) route group with sign-in and sign-up pages, both with email+password
form and "Continua con Google" button (Google flow active when env is set)
- Home page becomes async, shows signed-in user name with sign-out button or
routes to sign-up/sign-in CTAs
- Italian copy in messages/it.json under Auth.SignIn / Auth.SignUp
- transpilePackages includes @ketopath/auth
- .env.example: BETTER_AUTH_SECRET, BETTER_AUTH_URL, DATABASE_URL, Google placeholders
Build tooling:
- Drop .js extensions from internal package imports so Next webpack can
transpile them; switch packages/db tsconfig from NodeNext to Bundler
resolution to keep TS happy (same as packages/auth)
Verified end-to-end via browser:
- /sign-up creates user (Postgres rows in users + accounts), session cookie set,
redirect to / shows "Accesso effettuato come Mario Test"
- /sign-out clears session, page falls back to anonymous CTAs
- /sign-in with the same credentials restores session
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New workspace packages/auth exporting:
- auth: Better Auth server instance using Prisma adapter on @ketopath/db
- makeAuthClient: React client factory parameterised by baseURL
- readAuthEnv: Zod-validated env reader (BETTER_AUTH_SECRET min 32 chars,
BETTER_AUTH_URL, optional GOOGLE_CLIENT_ID/SECRET)
- emailAndPassword enabled with min 8 chars, requireEmailVerification: false
for MVP (re-enable in V1, see ADR 0001)
- Google provider conditionally registered when both env vars are present —
keeps the package usable before OAuth configuration is delivered
- 7-day sessions, 24h rolling refresh, cookie prefix 'ketopath'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- User extended with emailVerified, name, image (Better Auth fields)
- Session: signed session tokens with expiry, ip and user agent
- Account: credential rows for email+password (provider 'credential') and
OAuth providers (e.g. Google) — password hash stored on the credential row
- Verification: single-use tokens for email verification, password reset,
and magic links
- Re-export new types from @ketopath/db
Migration 20260429104721_add_auth_tables run against local Postgres.
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>
- Add @ketopath/db and fastify-plugin to apps/api
- prismaPlugin decorates the Fastify instance with prisma and disconnects
on app close, with FastifyInstance type augmented in src/types/fastify.d.ts
- DATABASE_URL is now required by env validation
- New module modules/db with GET /db/health running SELECT 1 via Prisma
- dev script switched to tsx --env-file=.env for env loading
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by `prisma migrate dev --name init` against the local Postgres.
Creates the 6 enums and the 3 tables with their unique indexes and
foreign keys (profiles.user_id and preferences.user_id with ON DELETE CASCADE).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vitest:
- Workspace at root (vitest.workspace.ts) running per-package configs
- packages/shared/vitest.config.ts with v8 coverage and 70% thresholds
- Root scripts: test / test:watch / test:coverage
Nutrition module in @ketopath/shared:
- calculateBmr (Mifflin-St Jeor) + calculateTdee with activity multipliers
- ACTIVITY_MULTIPLIERS constants matching CLAUDE.md domain knowledge
- 14 unit tests covering Michele/Laura PRD personas, all sex variants,
every activity level, and input validation (100% coverage on src/nutrition)
Playwright in apps/web:
- chromium-only project, webServer auto-boot of pnpm dev
- e2e/home.spec.ts smoke test asserts Italian copy and disclaimer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial schema covers only the 3 base entities from PRD §8 — additional
models (MealPlan, Recipe, WeightEntry, FastEvent, ShoppingList, etc.)
will be added alongside their respective features.
- Postgres datasource via DATABASE_URL
- Enums: Role, Gender, ActivityLevel, Phase, CookingTime, FastingProtocol
- Singleton PrismaClient export from @ketopath/db
- Root scripts: db:generate / db:migrate / db:studio / db:seed / db:format
- Seed stub at prisma/seed.ts
- Ignore .claude/settings.local.json (per-user CLI permissions)
The first migration must be run by the developer:
pnpm db:migrate --name init
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ESM Fastify 4 with Pino logger (pretty-print in dev only)
- Plugins: helmet, cors (allowlist via CORS_ORIGINS), rate-limit, sensible
- Zod-validated environment config in src/config/env.ts
- Modular structure under src/modules with /health route
- tsx watch for dev, tsc for build, dist/server.js as production entry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- App Router under src/app/[locale] with locale 'it' as default
- next-intl middleware and getRequestConfig with setRequestLocale
- Tailwind CSS with shared font variable and content from packages/ui
- ESLint via @ketopath/eslint-config/nextjs, tsconfig via @ketopath/tsconfig/nextjs
- Inter font, base layout, home page with translated copy and disclaimer
Tweaks to shared eslint-config:
- Disable consistent-type-imports for .cjs files (next parser is not @typescript-eslint)
- Configure import resolver to discover tsconfig in apps/* and packages/*
- Drop *.config.* ignore patterns (overrides handle them with env.node)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>