Architecture
Patrimo est une application full-Next.js 16 (App Router) monolithique, multi-utilisateurs, exécutée en Docker sur un serveur on-premise via Coolify. Pas de backend séparé, pas de message broker, pas de cache distribué.
Stack
| Couche | Technologie | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.2.4 |
| Runtime | React | 19.2.4 |
| Langage | TypeScript | ^5 |
| Base de données | PostgreSQL | 16 (Alpine) |
| ORM | Prisma | ^7.7.0 |
| Driver DB | @prisma/adapter-pg + pg | ^7.7.0 / ^8.20 |
| Auth | NextAuth v5 beta | ^5.0.0-beta.31 |
| CSS | Tailwind CSS | ^4 |
| Charts | Recharts | ^3.8.1 |
| UI primitives | Radix UI | multiple |
| Dev container | Docker Compose | — |
| Prod | Coolify (Docker Compose override) | — |
| Tests | Jest + Testing Library | ^30 / ^16 |
Couches applicatives
┌─────────────────────────────────────────────────────────┐
│ Browser (React 19) │
│ - Server Components par défaut │
│ - Client Components pour interactions (Sidebar, forms) │
└───────────┬─────────────────────────────────────────────┘
│ HTTP + Server Actions (fetch POST chiffré)
┌───────────▼─────────────────────────────────────────────┐
│ Next.js App Router │
│ - src/app/login (public) │
│ - src/app/(app)/* (authenticated route group) │
│ - src/app/api/* (route handlers) │
│ - src/app/actions/* ('use server' — API principale) │
└───────────┬────────────── ───────────────────────────────┘
│ Prisma Client (singleton, adapter pg)
┌───────────▼─────────────────────────────────────────────┐
│ PostgreSQL 16 (Docker volume `postgres_data`) │
│ - 12 modèles Prisma │
│ - Migrations versionnées │
└─────────────────────────────────────────────────────────┘
Flux de requête
Connexion via Authentik OIDC
Browser → GET /login
→ src/app/login/page.tsx (Client Component)
→ clic bouton "Se connecter avec Authentik"
→ redirect GET /api/auth/signin/authentik
→ redirect vers Authentik (IDP externe)
→ callback GET /api/auth/callback/authentik
→ src/lib/auth.ts : PrismaAdapter crée/lie OAuthAccount
→ JWT signé (AUTH_SECRET), token.id + token.isAdmin propagés
→ cookie HttpOnly
→ redirect /dashboard
Connexion via Credentials
Browser → GET /login
→ src/app/login/page.tsx (Client Component)
→ formulaire email + mot de passe
→ POST /api/auth/callback/credentials
→ src/lib/auth.ts : authorize()
→ prisma.user.findUnique(email)
→ guard : passwordHash obligatoire (users OIDC exclus)
→ bcrypt.compare(password, passwordHash)
→ JWT signé (AUTH_SECRET), token.id + token.isAdmin propagés
→ cookie HttpOnly
→ redirect /dashboard
Page authentifiée
Browser → GET /(app)/accounts
→ src/app/(app)/layout.tsx (Server Component)
→ await auth() → session JWT
→ si absente : redirect /login
→ rendu <Sidebar> + children
→ src/app/(app)/accounts/page.tsx (Server Component)
→ prisma.account.findMany(...)
→ HTML streamed
Mutation via Server Action
Browser (form submit)
→ Server Action 'use server' dans src/app/actions/accounts.ts
→ auth() guard (ownership)
→ validation Zod
→ prisma.account.create(...)
→ revalidatePath('/accounts')
→ Browser rerender (RSC payload)
Voir Server Actions pour la convention détaillée.
Authentification
Source : src/lib/auth.ts
NextAuth v5 beta, deux providers coexistants, stratégie de session JWT (maxAge 30 jours).
Providers
| Provider | Type | Rôle |
|---|---|---|
| Authentik | OIDC | Provider principal, délègue l'authentification à Authentik auto-hébergé |
| Credentials | Email + bcrypt | Fallback pour tous les utilisateurs ayant un passwordHash en base |
PrismaAdapter — workaround collision de noms
Source : src/lib/auth.ts:33-36
Le modèle financier Account occupe le nom que PrismaAdapter attend pour les comptes OAuth. Un proxy client redirige le delegate account vers prisma.oAuthAccount (@@map("auth_accounts")) :
const prismaForAdapter = {
...prisma,
account: prisma.oAuthAccount,
} as unknown as Parameters<typeof PrismaAdapter>[0];
Modèles NextAuth dans le schéma Prisma
| Modèle Prisma | Table SQL | Usage |
|---|---|---|
OAuthAccount | auth_accounts | Comptes OIDC liés à un User |
AuthSession | auth_sessions | Sessions (réservé — stratégie JWT, donc non utilisé activement) |
VerificationToken | verification_tokens | Tokens de vérification email |
JWT callbacks
Source : src/lib/auth.ts:83-101
À chaque génération de token (premier login ou refresh), token.id et token.isAdmin sont alimentés depuis la base. isAdmin est toujours lu en DB car le provider Authentik ne transmet pas cette information.
Pages personnalisées
signIn→/loginerror→/login
La vérification de session dans la zone (app)/ est effectuée côté serveur via await auth() dans le layout.
Variables d'environnement (voir ADR-006)
| Variable | Obligatoire | Description |
|---|---|---|
AUTH_SECRET | oui | Clé de signature JWT |
AUTH_URL | non | URL de base NextAuth (défaut http://localhost:4000) |
AUTHENTIK_CLIENT_ID | oui | Client ID de l'application Authentik |
AUTHENTIK_CLIENT_SECRET | oui | Secret client Authentik |
AUTHENTIK_ISSUER | oui | URL de l'issuer OIDC Authentik |
Docker
Développement (docker-compose.yml) :
db:postgres:16-alpine, healthcheckpg_isready, volumepostgres_data, port5432app: build stagedev, bind-mount source (hot-reload), port4000, exécuteprisma db pushavantnpm run dev
Production (Dockerfile stage runner + docker-compose.prod.yml) :
| Stage | Base | Rôle |
|---|---|---|
base | node:22-alpine | Variables communes, WORKDIR |
deps | base | npm ci |
dev | base + deps | Hot-reload, bind-mount |
builder | base + deps | prisma generate + next build (standalone) |
runner | node:22-alpine | Image finale minimale, user nextjs non-root |
Le mode standalone de Next.js (output: "standalone" dans next.config.ts) produit un server.js autonome. L'entrypoint (entrypoint.sh) exécute prisma db push puis node server.js.
L'override prod :
- Cible le stage
runner - Supprime les bind-mounts
- Healthcheck
GET /api/health(curl, 30 s interval, start_period 40 s) restart: always
Variables prod supplémentaires
| Variable | Obligatoire | Description |
|---|---|---|
POSTGRES_PASSWORD | oui | Mot de passe PostgreSQL |
POSTGRES_DB | non | Nom DB (défaut patrimo) |
POSTGRES_USER | non | Utilisateur DB (défaut patrimo) |
GEMINI_API_KEY | non | Clé API Gemini (catégorisation IA) |
Scripts npm (racine)
| Commande | Action |
|---|---|
npm run dev | Next.js sur le port 4000 |
npm run build | Build de production (standalone) |
npm run type-check | tsc --noEmit |
npm test | Jest |
npm run test:coverage | Jest avec coverage |
npm run db:migrate | prisma migrate dev |
npm run db:migrate:prod | prisma migrate deploy |
npm run db:seed | prisma db seed |
npm run db:studio | Prisma Studio |
npm run hash-password | Hash bcrypt d'un mot de passe |
npm run generate-secret | Génère un AUTH_SECRET aléatoire |
Décisions techniques
Voir les ADRs :
- ADR-001 — PostgreSQL vs MongoDB
- ADR-002 — CSV-first, Nordigen V2
- ADR-003 — Auth via variables d'environnement (remplacé)
- ADR-006 — Authentik OIDC multi-utilisateurs
Pour approfondir :