--- type: project-doc parent: "[[Tia]]" status: verified tags: [tia, architecture, engineering] --- # ๐Ÿ—๏ธ Tia โ€” Architecture > Verified from CLAUDE.md + memory files. ## Stack Next.js 16 (App Router, Turbopack) ยท PostgreSQL 16 + **pgvector** (pgvector/pgvector:pg18 on prod) ยท **Drizzle ORM** ยท **LiteLLM** gateway โ†’ **MiniMax** (minimax-2.7) ยท **Cloudflare R2** ยท **Resend** ยท Tailwind v4 ยท **Dokploy** (auto-migrate on deploy). ## Route groups ([[Decision Log#TD-005]]) - `(marketing)/` โ†’ `/`, `/pricing`, `/privacy`, `/terms` โ€” **static, no DB/auth imports** - `(app)/` โ†’ authenticated app with ThemeProvider + FamilyProvider + BottomNav; dashboard at `/home` ## Auth - User: `/login` โ†’ `POST /api/auth/signin` (bcrypt) โ†’ session row โ†’ httpOnly `tia_session`; routes use `requireFamily()` - Admin: `/admin-login` โ†’ `tia_admin_session`; admin layout is a server component calling `verifyAdminSession()` ## Data & context - `FamilyProvider` resolves family/child from session: `{ familyId, child, children, tier, memberCount, updateChildImage }` - Quota logic in `src/lib/quota.ts` (pure fns + queries); usage always derived via `SUM` ([[Decision Log#TD-001]]) ## Migrations (the big gotcha) ([[Decision Log#TD-003]]) - `migrate.mjs` runs before `server.js`; reads all SQL in `drizzle/`, applies any not in `__drizzle_migrations` - **`when` must be `Date.now()`** (> 1779539431897) or Drizzle silently skips it - Hot-apply via `POST /api/debug-migration` (header `x-run-migration: yes`), idempotent steps ## Media (R2) ([[Decision Log#TD-004]]) - 3-step upload: init โ†’ server-proxied `PUT /api/upload` โ†’ save URL - **Never** direct cross-origin PUT; **never** raw `*.r2.dev` in `` โ€” use `/api/img?key=` proxy ## Observability (admin) `error_events` table + `logError()` + `POST /api/errors`; `/admin/{health,errors,audit,ai}` over `audit_log`, `ai_usage` (p95 via `percentile_cont`). ## Related [[Tia]] ยท [[Tia - Decisions]] ยท [[Self-Hosting]] ยท [[Engineering Overview]] --- ## Auth evolution (important nuance) Two phases appear across the history: 1. **DB sessions** โ€” bcrypt + `sessions` table + httpOnly `tia_session` cookie; `requireFamily()` 2. **NextAuth v5** โ€” Google OAuth (public) + magic link (invite-only); used by the marketing-era build Hardening sprint **H1** explicitly "replaced broken password hash." Treat auth as **migrating/hardened** โ€” confirm the current source of truth. ## DB hardening (sprint H2) - Non-superuser role **`tia_app`** (LOGIN, only SELECT/INSERT/UPDATE/DELETE โ€” no DROP/CREATE/ALTER ROLE) - Separate `DATABASE_URL_SUPERUSER` for migrations only - Per-request **session context** `app.current_family_id` set in a transaction (`src/db/scoped.ts`) โ†’ foundation for **Row-Level Security (RLS)** ## Memories vision pipeline (sprint G2) Photo is mandatory; a **vision model does the metadata heavy-lifting**: photo โ†’ vision caption/tags โ†’ `memories.vision_embedding vector(1536)` (pgvector, ivfflat index `memories_embedding_idx`). Requires the `pgvector/pgvector:pg18` image ([[Deployment Checklist]]). ## Hosting **Hetzner** server via **Dokploy**; `output: 'standalone'`; PWA shell installable.