Tech Stack
NordJari is a Next.js 14 app deployed on Vercel, backed by Supabase (PostgreSQL) and the Anthropic Claude API.
Full stack overview
| Layer | Technology | Notes |
|---|---|---|
| Framework | Next.js 14 (App Router) | Server components by default |
| Language | TypeScript (strict mode) | any is forbidden |
| Styling | Tailwind CSS | Custom NordJari palette tokens |
| Database | Supabase (PostgreSQL) | Managed Postgres + Auth |
| Auth | Supabase Auth | Email magic link |
| ORM | Prisma | Schema in prisma/schema.prisma |
| AI Coach | Anthropic Claude API | claude-sonnet-4-20250514 |
| Deployment | Vercel | Cron jobs via Vercel Cron |
| Domain | nordjari.com | Wiki at wiki.nordjari.com |
Next.js App Router conventions
- All pages and layouts are server components unless they need interactivity
'use client'is added only at the lowest component that requires browser APIs or React hooks- API routes live in
app/api/and return{ data, error }consistently - DB queries are always in
lib/— never inlined in route handlers or components
Supabase + Prisma
NordJari uses both Supabase and Prisma together:
- Prisma for schema definition, migrations, and typed queries (
lib/prisma.ts) - Supabase Auth for authentication (magic link email flow)
- Supabase RLS (Row Level Security) on all tables — enforced at the database level
The DATABASE_URL (pooled) and DIRECT_URL (direct) are both configured so Prisma migrations work correctly with Supabase connection pooling.
Authentication pattern
// Always use getUser() — never getSession() for server-side auth
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// Resolve DB userId via email (Supabase UUID != Prisma UUID in some setups)
const dbUser = await prisma.user.findUnique({ where: { email: user.email! } })
const userId = dbUser?.id ?? user.id
Cron jobs
Two cron jobs run on a schedule:
| Job | Schedule | Route |
|---|---|---|
| Nightly score recalculation | 0 2 * * * | POST /api/cron/score |
| Monday weekly coach brief | 0 8 * * 1 | POST /api/cron/coach-brief |
Both routes verify a x-cron-secret header against CRON_SECRET env var.
Environment variables
DATABASE_URL= # Supabase pooled connection string
DIRECT_URL= # Supabase direct connection string (for migrations)
NEXT_PUBLIC_SUPABASE_URL= # Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Public anon key (safe to expose)
SUPABASE_SERVICE_ROLE_KEY= # Server-only — never NEXT_PUBLIC_
ANTHROPIC_API_KEY= # Server-only — never NEXT_PUBLIC_
CRON_SECRET= # Vercel cron authentication secret