# 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 ``` **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 ▼ 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: - git.arthurbarre.fr/ordinarthur/rubis-web: ↓ kubectl rollout (api + web) ↓ healthchecks readinessProbe → service public ``` --- ## 8. Conventions de code | Domaine | Convention | |---|---| | Branches | `feat/`, `fix/`, `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/.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`.*