rubis/docs/tech/architecture.md
ordinarthur e5530930b3
Some checks failed
Build & Deploy API / build-and-deploy (push) Failing after 17s
Build & Deploy Web / build-and-deploy (push) Successful in 1m15s
Build & Deploy Landing / build-and-deploy (push) Failing after 3m43s
feat: refactor frontend en stack React unifiée (Astro + packages/ui)
Trois surfaces partagent désormais le même design system, Tailwind v4
et React 19 — au lieu d'avoir landing en HTML vanilla, app en React, et
blog en Adonis SSR :

* packages/ui — design system partagé (tokens Tailwind v4 + composants
  TSX) extrait depuis apps/web : Brand, Gem, Button, Card, Chip, Eyebrow,
  EmptyState. apps/web migre 41 imports vers @rubis/ui.

* apps/landing — nouvelle app Astro 6 SSR (rubis.pro), remplace l'ancienne
  landing nginx vanilla. Embarque :
  - Landing complète portée en sections React (Hero, Stats, Promise,
    HowItWorks, Gamification, Legal, Pricing, FAQ, FinalCTA, Footnotes)
  - Pages légales (mentions, confidentialité, CGV) via LegalLayout.astro
  - Blog SSR (/blog, /blog/:slug) qui consomme /api/v1/posts
  - sitemap.xml, blog/rss.xml, robots.txt en endpoints Astro
  - SEO complet (canonical, hreflang, OG, Twitter Card, JSON-LD
    Article/BreadcrumbList/Blog/SoftwareApplication)

* apps/api — BlogController réduit à 2 endpoints JSON (GET /api/v1/posts
  + GET /api/v1/posts/:slug). Suppression des templates SSR Adonis
  (apps/api/app/blog/), de l'alias #blog/*, des deps react-dom et
  @types/react-dom. PostTransformer + PostSummaryTransformer ajoutés.
  Le service blog_renderer + le seeder + les 3 articles fondateurs
  restent intacts (réutilisés par futurs admin + cron IA).

