rubis/docs/tech/architecture.md
ordinarthur 1c5a58e09a
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 26s
Build & Deploy Landing / build-and-deploy (push) Successful in 27s
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
chore(domain): migrate rubis.arthurbarre.fr → rubis.pro
Bascule du domaine principal vers rubis.pro / app.rubis.pro :

- K3s ConfigMaps (api.yml, web.yml) : APP_URL, WEB_URL,
  COOKIE_DOMAIN, OAUTH callbacks pointent vers app.rubis.pro
- Dockerfile.web : ARG VITE_API_URL et VITE_PUBLIC_LANDING_URL
- Workflows Gitea : commentaires + build args web → rubis.pro
- Code API (mail_dispatcher, send_test_email, config/mail) :
  defaults env LANDING_URL et MAIL_FROM_ADDRESS migrés
- Templates env (.env.example) idem
- Docs (architecture, backend, frontend, brand-identity) idem
- AGENTS.md / CLAUDE.md / deploy-memory : pointeurs domaine MAJ

Note : MAIL_FROM_ADDRESS dans le secret K3s reste sur
rubis@arthurbarre.fr tant que le domaine rubis.pro n'est pas
Verified dans Resend. À switcher manuellement après vérif Resend.

Compat : un 301 Traefik redirige rubis.arthurbarre.fr → rubis.pro
(et app.X aussi) — config Ansible dans le repo proxmox.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 21:32:31 +02:00

457 lines
18 KiB
Markdown

