rubis/docs/tech/architecture.md
ordinarthur 8d3bab6a89 feat: scaffold frontend monorepo + first /login screen
Monorepo Turborepo (pnpm workspaces) avec 3 packages :

- apps/web : SPA React 19 + Vite 8 + Tailwind v4 (CSS-first)
  • TanStack Router (file-based, auto code-splitting), Query, Form
  • Radix primitives bruts + CVA + clsx + tailwind-merge
  • MSW pour mocker l'API tant qu'Adonis n'est pas branché
  • Polices Bricolage Grotesque + Inter self-hostées via fontsource
  • Tokens marque (rubis, cream, ink) exposés via @theme
  • Primitives maison : Gem, Brand, Eyebrow, Button, Input, Field
  • Route /login full flow : TanStack Form + Zod + mutation Query

- apps/api : Adonis 7 (kit api, scaffold via create-adonisjs)
  • Auth access tokens (Bearer) — cf. ADR-017
  • Tuyau core déjà câblé pour la génération de types
  • Routes /api/v1/auth/{signup,login} + /api/v1/account/{profile,logout}
  • Minimal — uniquement le pont front ↔ back

- packages/shared : types TS + schemas Zod + constantes
  • Source unique de vérité partagée api ↔ web
  • Domaines : User, Org, Auth, Client, Invoice, Plan

Tooling racine : Turbo, ESLint v9 flat, Prettier, husky, lint-staged.

CLAUDE.md et docs/decisions.md mis à jour avec ADR-014 à ADR-018
(stack, monorepo, PG existant, Bearer tokens, MinIO existant)
et le pointeur vers docs/tech/architecture.md.

Logo Rubis déplacé de landing/assets/ vers /assets/ (source unique
réutilisée par la landing et l'app).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 10:10:48 +02:00

18 KiB

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.arthurbarre.fr       │
                     │  app.rubis-sur-l-ongle.fr   │
                     └─────────────┬──────────────┘
                                   │
                ┌──────────────────┼──────────────────┐
                │                  │                  │
       ┌────────▼────────┐  ┌──────▼──────┐  ┌────────▼────────┐
       │  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 :

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.

// 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
  // ...
}
// 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

# 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-sur-l-ongle.fr  → rubis-api-svc:3333
    app.rubis-sur-l-ongle.fr  → rubis-web-svc:80
    rubis-sur-l-ongle.fr      → 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 (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-sur-l-ongle.fr 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.