57 lines
3.1 KiB
Markdown
57 lines
3.1 KiB
Markdown
---
|
|
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 `<img>` — 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.
|