* Infra :
  - Dockerfile.landing (multi-stage Node 22 + tini, Astro standalone)
  - k3s/app/landing.yml (Deployment + Service rubis-landing:4321 +
    ConfigMap avec API_URL=http://rubis-api.rubis.svc.cluster.local:3333)
  - .gitea/workflows/deploy.yml mis à jour pour build rubis-landing
  - .gitea/workflows/deploy-web.yml + Dockerfile.web : prennent en
    compte packages/ui/ comme dépendance
  - Suppression du Dockerfile nginx legacy + k3s/{deployment,service}.yml
  - Suppression de landing/ (assets favicons migrés vers
    apps/landing/public/)

* Docs : architecture.md (vue d'ensemble + §4bis apps/landing complet,
  §3 endpoints JSON blog, layout monorepo), CLAUDE.md (stack technique,
  documents associés, déploiement).

Note infra : l'ancien Deployment "rubis" (nginx) et son Service ne sont
PAS supprimés par la CI — à nettoyer manuellement après validation que
Traefik a été repointé sur rubis-landing:4321 dans le repo proxmox.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:09:13 +02:00

578 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) │
│ Host(rubis.pro) │
│ Host(app.rubis.pro) │
└─────────────┬──────────────┘
┌──────────────────────────┼─────────────────────────┐
│ │ │
┌───────▼─────────┐ ┌────────▼─────────┐ ┌──────────▼────────┐
│ Pod: rubis- │ │ Pod: rubis-web │ │ Pod: rubis-api │
│ landing (Astro) │ │ (nginx + Vite) │ │ (AdonisJS Node) │
│ rubis.pro/* │ │ app.rubis.pro/* │ │ /api/v1/* + │
│ (SSG + SSR) │ │ (proxy /api → api)│ │ jobs BullMQ │
└───────┬─────────┘ └────────┬─────────┘ └──────────┬────────┘
│ SSR fetch JSON │ │
└─────────────►──────────────────────►────────────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
┌────────▼─────────┐ ┌────────▼────────┐ ┌─────────▼────────┐
│ LXC: PostgreSQL │ │ LXC: MinIO │ │ Provider OCR │
│ (existant) │ │ (S3-compat) │ │ (à benchmarker) │
└──────────────────┘ └─────────────────┘ └──────────────────┘
```
**Composants** :
| Composant | Rôle | Hosting | Status |
|---|---|---|---|
| `apps/landing` | Landing + blog publics (Astro 6 SSR) | Pod Node K3s (`rubis-landing:4321`) | ✅ Déployé |
| `apps/web` | SPA React (SaaS) | nginx pod K3s (build statique) | À écrire |
| `apps/api` | API REST AdonisJS — logique métier, jobs, email | Pod Node K3s (`rubis-api:3333`) | À écrire |
| `packages/shared` | Types TS, schemas Zod, constantes communes | workspace local | À écrire |
| `packages/ui` | Design system partagé (tokens Tailwind + composants TSX) | workspace local | ✅ |
| 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/ # SPA React Vite (app.rubis.pro)
│ │ ├── src/
│ │ │ ├── routes/ # TanStack Router (file-based)
│ │ │ ├── components/ # composants app-spécifiques
│ │ │ ├── lib/ # api client, query keys, utils
│ │ │ └── main.tsx
│ │ ├── public/
│ │ ├── vite.config.ts
│ │ └── package.json
│ └── landing/ # Astro SSR (rubis.pro + /blog)
│ ├── src/
│ │ ├── pages/ # routes file-based (.astro + .ts endpoints)
│ │ ├── layouts/ # Layout.astro, LegalLayout.astro
│ │ ├── components/ # SiteHeader/Footer + sections + blog
│ │ ├── lib/api.ts # client API typé
│ │ └── styles/app.css
│ ├── public/ # favicons, manifest
│ ├── astro.config.mjs
│ └── package.json
├── packages/
│ ├── shared/ # Types TS + schemas Zod (api ↔ web)
│ │ └── src/{types,schemas,constants}
│ └── ui/ # Design system partagé (apps/web + apps/landing)
│ └── src/{components,lib,styles}
├── docs/
│ └── tech/architecture.md # (ce fichier)
├── k3s/
│ ├── namespace.yml
│ └── app/{api,web,landing,redis}.yml
├── Dockerfile.api # AdonisJS Node SSR
├── Dockerfile.web # nginx + bundle Vite
├── Dockerfile.landing # Astro Node SSR (standalone)
├── pnpm-workspace.yaml
└── 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)
### Endpoints blog publics
L'API expose les articles du blog en **JSON** (consommés par `apps/landing` en SSR). L'API ne sert plus de HTML — le rendu HTML déménagé côté Astro.
| Route | Description |
|---|---|
| `GET /api/v1/posts` | Liste des articles publiés (summary, sans contentHtml) |
| `GET /api/v1/posts/:slug` | Article complet (contentHtml inclus) + 3 articles liés |
Cf. `apps/api/app/controllers/blog_controller.ts` + `apps/api/app/transformers/post_transformer.ts`.
**Pipeline de contenu** :
1. **Manuel V1** : `node ace seed:blog` insère/upserte les 3 articles fondateurs.
2. **Admin (PR3)** : page React `/admin/blog` sur `app.rubis.pro` (auth-gated) — édition markdown + preview + bouton publier. Le markdown est rendu en HTML via `app/services/blog_renderer.ts` (marked v15, GFM, IDs auto) au save, cache dans `posts.content_html`.
3. **Cron IA hebdo (PR4)** : `apps/api/app/jobs/weekly_blog_post.ts` génère un draft Sonnet 4.6 chaque lundi 06:00 Europe/Paris ; review humaine obligatoire avant publish.
---
## 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
```
---
## 4bis. apps/landing — Astro 6 (rubis.pro)
Landing publique + blog, en Astro avec rendu hybride statique/SSR. Image Docker `rubis-landing` (Node 22 standalone), service ClusterIP `rubis-landing:4321`.
### Stack interne
- **Astro 6** + adapter `@astrojs/node` (mode standalone)
- **React 19** via `@astrojs/react` pour les composants interactifs / sections complexes
- **Tailwind v4** via `@tailwindcss/vite`
- **`@rubis/ui`** — tokens + composants partagés avec `apps/web`
### Stratégie de rendu
`output: "server"` dans `astro.config.mjs` → SSR par défaut, override par page :
| Page | Mode | Raison |
|---|---|---|
| `/` (landing) | SSG (`prerender = true`) | HTML figé, LCP/CLS optimaux, copy change rarement |
| `/mentions-legales`, `/confidentialite`, `/cgv` | SSG | Pages légales, mises à jour planifiées |
| `/blog` | SSR | Doit refléter immédiatement les publications admin |
| `/blog/:slug` | SSR | Idem + 404 dynamique |
| `/blog/rss.xml`, `/sitemap.xml`, `/robots.txt` | SSR (endpoints `.ts`) | Liste à jour |
**Cache HTTP** : pages SSR avec `Cache-Control: public, max-age=300, stale-while-revalidate=86400`. Absorbe les pics, et un publish admin propage en ≤5 min sans purge active.
### Structure
```
apps/landing/
├── astro.config.mjs # output: server, adapter Node, tailwindcss plugin
├── src/
│ ├── pages/
│ │ ├── index.astro # / (SSG)
│ │ ├── mentions-legales.astro # SSG
│ │ ├── confidentialite.astro # SSG
│ │ ├── cgv.astro # SSG
│ │ ├── sitemap.xml.ts # SSR endpoint
│ │ ├── robots.txt.ts # SSR endpoint
│ │ └── blog/
│ │ ├── index.astro # /blog (SSR)
│ │ ├── [slug].astro # /blog/:slug (SSR)
│ │ └── rss.xml.ts # SSR endpoint
│ ├── layouts/
│ │ ├── Layout.astro # SEO complet, Header + Footer
│ │ └── LegalLayout.astro # wrapper prose pour pages légales
│ ├── components/
│ │ ├── SiteHeader.tsx # nav publique
│ │ ├── SiteFooter.tsx
│ │ ├── sections/ # 10 sections de la landing
│ │ └── blog/PostCard.tsx
│ ├── lib/api.ts # client API typé (fetch /api/v1/posts)
│ ├── styles/app.css # @import @rubis/ui/styles + @source
│ └── public/ # favicons, manifest
```
### Connexion à l'API
En SSR, Astro fetch directement le service Adonis via le DNS K3s (interne, pas de TLS) — cf. `API_URL` dans le ConfigMap `rubis-landing-config` :
```
API_URL=http://rubis-api.rubis.svc.cluster.local:3333
```
En dev local : `API_URL=http://localhost:3333` dans `.env.development`.
---
## 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.
---
## 5bis. packages/ui — design system partagé
Source unique de vérité pour la marque visuelle, consommée par `apps/web` (et bientôt `apps/landing`).
```
packages/ui/
├── src/
│ ├── styles/
│ │ ├── tokens.css # @theme Tailwind v4 : palette, fonts, radius, shadows
│ │ └── base.css # @layer base + @utility (eyebrow, shadow-rubis…)
│ ├── components/
│ │ ├── Brand.tsx # Lockup ◆ + "Rubis sur l'ongle"
│ │ ├── Gem.tsx # SVG gem facetté direction A
│ │ ├── Button.tsx # Variants : primary/secondary/ghost/link/danger × sm/md/lg
│ │ ├── Card.tsx # Variants : default/flat/hero × paddings
│ │ ├── Chip.tsx # Pastille sélectionnable (filtres, choix tonalité)
│ │ ├── Eyebrow.tsx # Petit label majuscule rubis avec ◆ préfixe
│ │ └── EmptyState.tsx # Message centré + icon + cta
│ ├── lib/cn.ts # Helper Tailwind merge (clsx + tailwind-merge)
│ └── index.ts # Barrel : exports nommés uniquement
├── package.json # peerDeps : react, react-dom, lucide-react
└── tsconfig.json # extends ../../tsconfig.base.json
```
**Conventions** :
- **Pas de build step** : on exporte directement les `.ts/.tsx` (cohérent avec `packages/shared`). Vite/tsc résout les imports en mode source.
- **Tokens en CSS** : Tailwind v4 lit `@theme {}` et génère automatiquement les classes (`bg-rubis`, `font-display`, `rounded-card`…). Pour ajouter une couleur, c'est dans `tokens.css`, nulle part ailleurs.
- **Consommation** :
```css
/* dans le CSS racine de l'app */
@import "@rubis/ui/styles/tokens.css";
@import "@rubis/ui/styles/base.css";
@source "../../../packages/ui/src/**/*.{ts,tsx}"; /* scan des composants */
```
```ts
import { Button, Card, Brand } from "@rubis/ui";
```
- **`peerDeps`** : `react`, `react-dom`, `lucide-react`. Le consommateur fournit ces deps (évite la duplication de bundle).
- **Ce qui RESTE app-spécifique** (`apps/web/src/components/`) : layouts (`AppLayout`, `AppSidebar`), formulaires liés à TanStack Form, composants couplés à des hooks d'auth/router.
---
## 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`.*