## Context CandleAnnotator is a single-user full-stack app: Next.js 16 (App Router) + PostgreSQL (Drizzle ORM) + Python FastAPI ML service. All data is global — no users table, no auth, no data isolation. The entire app lives at `/` as a single page with 13 API routes. We need to add multi-user support: registration, login, per-user data isolation. Lovable design mockups exist for landing, login, and register pages. The existing app workspace moves behind auth. **Current stack**: Next.js 16, React 19, Drizzle ORM, PostgreSQL 16, Tailwind CSS, shadcn/ui, lightweight-charts, FastAPI (Python). ## Goals / Non-Goals **Goals:** - Email/password registration and login - Google OAuth login - Per-user data isolation (charts, annotations, models, settings) - Landing page, login page, register page (from Lovable designs) - User settings page (change password, display name, delete account) - Auth middleware protecting app routes and API routes - Smooth migration path for existing single-user data **Non-Goals:** - Email verification (defer — add later) - Password reset via email (defer — requires email service) - Admin panel / user management - Role-based access control - Rate limiting - Two-factor authentication - Social login beyond Google (GitHub, Discord, etc.) - Team/organization accounts - API key authentication for external integrations ## Decisions ### 1. Auth library: Auth.js v5 (NextAuth v5) **Choice**: Auth.js v5 (`next-auth@5`) with JWT strategy **Why**: Native App Router support, built-in Credentials + Google OAuth providers, `@auth/drizzle-adapter` exists for our ORM. JWT strategy avoids extra session table and keeps the ML service integration simple (pass user ID in headers). **Alternatives considered**: - **Lucia Auth**: Good but deprecated in favor of rolling your own. More manual setup. - **Clerk/Auth0**: SaaS dependency, cost, overkill for this project. - **Custom JWT**: More control but reinventing auth middleware, CSRF protection, etc. **Session strategy**: JWT (not database sessions). User ID embedded in token. Stateless, no session table needed. ### 2. Password hashing: bcryptjs **Choice**: `bcryptjs` (pure JS bcrypt implementation) **Why**: No native dependencies (works in Docker without build tools), well-tested, sufficient for this use case. **Alternatives considered**: - **bcrypt** (native): Requires build tools in Docker, fails on some platforms. - **argon2**: Better algorithm but requires native bindings. Overhead not justified here. ### 3. Database schema: `users` table + `user_id` FK on all tables **Choice**: Add `users` table. Add `user_id` column (nullable initially for migration, then NOT NULL) to: `charts`, `annotations`, `annotation_types`, `span_annotations`, `span_label_types`. **Users table**: ``` users ├── id: uuid (PK, default gen_random_uuid()) ├── name: text (nullable) ├── email: text (unique, not null) ├── email_verified: timestamp (nullable) ├── password_hash: text (nullable — null for OAuth-only users) ├── image: text (nullable) ├── provider: text (default 'credentials') — 'credentials' | 'google' ├── provider_account_id: text (nullable) — Google sub ID ├── created_at: timestamp ├── updated_at: timestamp ``` **Why UUID for user IDs**: Avoids enumeration attacks, safe to expose in URLs/tokens, standard for auth systems. **Why NOT use Drizzle adapter's default schema**: The `@auth/drizzle-adapter` creates `users`, `accounts`, `sessions`, `verification_tokens` tables. We'll use a simplified custom schema since we're using JWT (no sessions table) and deferring email verification. We'll handle the adapter mapping manually. **Alternatives considered**: - **Drizzle adapter default schema**: Creates 4 tables, adds complexity for features we don't need yet (verification tokens, separate accounts table). We can adopt it later if we add email verification or more OAuth providers. - **Serial integer IDs**: Enumerable, less secure for user-facing IDs. ### 4. Routing structure **Choice**: ``` / → Landing page (public) /login → Login page (public) /register → Register page (public) /app → Main workspace (protected, current page.tsx moves here) /app/settings → User settings (protected) /api/auth/* → NextAuth API routes /api/* → Existing API routes (protected) ``` **Why**: Clean separation of public marketing pages and protected app. `/app` prefix makes middleware rules simple. Existing API routes stay at `/api/*` — just add auth check. **Alternatives considered**: - **Keep app at `/` and use `/landing`**: Unconventional, confuses SEO. - **Subdomain (`app.candleannotator.com`)**: Adds deployment complexity. ### 5. Auth middleware approach **Choice**: Next.js `proxy.ts` at project root. Uses Auth.js `auth()` to check session. Redirects: - Unauthenticated users hitting `/app/*` or `/api/*` (except `/api/auth/*` and `/api/health`) → redirect to `/login` - Authenticated users hitting `/login` or `/register` → redirect to `/app` **Why**: Single enforcement point, runs on edge before page/API renders. Clean and standard Next.js pattern. ### 6. API route auth pattern **Choice**: Helper function `getAuthUser(request)` that extracts user from JWT session. Every API route calls this at the top: ```typescript const user = await getAuthUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); // All queries filter by user.id ``` All Drizzle queries add `.where(eq(table.user_id, user.id))` for reads, and set `user_id: user.id` on inserts. **Why**: Simple, explicit, no magic. Every route clearly shows it's user-scoped. ### 7. ML Service user context **Choice**: Pass `X-User-ID` header from Next.js API routes to FastAPI ML service. The ML service trusts this header (internal network only, not exposed to clients). **Why**: ML service doesn't need its own auth — it's internal. The Next.js layer validates auth and forwards user context. MLflow experiment names include user ID for isolation. **Alternatives considered**: - **JWT forwarding**: Requires Python JWT validation, adds complexity. - **Separate user DB in Python**: Duplicates user management. ### 8. User data seeding on registration **Choice**: On successful registration, seed default `annotation_types` and `span_label_types` for the new user (copy from a hardcoded default set). **Why**: New users need at least the standard annotation types (break_up, break_down, line) and span label types to start working. Without seeding, the app would be empty/broken. ### 9. Frontend auth state **Choice**: Use `next-auth/react` `SessionProvider` wrapping the app layout. Components use `useSession()` hook. Server components use `auth()` from `@/auth`. **Why**: Standard Auth.js pattern. SessionProvider handles token refresh automatically. ### 10. Lovable design integration **Choice**: Convert Lovable HTML mockups to React/Next.js components using existing Tailwind + shadcn/ui patterns. Extract the structure and classes, adapt to Next.js ``, form actions, etc. Keep the visual design but rewrite as proper React. **Why**: Lovable generates static HTML with Tailwind. Direct reuse of CSS classes is possible, but the HTML structure needs to become React components with proper event handling. ### 11. Settings page features **Choice**: `/app/settings` page with sections: - **Profile**: Display name, email (read-only for OAuth users) - **Security**: Change password (credentials users only) - **Danger zone**: Delete account (with confirmation dialog) **Why**: Covers essential user management needs. Change password is table stakes. Delete account is GDPR-friendly. Keep it minimal — add more settings later as needed. ## Risks / Trade-offs **[Data migration for existing deployments]** → Create a migration script that assigns all existing data to a default "admin" user. Document in DEPLOYMENT.md. The migration adds `user_id` as nullable first, runs the script, then makes it NOT NULL. **[OAuth users can't set password]** → If a user registers via Google, they have no password. The settings page should show "Signed in via Google" instead of password change form. If they want password login later, they'd need a "set password" flow (defer to future). **[JWT secret rotation]** → If `AUTH_SECRET` is changed, all existing sessions invalidate. Document this. Not a risk per se, but users will be logged out. **[ML service trusts X-User-ID header]** → The ML service is only accessible internally (bound to 127.0.0.1 in docker-compose). If it were exposed publicly, this would be a security hole. Document that ML service must never be publicly accessible. **[No email verification initially]** → Users can register with any email. This means no password reset via email. Acceptable for initial release — add email verification later. **[Unique constraint on annotation_types.name becomes per-user]** → Currently `annotation_types.name` has a unique constraint. With multi-user, the same name can exist for different users. Change to unique constraint on `(user_id, name)`. Same for `span_label_types.name`. ## Migration Plan ### Database migration steps: 1. Add `users` table 2. Add `user_id` column (nullable) to: `charts`, `annotation_types`, `span_label_types`, `annotations`, `span_annotations` 3. Create a default admin user 4. UPDATE all existing rows to set `user_id = admin_user_id` 5. ALTER columns to NOT NULL 6. Add foreign key constraints 7. Drop old unique constraints, add new composite unique constraints (e.g., `(user_id, name)` for annotation_types) 8. Add indexes on `user_id` columns for query performance ### Deployment steps: 1. Update `.env` with new variables: `AUTH_SECRET`, `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` 2. Run database migrations 3. Run data migration script (assign existing data to admin user) 4. Deploy new app version 5. First login as admin user to verify data ### Rollback strategy: - Keep `user_id` nullable during testing phase - If rollback needed: revert code, `user_id` columns remain but are ignored - No data loss on rollback ## Open Questions 1. **Default admin credentials**: What email/password should the default admin user have for migrating existing data? Suggest: configurable via env var `DEFAULT_ADMIN_EMAIL` / `DEFAULT_ADMIN_PASSWORD`. 2. **Google OAuth project**: Do we have a Google Cloud project with OAuth credentials ready, or should we document setup steps? 3. **ML model isolation**: Should trained models be fully isolated per user (separate MLflow experiments), or is a shared model registry acceptable initially?