# Architecture technique — Rubis Sur l'Ongle
> Version : 0.1 · Dernière maj : 2026-05-05
> Décisions de référence : ADR-014 (stack), ADR-015 (repo), ADR-016 (PG), ADR-017 (auth), ADR-018 (storage). Voir `/docs/decisions.md`.
Ce document est la source de vérité technique. Quand le code et ce fichier divergent, on tranche en discussion et on met à jour ici.
---
## 1. Vue d'ensemble
```
┌────────────────────────────┐
│ Internet (HTTPS) │
└─────────────┬──────────────┘
┌─────────────▼──────────────┐
│ Traefik (Proxmox gateway) │
│ rubis.pro │
│ app.rubis.pro │
└─────────────┬──────────────┘
┌──────────────────┼──────────────────┐
│ │ │
┌────────▼────────┐ ┌──────▼──────┐ ┌────────▼────────┐
│ Pod: web-static │ │ Pod: api │ │ Pod: landing │
│ nginx + Vite │ │ AdonisJS │ │ (déjà déployé) │
│ build │ │ Node │ │ │
└──────────────────┘ └──────┬─────┘ └─────────────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
┌────────▼─────────┐ ┌────────▼────────┐ ┌─────────▼────────┐
│ LXC: PostgreSQL │ │ LXC: MinIO │ │ Provider OCR │
│ (existant, pool │ │ (existant, │ │ (à benchmarker) │
│ Proxmox) │ │ S3-compatible) │ │ │
└──────────────────┘ └─────────────────┘ └──────────────────┘
┌────────▼─────────┐
│ Provider Email │
│ (à benchmarker) │
└──────────────────┘
```
**Composants** :
| Composant | Rôle | Hosting | Status |
|---|---|---|---|
| `apps/web` | SPA React Vite — interface utilisateur | nginx pod K3s (build statique) | À écrire |
| `apps/api` | API REST AdonisJS — logique métier, jobs, email | Pod Node K3s | À écrire |
| `packages/shared` | Types TS, schemas Zod, constantes communes | npm workspace local | À écrire |
| `landing` | Landing publique waitlist | nginx pod K3s | ✅ Déployé |
| PostgreSQL | Base de données métier | LXC Proxmox existant | ✅ En place |
| MinIO | Stockage PDF + pièces jointes (S3-compat) | LXC Proxmox existant | ✅ En place |
| Provider OCR | Extraction texte des factures | Externe (HTTPS) | ADR-020 à venir |
| Provider Email | Envoi outbound (relances + check-in) | Externe (HTTPS) | ADR-021 à venir |
---
## 2. Repo layout (monorepo)
```
rubis/
├── apps/
│ ├── api/ # AdonisJS v7 backend
│ │ ├── app/ # Controllers, models, services
│ │ ├── config/ # Auth, database, mail, queue
│ │ ├── database/
│ │ │ ├── migrations/
│ │ │ └── seeders/
│ │ ├── start/ # Routes, kernel
│ │ ├── tests/
│ │ ├── ace.js # CLI Adonis
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── web/ # React + Vite SPA
│ ├── src/
│ │ ├── routes/ # TanStack Router (file-based)
│ │ ├── components/
│ │ ├── lib/ # api client, query keys, utils
│ │ └── main.tsx
│ ├── public/
│ ├── index.html
│ ├── vite.config.ts
│ ├── package.json
│ └── tsconfig.json
├── packages/
│ └── shared/ # Code partagé api ↔ web
│ ├── src/
│ │ ├── types/ # Types TS (DTOs API)
│ │ ├── schemas/ # Schemas Zod (validation)
│ │ └── constants/ # Énums, règles métier
│ ├── package.json
│ └── tsconfig.json
├── landing/ # Landing publique (déjà déployée)
├── docs/ # Documentation
│ ├── produit.md
│ ├── marque.md
│ ├── decisions.md
│ └── tech/ # Doc technique
│ └── architecture.md # (ce fichier)
├── k3s/ # Manifests Kubernetes
├── Dockerfile.api # Build image api
├── Dockerfile.web # Build image web (nginx + bundle)
├── pnpm-workspace.yaml
├── package.json # Scripts root, devDependencies communes
├── tsconfig.base.json # Config TS partagée
└── CLAUDE.md
```
**Outils monorepo** :
- **pnpm workspaces** — léger, rapide, gestion native des liens symboliques entre packages
- **TypeScript project references** — résout les imports cross-package sans build préalable
- **Turborepo** *(optionnel, à voir au volume)* — cache + parallélisation des scripts
**Commandes racine** typiques :
```bash
pnpm install # installe tout
pnpm -F api dev # dev API
pnpm -F web dev # dev SPA
pnpm -F api migration:run # migrations DB
pnpm -F api test # tests API
pnpm build # build api + web pour prod
```
---
## 3. apps/api — AdonisJS v7
### Stack interne
- **AdonisJS v7** + **Lucid ORM** (PG)
- **`@adonisjs/auth`** — access tokens Bearer (stateless)
- **`@adonisjs/bouncer`** — autorisations par policy (admin/lecture/édition pour V2 multi-users)
- **`@adonisjs/mail`** — emails outbound (provider à choisir ADR-021)
- **`@adonisjs/queue`** ou **BullMQ** — jobs différés (relances programmées, OCR, check-ins)
- **`@adonisjs/limiter`** — rate limiting sur les routes publiques (login, signup)
- **Vine** (validateur natif Adonis 7) ou **Zod** côté API pour validation des payloads
### Conventions de routes
Toutes les routes API sous `/api/v1/`. Versioning explicite — V2 vivra côté `/api/v2/` sans casser V1.
```
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/logout
POST /api/v1/auth/refresh
GET /api/v1/me
PATCH /api/v1/me
GET /api/v1/organizations/:id
GET /api/v1/invoices
POST /api/v1/invoices # create manual
POST /api/v1/invoices/upload # OCR pipeline
GET /api/v1/invoices/:id
PATCH /api/v1/invoices/:id
DELETE /api/v1/invoices/:id
POST /api/v1/invoices/:id/relance # relance manuelle
POST /api/v1/invoices/:id/mark-paid
GET /api/v1/plans
POST /api/v1/plans
PATCH /api/v1/plans/:id
DELETE /api/v1/plans/:id
GET /api/v1/clients
POST /api/v1/clients
PATCH /api/v1/clients/:id
GET /api/v1/dashboard/kpis
GET /api/v1/dashboard/activity
```
### Conventions de réponse
- JSON systématique
- Format succès : `{ data: ..., meta?: { ... } }`
- Format erreur : `{ errors: [{ code, message, field? }] }`
- Codes HTTP standards (200, 201, 204, 400, 401, 403, 404, 422, 500)
- Pagination cursor-based pour les listes (préférable à offset pour les flux modifiés en temps réel)
---
## 4. apps/web — React + Vite
### Stack interne
- **React 19** + **Vite 6**
- **TanStack Router** — routing file-based (à privilégier), search params type-safe pour les filtres facture
- **TanStack Query** — cache + invalidation + optimistic updates pour le state serveur
- **TailwindCSS** *(à confirmer)* — utility-first, cohérent avec les couleurs de marque
- **Lucide React** pour les icônes
- **Bricolage Grotesque + Inter** via Google Fonts (cohérent landing)
### Auth côté SPA (cf. ADR-017)
- **Access token** stocké en mémoire (variable de module / state Query) — pas localStorage pour éviter XSS
- **Refresh token** en cookie httpOnly + SameSite=Strict
- Au boot du SPA : appel `/auth/refresh` pour obtenir un nouvel access token (silent reauth)
- Si refresh échoue → redirect `/login`
### Organisation des routes
File-based via TanStack Router :
```
src/routes/
├── __root.tsx # Layout global + AuthGate
├── login.tsx
├── signup.tsx
├── _app/ # Routes protégées
│ ├── _app.tsx # Layout app (sidebar, header, brand)
│ ├── index.tsx # Dashboard
│ ├── factures.tsx # Liste factures
│ ├── factures.$id.tsx # Détail facture
│ ├── plans.tsx # Bibliothèque plans
│ ├── plans.$id.tsx # Éditeur plan
│ ├── clients.tsx
│ └── parametres.tsx
└── onboarding/
├── compte.tsx
├── entreprise.tsx
└── signature.tsx
```
---
## 5. packages/shared — types et schémas partagés
Le but : un client API typé fortement, sans duplication de définitions.
```ts
// packages/shared/src/types/invoice.ts
export type InvoiceStatus = 'pending' | 'awaiting_user_confirmation' | 'paid' | 'in_relance' | 'litigation' | 'cancelled'
export type Invoice = {
id: string
numero: string
clientId: string
amountTtc: number
dueDate: string // ISO
status: InvoiceStatus
planId: string | null
// ...
}
```
```ts
// packages/shared/src/schemas/invoice.ts
import { z } from 'zod'
export const createInvoiceSchema = z.object({
numero: z.string().min(1),
clientId: z.string().uuid(),
amountTtc: z.number().positive(),
dueDate: z.string().datetime(),
// ...
})
export type CreateInvoiceInput = z.infer<typeof createInvoiceSchema>
```
**Avantage** : Adonis valide avec ce schéma, le SPA valide avec le même, le type est inféré une seule fois.
---
## 6. Flux de données critiques
### 6.1 Upload + OCR + création facture
```
SPA (drag & drop PDF)
│ POST /api/v1/invoices/upload (multipart)
api: stocke le PDF dans MinIO (bucket rubis-invoices)
│ retourne { uploadId, status: 'processing' }
api: enqueue job ProcessOcr(uploadId)
worker: récupère le PDF depuis MinIO
│ appel HTTP vers OCR provider
worker: parse les champs extraits, crée l'Invoice en DB (status: pending)
SPA: poll ou WebSocket → reçoit l'Invoice prête à valider
```
**Points d'attention** : le job OCR doit être idempotent (même uploadId rejoué = pas de duplicate). Le SPA peut afficher un spinner pendant les 3-10 secondes d'OCR.
### 6.2 Programmation des relances
```
SPA: utilisateur clique "Valider" sur l'Invoice
│ PATCH /api/v1/invoices/:id (status: scheduled, planId: …)
api: créé N RelanceTasks (une par étape du plan)
chaque RelanceTask a un sendAt (calculé d'après dueDate + offset étape)
queue: tâches en attente
│ ┌─ avant chaque relance, créer aussi un CheckinTask (T-2j) ─┐
▼ ▼ ▼
worker @ sendAt: vérifie l'état de l'Invoice (toujours pending ?)
│ si invoice.status === 'pending' → envoie l'email
│ sinon → no-op (l'invoice a été marquée payée entre-temps)
```
### 6.3 Check-in email à l'utilisateur
```
worker @ checkinTask.sendAt:
│ génère un token signé (avec invoice.id + reply_action: 'paid' | 'not_paid')
api: envoie un email à l'utilisateur (pas au client) avec 2 boutons
│ chaque bouton = lien GET /api/v1/checkin/:token (action embeddée)
utilisateur clique "Oui, j'ai été payé"
api: GET /checkin/:token → vérifie token, marque invoice.status = 'paid'
→ annule les RelanceTasks futures de cette invoice
→ redirect SPA avec confirmation
```
**Sécurité** : le token doit être signé (HMAC ou JWT court) et avoir une durée limitée (24h après émission). Pas d'auth Bearer requise pour ce endpoint car c'est un click depuis email.
### 6.4 Authentification Bearer
```
SPA: POST /api/v1/auth/login { email, password }
│ api valide credentials, crée AccessToken (TTL 30 min) + RefreshToken (TTL 30j httpOnly cookie)
SPA reçoit { accessToken, user } — accessToken stocké en mémoire
│ chaque requête API : Authorization: Bearer <accessToken>
30 min plus tard : 401 sur appel API
SPA: POST /api/v1/auth/refresh (cookie httpOnly envoyé auto)
│ api valide refresh, émet nouvel accessToken
SPA retry l'appel original avec nouveau token
```
---
## 7. Topologie de déploiement
### Réseau Proxmox
| Resource | Type | Rôle |
|---|---|---|
| Cluster K3s | Pool VMs Proxmox | Orchestration des pods app |
| LXC `postgres` | LXC dédié | PostgreSQL — accessible aux pods K3s via réseau interne |
| LXC `minio` | LXC dédié | MinIO — accessible aux pods K3s via réseau interne |
| Traefik | Reverse proxy | TLS termination + routing par hostname |
### Pods K3s
```yaml
# Namespace: rubis
- Deployment: rubis-api # AdonisJS Node, port 3333
- Deployment: rubis-web # nginx, sert le bundle Vite, port 80
- Deployment: rubis-landing # déjà existant
- Service: rubis-api-svc # ClusterIP
- Service: rubis-web-svc # ClusterIP
- Service: postgres-external # ExternalName → IP du LXC postgres
- Service: minio-external # ExternalName → IP du LXC minio
- Secret: rubis-config # DB credentials, MinIO credentials, OCR API key, mail API key
- IngressRoute (Traefik) :
api.rubis.pro → rubis-api-svc:3333
app.rubis.pro → rubis-web-svc:80
rubis.pro → rubis-landing-svc:80
```
### Pipeline CI Gitea
```
git push gitea main
.gitea/workflows/build.yml
build & push images :
- git.arthurbarre.fr/ordinarthur/rubis-api:<sha>
- git.arthurbarre.fr/ordinarthur/rubis-web:<sha>
kubectl rollout (api + web)
healthchecks readinessProbe → service public
```
---
## 8. Conventions de code
| Domaine | Convention |
|---|---|
| Branches | `feat/<short-desc>`, `fix/<short-desc>`, `chore/<…>` |
| Commits | [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`) |
| TypeScript | `strict: true`, pas de `any` (sauf justifié + commenté), `noUncheckedIndexedAccess: true` |
| Linting | ESLint + Prettier, config partagée via `tsconfig.base.json` + `.eslintrc.cjs` racine |
| Tests | Japa (Adonis) pour API, Vitest pour shared/, Playwright pour E2E utilisateur (V2) |
| Migrations | Versionnées, jamais éditées rétroactivement (cf. principe ADR du log de décisions) |
| Secrets | Jamais en clair dans le repo. `.env.local` git-ignoré, secrets K3s pour la prod |
---
## 9. Sécurité & RGPD
- **Hébergement français** (Proxmox France) — conforme RGPD pour la cible TPE-PME
- **Chiffrement at-rest** : disque Proxmox chiffré (LUKS) — à confirmer côté infra
- **Chiffrement in-transit** : TLS partout (Traefik), connexions PG et MinIO en SSL interne
- **Rate limiting** : `@adonisjs/limiter` sur `/auth/*` (5 req/min par IP), routes OCR (10/h par utilisateur)
- **CORS** : whitelist stricte (`app.rubis.pro` uniquement) — refus des origines tierces
- **CSRF** : non-applicable car auth via Bearer header (pas cookie session). Les endpoints email check-in utilisent des tokens signés à TTL court.
- **Backups** :
- PG : dump quotidien dans MinIO (`rubis-backups/pg/<date>.dump`)
- MinIO : snapshot Proxmox du LXC quotidien
- Retention : 30 jours mini (à confirmer)
- **Suppression** : RGPD Article 17 — endpoint `DELETE /api/v1/me` qui purge data utilisateur + factures + pièces jointes
---
## 10. Décisions encore en attente
À trancher avant fin V1, par ordre de priorité :
| # | Sujet | Échéance suggérée |
|---|---|---|
| 019 | **Domain model** (entités, relations, index) | Avant la 1ère migration |
| 020 | **Provider OCR** (Mindee, Document AI, Textract, Tesseract) | Avant l'implémentation du job ProcessOcr |
| 021 | **Provider email** (Resend, Postmark, SendGrid, AWS SES) | Avant l'implémentation des relances |
| 022 | **Pricing exact** (Free 5 factures ? Pro 19 €/mois ?) | Avant le payment flow |
| 023 | **Endpoint waitlist** (Resend / Formspree / API Adonis) | Au push de la landing en prod |
---
## 11. Évolutions V2+ anticipées
- **Multi-utilisateurs** : tables `organizations` et `memberships` à prévoir dès la V1 (même si UI mono-user)
- **SMS** : provider Twilio/OVH abstrait derrière un service `MessageDispatcher` qui route email/sms selon plan + cadence
- **Intégration banking** : webhook entrant sur `/api/v1/banking/payment-confirmed` qui marque les invoices payées automatiquement (le check-in email V1 devient fallback)
- **Intégrations comptables** (Pennylane/Sage) : modèle d'événement abstrait `invoice.created` exportable en webhook sortant
- **API publique** : sous `/api/v1/public/*` avec abilities/scopes par token (lecture seule, écriture limitée)
---
*Maintenu par Arthur + Claude. Ce document est versionné — les changements significatifs passent par un ADR dans `/docs/decisions.md`.*