Compare commits

..

3 Commits

Author SHA1 Message Date
ordinarthur
fb248553a8 docs(audit-3/3): aligner brand + marketing sur le code livré
Audit cross-doc/code, batch brand & marketing : nettoyage de l'or
accent obsolète, correction des chemins cassés, retrait du hashtag
#recouvrement des templates LinkedIn (mot interdit en com publique
par marque.md).

docs/brand-identity.html
- §3 Palette : retrait du swatch « Or discret #B89F6B » et de la
  description « accent or à doser » (CLAUDE.md indiquait le caveat
  depuis le début, mais le HTML lui-même n'avait jamais été
  nettoyé). Reformulation alignée avec marque.md (rubis +
  neutres only).
- §Notes finales : « Or accent : à utiliser très peu » → « 
  retiré de la palette » avec renvoi vers marque.md
- (Variable CSS --gold gardée car référencée dans deux contextes
  inactifs lignes 24/203 — non visible côté affichage. Non
  bloquant.)

docs/marque.md
- §Préambule : lien `/brand-identity.html` → `/docs/brand-identity.html`
  (chemin réel)
- §Typographie : « Source Google Fonts » → « self-hosted via
  @fontsource-variable/{bricolage-grotesque,inter} » (réalité
  package.json)
- Header bumpé 2026-05-05 v0.1 → 2026-05-09 v0.2

docs/munitions-marketing.md
- §Pricing : retrait de la promesse « intégration banking » sur
  le plan Business — explicitement V2+ (CLAUDE.md V1 OUT). Note
  le check-in email comme alternative V1.

docs/marketing/playbook.md
- §Templates LinkedIn : hashtag `#recouvrement` → `#impayés` +
  warning explicite renvoyant à marque.md:151
- §Annexe ressources : chemin obsolète `/landing/index.html` →
  chemin réel `apps/landing/src/pages/index.astro`

docs/marketing/launch-kit.md (NOUVEAU — était untracked)
- Tracked avec correction des 2 hashtags #recouvrement →
  #impayés (variantes A et B des posts LinkedIn). La stat
  AFDCC ligne 39 (« taux de recouvrement ») est gardée — terme
  technique cité dans un contexte factuel, pas un positionnement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 19:17:27 +02:00
ordinarthur
9eaac0c7ef docs(audit-2/3): aligner doc tech sur le code livré
Audit cross-doc/code, batch tech : architecture, backend, frontend,
dev-setup. Corrige les claims qui pouvaient induire un dev en erreur
(noms de services K3s, hostnames Traefik, Tuyau, queue wrapper,
seeders, env vars, polices).

architecture.md
- Composants : status « À écrire » → «  Déployé » (apps/web,
  apps/api, packages/shared) ; ajout Redis Deployment K3s ; OCR =
  Mistral choisi ; mail = Resend (sortant) + OVH MX (entrant)
  validés
- §7.5 Pods K3s : noms réels (rubis-api / rubis-web / rubis-landing
  / rubis-redis, pas de *-svc) ; pas d'IngressRoute api.rubis.pro
  (l'API est servie via app.rubis.pro/api/* proxifié par nginx du
  pod web) ; PG/MinIO en URL directe dans la ConfigMap, pas de
  Service ExternalName
- §10 Décisions en attente : ADRs 019-024 mises à jour
  (tranchées / obsolètes), suppression du wording « à venir » pour
  les choix déjà figés dans le code

backend.md
- Note de cohérence en tête : pointe vers start/routes.ts comme
  source de vérité de la surface API (~80 routes — Stripe,
  Demo, AI, Microsoft SSO, admin blog, posts publics, KPIs
  timeseries) que cette doc n'inventorie pas exhaustivement
- §1 Vue d'ensemble : Tuyau marqué « non utilisé en pratique »
  (présent en deps mais zéro import côté SPA), partage de types
  via packages/shared. OCR Mistral choisi. Mail Resend choisi.
  BullMQ direct (workers inline pod API). Sentry ADR-024.
- §2 Stack : queue = BullMQ direct (pas @rlanz/bull-queue, qui
  n'est pas installé) ; type-sharing = packages/shared
- §2 Dépendances : remplacé la todo-list pré-livraison par la
  liste réelle des packages dans apps/api/package.json
- §3 Repo layout : `database/factories/` (dossier) → `factories.ts`
  (mono-fichier) ; `database/seeders/{default_plans,demo_data}` →
  inexistants, services à la place
- §13.2 Jobs : ProcessOcrJob + RecomputeKpisJob retirés
  (n'existent pas — OCR synchrone via services/import_batch.ts,
  KPIs calculés on-the-fly). Liste des jobs réels :
  send_relance, send_checkin, send_payment_thanks
- env vars : MINIO_* → S3_* (cf. .env.example + manifest k3s) ;
  bucket prod = rubis-prod-invoices

frontend.md
- Note de cohérence en tête : Tuyau pas utilisé, tokens dans
  packages/ui (pas inline), polices @fontsource-variable (pas
  Google Fonts via <link>)
- §1 Vue d'ensemble : client API = fetch minimaliste dans
  apps/web/src/lib/api.ts ; périmètre livré = ~15 routes _app/
- §3 Polices : section Google Fonts → @fontsource-variable
  (avec note preload woff2 critique sur la landing Astro)
- §4 Routes : arbo `_onboarding/` (faux) → `onboarding/`
  (réel, segment URL) + ajout admin.blog*, clients_.$id, insights,
  parametres_.abonnement, plans_.nouveau, factures_.import
- §6 Tuyau : section marquée « historique, non utilisé en V1 »
  avec note explicative en tête
- §10 env vars : VITE_API_URL=https://api.rubis.pro → vide
  (proxifié same-origin par nginx) + ajout VITE_USE_MOCKS,
  VITE_SENTRY_DSN_WEB, VITE_APP_VERSION

dev-setup.md
- Mailhog → Mailpit (3 occurrences) — c'est ce qui tourne dans
  docker-compose.dev.yml

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 19:13:44 +02:00
ordinarthur
801168fc74 docs(audit-1/3): aligner top-level + produit sur le code
Issu d'un audit de cohérence doc-vs-code. Corrige les claims périmés
sur le périmètre V1, la nomenclature des plans, les statuts de
facture et les sources typographiques.

CLAUDE.md
- IN/V1 : ajout Microsoft SSO + Stripe billing + mode démo (livrés)
- IN/V1 : « admin blog (à venir PR3) » → admin blog livré
- IN/V1 : 4 plans nommés concrètement (Standard B2B, Rapide, Patient,
  Ferme) au lieu de la mention vague « 4 plans fournis »
- Glossaire : « Check-in » → « Confirmation » (terme migré dans
  flow.md depuis ADR-008, alignement)
- Brand identité : typo Bricolage + Inter sont self-hosted via
  @fontsource-variable, pas Google Fonts via <link>
- Brand : lien `/brand-identity.html` → `/docs/brand-identity.html`
  (chemin réel) + retrait du caveat « or accent obsolète » (HTML
  sera nettoyé dans le batch brand)
- Bumpé la dernière maj 2026-05-07 → 2026-05-09

docs/produit.md
- §4.1 : ajout Microsoft SSO
- §4.3 : 4 plans réels (Standard B2B / Rapide / Patient / Ferme),
  variables Mustache réelles ({{client.name}}, {{amount}},
  {{dueDate}}), tons réels (amical | courtois | ferme |
  mise_en_demeure)
- §7 : trial commercial 30 jours (vs 14j historique). Note
  l'incohérence avec la « grâce 3 mois » dans billing.ts à fixer
  dans un PR séparé
- §8 : statut `relanced` → `in_relance` (constants/index.ts) +
  « check-in » → « confirmation »
- Header bumpé 2026-05-05 v0.1 → 2026-05-09 v0.2

docs/flow.md
- §6.1 : tons d'étapes corrigés (était amical|ferme|stricte —
  réel : amical|courtois|ferme|mise_en_demeure)
- Slug exemple : `standard-b2b` → `standard-30j` (réel)

docs/wireframes-mvp.html
- Compteur « 13 écrans » nuancé : c'était le scope low-fi initial,
  la SPA livrée en compte ~15 + onboarding multi-étapes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 19:06:59 +02:00
13 changed files with 573 additions and 157 deletions

View File

@ -36,12 +36,12 @@ TPE-PME françaises, 5 à 50 salariés, qui émettent 10 à 200 factures par moi
| **Couleur primaire** | `#9F1239` — rubis profond légèrement violacé. *Anti-Coca-Cola.* | | **Couleur primaire** | `#9F1239` — rubis profond légèrement violacé. *Anti-Coca-Cola.* |
| **Couleur secondaires** | `#771328` (deep), `#C9415C` (light), `#FBE4EA` (glow) | | **Couleur secondaires** | `#771328` (deep), `#C9415C` (light), `#FBE4EA` (glow) |
| **Neutres** | Crème `#FAF7F2`, encre chaude `#1A1410`. Jamais de blanc pur, jamais de noir pur. | | **Neutres** | Crème `#FAF7F2`, encre chaude `#1A1410`. Jamais de blanc pur, jamais de noir pur. |
| **Typo display** | Bricolage Grotesque (500800), Google Fonts | | **Typo display** | Bricolage Grotesque (500800), self-hosted via `@fontsource-variable/bricolage-grotesque` |
| **Typo body** | Inter (400700), Google Fonts | | **Typo body** | Inter (400700), self-hosted via `@fontsource-variable/inter` |
| **Icônes** | Lucide (regular weight) | | **Icônes** | Lucide (regular weight) |
| **Pas de** | or, bleu, vert, violet, emojis joaillerie 💎💰, mot "recouvrement" en com publique | | **Pas de** | or, bleu, vert, violet, emojis joaillerie 💎💰, mot "recouvrement" en com publique |
Voir `/docs/marque.md` pour la référence complète et `/brand-identity.html` pour la présentation visuelle (note : la mention de l'or accent dans ce fichier est obsolète, à ignorer). Voir `/docs/marque.md` pour la référence complète et `/docs/brand-identity.html` pour la présentation visuelle.
## Voix ## Voix
@ -55,7 +55,7 @@ Direct, concret, chaleureux, précis, empathique. *On parle comme un bon associ
- **Rubis** : unité de gamification. **1 rubis = 10 minutes libérées** = 1 relance qu'on n'a pas eu à faire à la main. - **Rubis** : unité de gamification. **1 rubis = 10 minutes libérées** = 1 relance qu'on n'a pas eu à faire à la main.
- **Plan de relance** : cadence d'emails automatisés (ex. J+3, J+10, J+20). Chaque facture est associée à un plan. - **Plan de relance** : cadence d'emails automatisés (ex. J+3, J+10, J+20). Chaque facture est associée à un plan.
- **Étape** : un email programmé dans un plan (ex. "J+10 — relance ferme"). - **Étape** : un email programmé dans un plan (ex. "J+10 — relance ferme").
- **Check-in** : email envoyé **à l'utilisateur** (pas au client) pour confirmer si une facture a été payée avant l'envoi de la prochaine relance. Remplace l'intégration banking en V1. - **Confirmation** *(anciennement « check-in »)* : email envoyé **à l'utilisateur** (pas au client) pour confirmer si une facture a été payée avant l'envoi de la prochaine relance. Remplace l'intégration banking en V1.
- **Mise en demeure** : étape ferme du plan. **Toujours sous validation manuelle** via modale de confirmation, jamais auto. - **Mise en demeure** : étape ferme du plan. **Toujours sous validation manuelle** via modale de confirmation, jamais auto.
- **DSO** : Days Sales Outstanding. Métrique secondaire dans l'app, jamais dans la com publique. - **DSO** : Days Sales Outstanding. Métrique secondaire dans l'app, jamais dans la com publique.
- **LME** : loi de modernisation de l'économie (2008). Plafonne les délais de paiement à 60 jours (ou 45 jours fin de mois). Sanctions DGCCRF jusqu'à 2 M€. - **LME** : loi de modernisation de l'économie (2008). Plafonne les délais de paiement à 60 jours (ou 45 jours fin de mois). Sanctions DGCCRF jusqu'à 2 M€.
@ -64,18 +64,20 @@ Direct, concret, chaleureux, précis, empathique. *On parle comme un bon associ
### IN ### IN
- Auth email/password + Google SSO - Auth email/password + Google SSO + Microsoft SSO
- Onboarding 3 étapes (compte, entreprise, signature email) - Onboarding 3 étapes (compte, entreprise, signature email)
- Upload drag-and-drop + OCR factures (PDF, PNG, JPG) - Upload drag-and-drop + OCR factures (PDF, PNG, JPG)
- Saisie manuelle (fallback) - Saisie manuelle (fallback)
- Bibliothèque de plans (4 plans fournis par défaut) - Bibliothèque de plans (4 plans fournis par défaut : *Standard B2B*, *Rapide*, *Patient*, *Ferme*)
- Éditeur de plan (cadence + templates email avec variables) - Éditeur de plan (cadence + templates email avec variables)
- Check-in email à l'utilisateur (cadence configurable) → confirme si payé → relance ou stop - Confirmation par email à l'utilisateur (cadence configurable) → confirme si payé → relance ou stop. *Anciennement « check-in ».*
- Dashboard avec compteur rubis + KPIs (à relancer, encaissé, DSO) - Dashboard avec compteur rubis + KPIs (à relancer, encaissé, DSO)
- Liste filtrable des factures - Liste filtrable des factures
- Détail facture avec timeline des relances - Détail facture avec timeline des relances
- Stripe billing (checkout, portal, webhook) + période de grâce post-signup
- Mode démo (sandbox in-app sans engagement)
- App mobile (web responsive) - App mobile (web responsive)
- **Blog `rubis.pro/blog`** — SSR par `apps/landing` (Astro 6), contenu en DB (`posts`) servi par `apps/api` via `/api/v1/posts/*`, admin de validation côté `app.rubis.pro/admin/blog` (à venir PR3), génération hebdomadaire IA via cron (Sonnet 4.6) avec review humaine obligatoire. Détails dans `/docs/tech/architecture.md`. - **Blog `rubis.pro/blog`** — SSR par `apps/landing` (Astro 6), contenu en DB (`posts`) servi par `apps/api` via `/api/v1/posts/*`, admin de validation côté `app.rubis.pro/admin/blog`, génération hebdomadaire IA via cron (Sonnet 4.6) avec review humaine obligatoire. Détails dans `/docs/tech/architecture.md`.
### OUT (V2 ou plus tard) ### OUT (V2 ou plus tard)
@ -180,4 +182,4 @@ Détails dans `/docs/tech/backend.md` §12.5.
--- ---
*Dernière mise à jour : 2026-05-07 · Maintenu par Arthur + Claude.* *Dernière mise à jour : 2026-05-09 · Maintenu par Arthur + Claude.*

View File

@ -680,7 +680,7 @@
<div class="section-head"> <div class="section-head">
<span class="num">03</span> <span class="num">03</span>
<h2>Palette</h2> <h2>Palette</h2>
<p>Une seule couleur dominante (le rubis), des neutres chauds, un accent or à doser. Pas de bleu — on n'est pas une fintech.</p> <p>Une seule couleur dominante (le rubis), des neutres chauds. Pas d'or, pas de bleu, pas de vert — on n'est pas une fintech, et le rubis suffit à signer la marque.</p>
</div> </div>
<h4>Couleur de marque</h4> <h4>Couleur de marque</h4>
@ -717,14 +717,9 @@
<div class="role">Backgrounds doux, panneaux mis en avant, succès</div> <div class="role">Backgrounds doux, panneaux mis en avant, succès</div>
</div> </div>
</div> </div>
<div class="swatch"> <!-- L'accent or (#B89F6B) a été retiré de la palette officielle.
<div class="chip" style="background: var(--gold);"></div> La marque s'en tient désormais à un système 100% rubis + neutres
<div class="meta"> crème/encre. Voir docs/marque.md (référence écrite). -->
<div class="name">Or discret</div>
<div class="hex">#B89F6B</div>
<div class="role">Accent rare · badges premium · jamais &gt; 5 % de l'écran</div>
</div>
</div>
</div> </div>
<h4>Neutres chauds</h4> <h4>Neutres chauds</h4>
@ -1119,7 +1114,7 @@
<li><b>Direction de logo :</b> recommandation A+C, mais la D est défendable si tu veux un positionnement plus polarisant côté éditorial.</li> <li><b>Direction de logo :</b> recommandation A+C, mais la D est défendable si tu veux un positionnement plus polarisant côté éditorial.</li>
<li><b>Bricolage Grotesque vs General Sans :</b> les deux sont gratuits. Bricolage a plus de caractère "français contemporain", General Sans est plus neutre/scalable. À tester côte à côte sur un vrai bloc de copy.</li> <li><b>Bricolage Grotesque vs General Sans :</b> les deux sont gratuits. Bricolage a plus de caractère "français contemporain", General Sans est plus neutre/scalable. À tester côte à côte sur un vrai bloc de copy.</li>
<li><b>Couleur primaire :</b> #9F1239 est mon best guess (rubis profond légèrement violacé). À tester sur écran imprimé/affiche — peut paraître trop sombre pour certains contextes outdoor. Variante plus vive : #B91C3C.</li> <li><b>Couleur primaire :</b> #9F1239 est mon best guess (rubis profond légèrement violacé). À tester sur écran imprimé/affiche — peut paraître trop sombre pour certains contextes outdoor. Variante plus vive : #B91C3C.</li>
<li><b>Or accent :</b> à utiliser <i>très</i> peu — éventuellement pour un badge "premium" ou un état spécial. Si on l'enlève complètement, ça simplifie sans appauvrir. À voir.</li> <li><b>Or accent :</b> <i>retiré de la palette</i> — la décision finale (cf. <code>docs/marque.md</code>) est de tenir sur 100 % rubis + neutres. Plus simple, plus signature.</li>
<li><b>Animation du gem :</b> potentiel logo animé sur la landing (rotation lente, scintillement) — à scoper en tant que micro-interaction plus tard.</li> <li><b>Animation du gem :</b> potentiel logo animé sur la landing (rotation lente, scintillement) — à scoper en tant que micro-interaction plus tard.</li>
<li><b>Iconographie :</b> on n'a pas tranché le set d'icônes UI (Lucide, Phosphor, custom ?). Pour le MVP, je recommande Lucide en regular weight — neutre et libre.</li> <li><b>Iconographie :</b> on n'a pas tranché le set d'icônes UI (Lucide, Phosphor, custom ?). Pour le MVP, je recommande Lucide en regular weight — neutre et libre.</li>
</ul> </ul>

View File

@ -265,15 +265,18 @@ Toutes appellent in fine la même logique métier (mêmes mutations DB, mêmes e
``` ```
Plan Plan
├── name: "Standard B2B" ├── name: "Standard B2B"
├── slug: "standard-b2b" ├── slug: "standard-30j"
├── description: "Plan équilibré pour la majorité des factures" ├── description: "Cadence sobre, ton qui monte progressivement."
└── steps[] └── steps[]
├── PlanStep { offsetDays: 3, tone: "amical", subject, body, requiresManualValidation: false } ├── PlanStep { offsetDays: 3, tone: "amical", subject, body, requiresManualValidation: false }
├── PlanStep { offsetDays: 10, tone: "ferme", subject, body, requiresManualValidation: false } ├── PlanStep { offsetDays: 10, tone: "courtois", subject, body, requiresManualValidation: false }
└── PlanStep { offsetDays: 25, tone: "stricte", subject, body, requiresManualValidation: true } └── PlanStep { offsetDays: 25, tone: "ferme", subject, body, requiresManualValidation: true }
mise en demeure → bouton manuel, mise en demeure → bouton manuel,
jamais d'envoi auto jamais d'envoi auto
Tons disponibles : `amical | courtois | ferme | mise_en_demeure`
(cf. `apps/api/app/services/default_plans.ts:18`).
``` ```
### 6.2 Plans pré-fournis ### 6.2 Plans pré-fournis

View File

@ -0,0 +1,371 @@
# Launch kit — J1 prêt à dégainer
> Contenu copy-paste pour activer ton acquisition tomorrow morning.
> Référence stratégique : `/docs/marketing/playbook.md`.
---
## 1. Profil LinkedIn fondateur (10 min de setup)
### Bannière (1584 × 396 px)
À générer rapidement (Canva / Figma). Composition simple :
- Fond `#FAF7F2` (cream)
- Le ◆ rubis (#9F1239) à gauche, taille moyenne
- Tagline en Bricolage Grotesque 700 à droite : *"Vos factures relancées toutes seules pendant que vous travaillez."*
- Sous-tagline en Inter 500, encre #4F4640 : *"L'app de relance pour TPE-PME françaises."*
- Coin en bas droite : `rubis.pro` en petit, italique
### Headline (220 caractères max)
**Option A — sobre :**
> Fondateur Rubis Sur l'Ongle · L'app qui automatise la relance de factures impayées pour les TPE-PME françaises
**Option B — plus accrocheuse (recommandée) :**
> J'aide les TPE et freelances à arrêter de courir après leurs factures impayées · Fondateur Rubis Sur l'Ongle (rubis.pro)
### About (3000 caractères max — colle ce qui suit)
```
Il y a deux ans, je facturais en freelance. Sur 12 factures émises ce mois-là, 5 ont été payées en retard. Une de 45 jours.
Le pire ? Le temps que j'ai passé à les relancer. Écrire des emails polis, vérifier qui avait répondu, attendre, relancer encore. Plusieurs heures par semaine. Pour de l'argent qu'on me devait déjà.
Je me suis dit que ça n'avait pas de sens.
Quelques chiffres qui m'ont confirmé que je n'étais pas seul :
• 44 jours — le retard moyen pour une facture de TPE en France (Altares)
• 15 milliards d'euros — la trésorerie bloquée chez les PME françaises
26 % — le taux de recouvrement quand on attend 30 jours pour relancer (AFDCC)
J'ai construit Rubis Sur l'Ongle pour que ce cercle vicieux s'arrête.
Tu glisses ta facture (PDF, photo, scan).
Tu choisis un plan de relance (J+3, J+10, J+20).
Et c'est tout.
Rubis envoie les emails au bon moment, sous ton nom, suit les ouvertures, et te demande discrètement avant chaque relance si la facture a été payée entre-temps.
Tu travailles. Rubis relance.
Bench moyen chez les utilisateurs d'outils similaires : 5 heures par semaine récupérées.
Si tu es freelance, dirigeant de TPE ou PME, et que les retards de paiement te bouffent du temps — viens jeter un œil : rubis.pro
DM ouvertes pour tes retours, tes objections, ou si tu veux juste discuter du sujet.
Arthur
```
### Lien du profil
Ajoute `rubis.pro` dans le champ "Site web personnel" du profil (Preferences → Profile → About → Add website).
### Photo de profil
- Pas de costume-cravate. Ton métier c'est de servir des entrepreneurs détendus, pas une banque.
- Lumière naturelle, sourire moyen, fond simple.
- Si tu n'as pas le temps de faire mieux : la photo que tu as déjà fera l'affaire. Évite juste les photos de soirée ou floues.
### Calendly publique
`cal.com` (gratuit). Crée un événement *"Démo Rubis (30 min)"*, dispos en semaine 9h-18h (Marseille), avec le formulaire qui demande : prénom, email, taille de boîte, nombre de factures/mois.
URL à mettre dans :
- Headline LinkedIn (en lien si possible)
- Signature email
- Bio Twitter / X si tu en as une
- About LinkedIn (à la fin, après "DM ouvertes")
---
## 2. Premier post LinkedIn — 3 variantes au choix
**Recommandation : Variante A** (storytelling, conversion la plus haute pour un lancement). Mais teste laquelle te ressemble le plus.
### Variante A — storytelling personnel (♥ recommandée)
```
Il y a 2 ans, je facturais en freelance.
5 factures sur 12 ont été payées en retard ce mois-là. Une de 45 jours.
Le pire ? Le temps que j'ai passé à les relancer.
À écrire des emails polis, à vérifier qui avait répondu, à attendre, à relancer encore.
Plusieurs heures par semaine. Pour de l'argent qu'on me devait déjà.
Je n'étais pas seul.
→ 44 jours : le retard moyen pour une facture de TPE en France (Altares, 2024)
→ 15 milliards d'euros bloqués chez les PME françaises
→ -26 % de chances d'être payé si tu attends 30 jours pour relancer (AFDCC)
J'ai construit Rubis Sur l'Ongle pour que ça s'arrête.
Tu glisses ta facture.
Tu choisis un plan de relance.
Tu travailles.
Rubis envoie les emails au moment prévu, sous ton nom, et te demande discrètement avant chaque relance si la facture a été payée entre-temps.
Bench moyen : 5 heures par semaine récupérées.
On est en ligne : rubis.pro
Si tu es freelance, dirigeant de TPE/PME, ou si tu connais quelqu'un qui galère sur ce sujet — DM ouvertes.
#freelance #TPE #PME #facturation #impayés
```
### Variante B — data-first
```
44 jours.
C'est le retard moyen pour une facture de TPE en France en 2024 (Altares).
15 milliards d'euros bloqués chez les PME.
+25 % de risque de faillite quand tu subis des retards.
-26 % de chances d'être payé si tu laisses traîner 30 jours.
Et pendant ce temps, les dirigeants de TPE passent en moyenne 5 à 8 heures par semaine à rédiger des emails de relance qu'ils détestent.
J'en ai eu marre.
J'ai construit Rubis Sur l'Ongle. Tu glisses tes factures, tu choisis un plan de relance (J+3, J+10, J+20), et c'est tout. L'app envoie, suit, relance. Tu récupères tes heures.
On est en ligne aujourd'hui : rubis.pro
Si tu factures du B2B et que les retards de paiement te bouffent ta semaine, viens tester. DM si questions.
#TPE #PME #facturation #impayés #frenchtech
```
### Variante C — directe, engagement bait
```
Question aux freelances et dirigeants de TPE/PME qui me lisent :
→ Combien de temps tu passes par semaine à relancer tes factures impayées ?
(Vraiment, je suis curieux. Réponds en commentaire ou en DM.)
Je pose la question parce que j'ai construit un truc autour de ça : Rubis Sur l'Ongle (rubis.pro).
Tu glisses tes PDF, tu choisis un plan de relance, et l'app envoie les emails à ta place — sous ton nom, au bon moment, en te tenant au courant.
Le bench que je vois chez les early users : ~5h/semaine récupérées.
Mais avant de te dire que c'est génial, j'aimerais d'abord t'écouter. C'est combien chez toi ? Et qu'est-ce qui te gonfle le plus dans ce processus ?
```
### Tips de post
- Publie le **mardi ou mercredi 8h-10h** ou **jeudi 17h-19h** (engagement LinkedIn FR le plus fort)
- **Réponds à TOUS les commentaires dans la 1ère heure** (l'algo LinkedIn boost les posts avec engagement précoce)
- **Tag personne** dans le post initial (ça paraît artificiel) — laisse les gens venir à toi
- **Re-partage le post 7 jours plus tard** avec un nouveau hook ("Une semaine après, voici ce qui s'est passé...")
---
## 3. Messages perso — 30 contacts à activer (J2)
Liste 30 contacts pro dans ton réseau warm (anciens collègues, école, amis entrepreneurs, famille avec activité pro). Adapte le message à chaque relation. **Ne JAMAIS copy-paste le même texte sur les 30** — ça se voit, ça brûle.
### Template 1 — Ancien collègue / dev / tech
```
Hey [prénom],
J'espère que [contexte boîte/projet — ex. la migration K8s chez Acme, le launch de votre nouvelle plateforme] roule bien.
De mon côté, je viens de lancer Rubis Sur l'Ongle (rubis.pro) — une app qui automatise la relance de factures impayées pour les TPE-PME et freelances B2B. Ça fait gagner ~5h/sem de relances à la main.
Je cherche 5 testeurs early adopters qui ont au moins 10 factures/mois. Tu connais quelqu'un dans ton réseau qui pourrait être intéressé ? Ou toi-même si tu factures en parallèle ?
Si oui, je te file un accès gratuit pendant 3 mois et je m'occupe perso de l'onboarding.
Pas de pression, dis-moi !
Arthur
```
### Template 2 — Ami entrepreneur / freelance
```
Salut [prénom],
T'as 30 secondes ? Je viens de lancer un truc et je pense direct à toi.
Rubis Sur l'Ongle (rubis.pro) — automatise la relance de factures impayées pour les indépendants et TPE B2B. Drag & drop des PDF, choix d'un plan de relance, c'est l'app qui envoie au moment prévu.
Vu que tu factures du B2B [détail spécifique de leur activité], ça pourrait te gagner ~5h/sem.
J'offre 3 mois Pro gratuits aux 5 premiers testeurs warm — tu prends ?
Si pas pour toi, tu connais quelqu'un dans ta sphère qui galère avec ce sujet ?
```
### Template 3 — Connaissance pro / famille avec activité
```
Hello [prénom],
J'espère que tu vas bien. J'ai pensé à toi parce que [contexte — ex. tu m'avais raconté que tes clients payaient avec X jours de retard / parce que ton fils m'avait dit que tu lances ta boîte].
Je viens de lancer Rubis Sur l'Ongle (rubis.pro), une app qui automatise les relances de factures impayées. C'est pensé pour les TPE et freelances qui en ont marre de courir après l'argent qu'on leur doit.
Si ça parle à ton activité (ou si tu connais quelqu'un autour de toi à qui ça parlerait), j'offre 3 mois Pro gratuits aux 5 premiers testeurs.
Sinon dis-moi juste ce que tu en penses, ton retour m'intéresse.
À bientôt,
Arthur
```
### Règles d'or pour ces 30 messages
1. **Personnalisation obligatoire** — le détail [contexte] doit être réel, pas générique.
2. **Demande claire** — soit "tu testes" soit "tu me présentes quelqu'un". Pas les deux dans la même phrase.
3. **Pas de gros pitch** — 4 lignes max, le but c'est démarrer une conversation.
4. **Pas le dimanche ni le lundi matin** — préfère mardi-jeudi en milieu de journée.
### Ratio attendu
Sur 30 messages :
- ~10 répondent (bon ou mauvais)
- ~5 introduisent un contact warm
- ~2-3 testent eux-mêmes
- = **3 à 5 testeurs/clients en 7-10 jours**
---
## 4. Dream 100 — template prêt à copier
Crée un Notion ou Google Sheet avec ces colonnes :
| # | Prénom Nom | Entreprise | Avatar | Volume estimé | Lieu | LinkedIn | Email pro | Source | Statut | Dernier contact | Prochain follow-up | Notes |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
**Définition des colonnes** :
- **Avatar** : `1` (consultant freelance B2B premium), `2` (TPE service B2B 5-15 sal), `3` (entrepreneur récent 12-24 mois). Cf. playbook section 2.
- **Volume estimé** : nombre de factures/mois (estimer à partir de leur activité).
- **Source** : `Sales Nav`, `LinkedIn organique`, `Recommandation de [nom]`, `Forum X`, etc.
- **Statut** : `À contacter``Connect envoyé``Accepté``DM envoyé``Démo bookée``Démo faite``Client` / `Out`
- **Notes** : verbatim de leur profil (ce qu'ils ont posté, leur pain point visible). C'est ce qui te permet de personnaliser ton message à 99 %.
### Filtres Sales Navigator pour Avatar 1 (consultant freelance B2B)
Connecte-toi à [linkedin.com/sales](https://linkedin.com/sales) (essai gratuit 1 mois si pas encore abonné), puis :
- **Géographie** : France
- **Taille de l'entreprise** : 1-10 employés
- **Fonction** : Founder, Owner, Self-employed, Independent Consultant
- **Secteur** : Services aux entreprises, Conseil, Marketing & publicité, IT services
- **Mots-clés dans le poste actuel** : "freelance" OR "consultant" OR "indépendant" OR "fondatrice" OR "fondateur"
→ Tu obtiens ~2000-5000 résultats. **Ne fais pas tout** — prends les 30 premiers qui sont actifs sur LinkedIn (dernier post < 30 jours = signal qu'ils utilisent vraiment la plateforme et risquent de répondre).
### Filtres Sales Navigator pour Avatar 2 (TPE service B2B 5-15 sal)
- **Géographie** : France (cibler Paris, Lyon, Marseille, Toulouse, Bordeaux, Nantes en priorité)
- **Taille** : 11-50 employés
- **Fonction** : CEO, Founder, Managing Director, Directeur Général, Gérant
- **Secteur** : Marketing, Conseil, Services IT, RH, Comptabilité
### Sources gratuites (sans Sales Navigator)
- **Maltverse / Malt** — annuaires de freelances, profils publics
- **Twitter recherche** : `"facture impayée" lang:fr min_replies:2`
- **Communautés Slack freelances FR** — observer qui parle de facturation/cash flow
- **Annuaire sociétés.com** : taille + secteur + géo, payant mais utilisable en V2
### 5 exemples concrets de prospects à mettre dans la liste
| # | Type | Comment trouver | Pourquoi parfait |
|---|---|---|---|
| 1 | Une consultante RH freelance qui a tweeté son ras-le-bol des retards | Twitter `"retard de paiement" lang:fr` | Pain point exprimé = warm de fait |
| 2 | Le gérant d'une agence digitale 8 personnes à Lyon | LinkedIn taille 5-10 + Lyon + agence | Volume pertinent + LinkedIn-actif |
| 3 | Un développeur freelance qui poste sur sa facturation | LinkedIn recherche "freelance dev" actif | Public engageant + aligné avec toi |
| 4 | Un cabinet d'avocats indépendant à Marseille | Annuaire CCI + LinkedIn vérification | Pain point classique + ta zone géo |
| 5 | Un prestataire SEO indépendant qui poste sur le cash flow | LinkedIn hashtag #freelance #cashflow | Triple match : profil + thématique + actif |
---
## 5. Plan de bataille — 7 jours, heure par heure
### J1 — Lundi (2h30)
- 09:00 → 10:00 : LinkedIn profile (bannière, headline, about, photo)
- 10:00 → 10:15 : Setup Calendly + insertion lien dans LinkedIn
- 10:15 → 11:30 : Construire la Dream 100 spreadsheet avec les **30 premiers** prospects (cf. filtres section 4)
### J2 — Mardi (1h30)
- 09:00 → 09:30 : Publier le post LinkedIn (variante A par défaut)
- 09:30 → 11:00 : Envoyer 30 messages perso (templates section 3) à ton réseau warm
- Toute la journée : **répondre à chaque commentaire / DM dans l'heure** sur le post
### J3 — Mercredi (1h)
- 14:00 → 15:00 : Connect requests LinkedIn aux 30 premiers Dream 100 (note personnalisée pour chacun, cf. playbook section 8.1)
### J4 — Jeudi (1h30)
- 09:00 → 09:30 : Suivi des connect acceptés depuis hier → DM 1ᵉʳ touche
- 09:30 → 10:30 : Préparer 5 cold emails ultra-personnalisés (5 min de recherche par prospect)
- 10:30 → 11:00 : Envoyer les 5 cold emails
### J5 — Vendredi (1h30 — variable selon les bookings)
- Honorer les **interviews découverte** bookées via Calendly
- Notes denses dans Notion (vocabulaire prospect, pain points cités, demande de référence)
### Week-end
Repos. Tu as travaillé.
### J7 — Dimanche soir (30 min)
- Compter : connects envoyés, acceptés, DMs envoyés, emails envoyés, démos bookées
- Identifier ce qui a marché vs raté (taux d'acceptation < 30 % revoir la note de connect)
- Préparer le batch de la semaine 2
---
## 6. Stack outils gratuits/cheap pour démarrer
| Besoin | Outil | Coût | Pourquoi |
|---|---|---|---|
| CRM Dream 100 | Notion (gratuit) ou Folk (gratuit) | 0 € | Suffit largement pour 100 prospects |
| Booker démos | Cal.com | 0 € | Open-source, dispo Calendly-like |
| Trouver emails | Hunter (essai 25 emails/mois) | 0 € | Pour les cold emails, suffisant J1-J7 |
| Outbound LinkedIn | LinkedIn manuel | 0 € | Sales Navigator (~80 €/mois) seulement si tu accélères en S2 |
| Tracking landing | Plausible | déjà branché | Tu vois en temps réel les visites depuis ton post |
**Total budget acquisition semaine 1** : 0 €.
---
## 7. Ce qu'il NE faut PAS faire en S1
- ❌ Lancer un Product Hunt — pas le moment, prépare-le pour S6 quand tu as 10 témoignages
- ❌ Activer du paid (Google/LinkedIn Ads) — pas de baseline pour étalonner
- ❌ Spammer LinkedIn (200 connects/jour) — ton compte se fait limiter, ton domaine mail aussi
- ❌ Construire de nouvelles features parce que "ça serait cool" — tu DOIS résister, c'est le moment d'écouter, pas de coder
- ❌ Te décourager si tu fais 0 démo bookée la première semaine — c'est normal, ça démarre vraiment en S2
---
## 8. Mantras à se répéter
> *"Le seul KPI qui compte cette semaine c'est : nombre de conversations qualifiées."*
> *"Demande une référence à chaque conversation : 'Tu connais quelqu'un d'autre qui galère sur ce sujet ?'"*
> *"5 conversations / semaine. Toutes les semaines. Pas de magie ailleurs."*
---
*Maintenu par Arthur + Claude. Si tu plafonnes après 2 semaines, on revient en discussion pour ajuster le plan plutôt que d'envoyer plus de messages dans le vide.*

View File

@ -459,7 +459,9 @@ Tu peux démarrer demain. Voici le calendrier minute par minute :
> >
> *On lance les premiers comptes au printemps 2026. Si tu es freelance ou TPE et que tu en as marre de courir après ton argent, viens sur la waitlist : [lien]* > *On lance les premiers comptes au printemps 2026. Si tu es freelance ou TPE et que tu en as marre de courir après ton argent, viens sur la waitlist : [lien]*
> >
> *#freelance #facturation #TPE #PME #recouvrement* > *#freelance #facturation #TPE #PME #impayés*
⚠️ **Pas de `#recouvrement`** en com publique (cf. `marque.md:151`). Le mot signale un produit "DAF" auquel on ne veut pas être assimilés.
À adapter avec ta voix mais garde la structure : storytelling personnel → stat-choc → produit → bénéfice chiffré → CTA waitlist. À adapter avec ta voix mais garde la structure : storytelling personnel → stat-choc → produit → bénéfice chiffré → CTA waitlist.
@ -483,7 +485,7 @@ Tu peux démarrer demain. Voici le calendrier minute par minute :
- `/docs/marque.md` — voix et ton à reprendre dans tous les messages - `/docs/marque.md` — voix et ton à reprendre dans tous les messages
- `/docs/munitions-marketing.md` — chiffres (44j, 15 Md€, 26 %) à citer dans la copy outreach - `/docs/munitions-marketing.md` — chiffres (44j, 15 Md€, 26 %) à citer dans la copy outreach
- `/docs/decisions.md` — décisions produit pour répondre aux objections lors des démos - `/docs/decisions.md` — décisions produit pour répondre aux objections lors des démos
- `/landing/index.html` — la page que les prospects voient en premier - `apps/landing/src/pages/index.astro` — la page que les prospects voient en premier (Astro 6 SSR sur `https://rubis.pro`)
--- ---

View File

@ -1,8 +1,8 @@
# Référence de marque — Rubis Sur l'Ongle # Référence de marque — Rubis Sur l'Ongle
> Version : 0.1 · Dernière maj : 2026-05-05 > Version : 0.2 · Dernière maj : 2026-05-09
Ce document est la source de vérité écrite pour la marque. Pour la présentation visuelle, voir `/brand-identity.html`. Quand les deux divergent, ce fichier gagne. Ce document est la source de vérité écrite pour la marque. Pour la présentation visuelle, voir [`/docs/brand-identity.html`](./brand-identity.html). Quand les deux divergent, ce fichier gagne.
--- ---
@ -86,8 +86,8 @@ Un rubis géométrique vu de dessus, kite/diamond shape avec facettes suggérée
| Usage | Typo | Source | Poids utilisés | | Usage | Typo | Source | Poids utilisés |
|---|---|---|---| |---|---|---|---|
| Display (titres) | **Bricolage Grotesque** | Google Fonts | 500, 600, 700, 800 | | Display (titres) | **Bricolage Grotesque** | self-hosted via `@fontsource-variable/bricolage-grotesque` | 500, 600, 700, 800 |
| Body (texte courant, UI) | **Inter** | Google Fonts | 400, 500, 600, 700 | | Body (texte courant, UI) | **Inter** | self-hosted via `@fontsource-variable/inter` | 400, 500, 600, 700 |
### Échelle typographique ### Échelle typographique

View File

@ -202,7 +202,7 @@ Pas de chiffres figés tant qu'on n'a pas testé, mais les bornes du marché don
- **Free** : jusqu'à 5 factures actives en relance (attire les freelances, montre la valeur). - **Free** : jusqu'à 5 factures actives en relance (attire les freelances, montre la valeur).
- **Pro** : 19 €/mois (1050 factures, OCR illimité, 4 plans, 1 utilisateur) → **prix d'entrée agressif** vs Sellsy/Axonaut, défensable car on est mono-produit. - **Pro** : 19 €/mois (1050 factures, OCR illimité, 4 plans, 1 utilisateur) → **prix d'entrée agressif** vs Sellsy/Axonaut, défensable car on est mono-produit.
- **Business** : 49 €/mois (illimité, multi-utilisateurs, branding email, intégration banking). - **Business** : 49 €/mois (illimité, multi-utilisateurs V2, branding email, SMS V2). *L'intégration banking est explicitement V2+ — on ne la promet pas en V1, la confirmation paiement passe par les emails de check-in à l'utilisateur.*
> Le sweet spot du discours : *« moins cher qu'une heure de votre temps mensuel »*. À 50 €/h facturés, 19 €/mois est trivial à vendre. > Le sweet spot du discours : *« moins cher qu'une heure de votre temps mensuel »*. À 50 €/h facturés, 19 €/mois est trivial à vendre.

View File

@ -1,6 +1,6 @@
# Spécification produit — Rubis Sur l'Ongle # Spécification produit — Rubis Sur l'Ongle
> Version : 0.1 (MVP) · Dernière maj : 2026-05-05 > Version : 0.2 (MVP — blog + Stripe livrés) · Dernière maj : 2026-05-09
Ce document est la source de vérité produit. Il décrit ce qu'on fait, pour qui, comment, et **ce qu'on ne fait pas**. Quand un wireframe et ce document divergent, ce document gagne. Ce document est la source de vérité produit. Il décrit ce qu'on fait, pour qui, comment, et **ce qu'on ne fait pas**. Quand un wireframe et ce document divergent, ce document gagne.
@ -44,7 +44,7 @@ Rubis Sur l'Ongle libère le temps des dirigeants de TPE-PME en automatisant la
### 4.1 Authentification & onboarding ### 4.1 Authentification & onboarding
- Inscription email/password (2 champs minimum) - Inscription email/password (2 champs minimum)
- Google SSO en option - Google SSO + Microsoft SSO en option (via Adonis Ally)
- Onboarding 3 étapes au max : - Onboarding 3 étapes au max :
1. Compte (email, mot de passe) 1. Compte (email, mot de passe)
2. Entreprise (nom, SIRET facultatif, secteur, volume mensuel via chips) 2. Entreprise (nom, SIRET facultatif, secteur, volume mensuel via chips)
@ -61,13 +61,14 @@ Rubis Sur l'Ongle libère le temps des dirigeants de TPE-PME en automatisant la
### 4.3 Plans de relance ### 4.3 Plans de relance
- **Bibliothèque** : 4 plans pré-fournis par défaut - **Bibliothèque** : 4 plans pré-fournis par défaut (cf. `apps/api/app/services/default_plans.ts`)
- *Standard B2B* : J+3, J+10, J+20 (mise en demeure brouillon) - *Standard B2B* (`standard-30j`) : J+3, J+10, J+25 — *amical → courtois → mise en demeure*
- *Premium clients VIP* : J+7, J+15, J+30 — ton doux - *Rapide* (`rapide-15j`) : J+1, J+7, J+15 — cadence resserrée pour récurrents / délais courts
- *Court cash flow tendu* : J+1, J+5, J+10 — ton ferme - *Patient* (`patient-60j`) : J+15, J+30 — clients de longue date, on laisse respirer
- *Personnalisable* : à créer par l'utilisateur - *Ferme* (`ferme-7j`) : cadence stricte pour clients à risque
- **Éditeur** : cadence à gauche, contenu email à droite. Variables sous forme de chips drag-to-insert (`{{prenom_client}}`, `{{numero}}`, `{{montant}}`, `{{date_echeance}}`, `{{signature}}`) - **Personnalisation** : l'utilisateur crée ses propres plans via `/plans/nouveau` (variantes / sur-mesure)
- **Tonalité** affichée comme tag visible (Doux / Standard / Ferme) - **Éditeur** : cadence à gauche, contenu email à droite. Variables sous forme de chips drag-to-insert (`{{client.name}}`, `{{numero}}`, `{{amount}}`, `{{dueDate}}`, `{{signature}}`)
- **Tonalité** affichée comme tag visible — 4 valeurs : `amical`, `courtois`, `ferme`, `mise_en_demeure` (la dernière déclenche `requiresManualValidation: true`)
### 4.4 Check-in email (réconciliation V1) ### 4.4 Check-in email (réconciliation V1)
@ -171,14 +172,14 @@ Rubis Sur l'Ongle libère le temps des dirigeants de TPE-PME en automatisant la
**Stratégie** : prix d'entrée agressif vs Sellsy/Axonaut (~30 €) défendable parce qu'on est mono-produit. Argument : *"moins cher qu'une heure de votre temps mensuel"*. **Stratégie** : prix d'entrée agressif vs Sellsy/Axonaut (~30 €) défendable parce qu'on est mono-produit. Argument : *"moins cher qu'une heure de votre temps mensuel"*.
**Trial** : 14 jours offerts sans CB, puis fallback automatique sur Free. **Trial commercial** : 30 jours d'essai sans carte bancaire (CTA landing : *« Commencer l'essai 30 jours »*). Le code utilise actuellement une *« période de grâce 3 mois »* post-signup côté `apps/api/app/services/billing.ts` — incohérence à aligner sur 30 jours dans un PR séparé (cf. `docs/decisions.md`).
## 8. Notes architecture (à formaliser avec stack) ## 8. Notes architecture (à formaliser avec stack)
Quelques contraintes produit qui doivent guider l'architecture : Quelques contraintes produit qui doivent guider l'architecture :
- **Statuts de facture pivotables** : `pending`, `awaiting_user_confirmation`, `paid`, `relanced`, `litigation`, `cancelled` — pas hardcodés au flow email - **Statuts de facture pivotables** : `pending`, `awaiting_user_confirmation`, `paid`, `in_relance`, `litigation`, `cancelled` — pas hardcodés au flow email (cf. `packages/shared/src/constants/index.ts`)
- **Modèle d'événement abstrait** pour le check-in : un `payment_status_check` peut être déclenché par email-réponse en V1, par webhook banking en V2 — l'app ne doit pas savoir d'où vient le signal - **Modèle d'événement abstrait** pour la confirmation : un `payment_status_check` peut être déclenché par email-réponse en V1, par webhook banking en V2 — l'app ne doit pas savoir d'où vient le signal
- **Multi-tenant dès le jour 1** : même si V1 est mono-user par compte, la structure DB doit supporter `organization``users``invoices` pour ne pas refactorer en V2 - **Multi-tenant dès le jour 1** : même si V1 est mono-user par compte, la structure DB doit supporter `organization``users``invoices` pour ne pas refactorer en V2
- **OCR** : provider abstrait derrière une interface — on doit pouvoir switcher Mindee → Document AI → Tesseract sans toucher au reste - **OCR** : provider abstrait derrière une interface — on doit pouvoir switcher Mindee → Document AI → Tesseract sans toucher au reste
- **Email outbound** : provider abstrait (Resend / Postmark / SendGrid) avec retry et tracking - **Email outbound** : provider abstrait (Resend / Postmark / SendGrid) avec retry et tracking

View File

@ -44,14 +44,16 @@ Ce document est la source de vérité technique. Quand le code et ce fichier div
| Composant | Rôle | Hosting | Status | | Composant | Rôle | Hosting | Status |
|---|---|---|---| |---|---|---|---|
| `apps/landing` | Landing + blog publics (Astro 6 SSR) | Pod Node K3s (`rubis-landing:4321`) | ✅ Déployé | | `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/web` | SPA React + TanStack Router/Query (SaaS) | nginx pod K3s (build statique + proxy `/api/*` → rubis-api) | ✅ Déployé |
| `apps/api` | API REST AdonisJS — logique métier, jobs, email | Pod Node K3s (`rubis-api:3333`) | À écrire | | `apps/api` | API REST AdonisJS v7 — logique métier, jobs BullMQ inline, email, billing Stripe | Pod Node K3s (`rubis-api:3333`) | ✅ Déployé |
| `packages/shared` | Types TS, schemas Zod, constantes communes | workspace local | À écrire | | `packages/shared` | Types TS, schemas Zod, constantes (statuts, tons, plans, etc.) | workspace local | ✅ |
| `packages/ui` | Design system partagé (tokens Tailwind + composants TSX) | workspace local | ✅ | | `packages/ui` | Design system partagé (tokens Tailwind v4 + composants TSX) | workspace local | ✅ |
| PostgreSQL | Base de données métier | LXC Proxmox existant | ✅ En place | | PostgreSQL | Base de données métier | LXC Proxmox existant (`10.10.10.3`) | ✅ En place |
| MinIO | Stockage PDF + pièces jointes (S3-compat) | LXC Proxmox existant | ✅ En place | | MinIO | Stockage PDF + uploads blog (S3-compat, bucket `rubis-prod-invoices`) | namespace K3s `minio` | ✅ En place |
| Provider OCR | Extraction texte des factures | Externe (HTTPS) | ADR-020 à venir | | Redis | Backend BullMQ (queues: relances, checkins, payment-thanks) | Deployment K3s `rubis-redis` + PVC | ✅ Déployé |
| Provider Email | Envoi outbound (relances + check-in) | Externe (HTTPS) | ADR-021 à venir | | Provider OCR | Extraction texte des factures | Mistral (`OCR_PROVIDER=mistral`) | ✅ Choisi |
| Provider Email outbound | Resend (`relances@rubis.pro`, sub-domaine `send.rubis.pro`) | Externe (HTTPS) | ✅ En place |
| Provider Email inbound | OVH MX (`contact@rubis.pro`, `dev@rubis.pro`) | Externe (HTTPS/IMAP) | ✅ En place |
--- ---
@ -487,21 +489,25 @@ SPA retry l'appel original avec nouveau token
### Pods K3s ### Pods K3s
```yaml ```yaml
# Namespace: rubis # Namespace: rubis (cf. k3s/app/*.yml)
- Deployment: rubis-api # AdonisJS Node, port 3333 - Deployment: rubis-api # AdonisJS Node, port 3333 (workers BullMQ inline, pas de pod séparé)
- Deployment: rubis-web # nginx, sert le bundle Vite, port 80 - Deployment: rubis-web # nginx, sert le bundle Vite + proxy /api → rubis-api:3333, port 80
- Deployment: rubis-landing # déjà existant - Deployment: rubis-landing # Astro 6 SSR Node, port 4321
- Service: rubis-api-svc # ClusterIP - Deployment: rubis-redis # Redis 7 + PVC pour BullMQ
- Service: rubis-web-svc # ClusterIP - Service: rubis-api # NodePort 30100 → 3333
- Service: postgres-external # ExternalName → IP du LXC postgres - Service: rubis-web # NodePort 30110 → 80
- Service: minio-external # ExternalName → IP du LXC minio - Service: rubis-landing # NodePort 30111 → 4321
- Secret: rubis-config # DB credentials, MinIO credentials, OCR API key, mail API key - Service: rubis-redis # ClusterIP 6379
- IngressRoute (Traefik) : - ConfigMap: rubis-api-config + rubis-landing-config + rubis-web-config
api.rubis.pro → rubis-api-svc:3333 - Secret: rubis-api-secrets # DB credentials, S3 credentials, OCR/Mistral API key, Resend key, Stripe keys
app.rubis.pro → rubis-web-svc:80 - Routing Traefik (config dynamique sur la VM gateway, repo proxmox) :
rubis.pro → rubis-landing-svc:80 rubis.pro → 10.10.10.5:30111 (rubis-landing)
app.rubis.pro/* → 10.10.10.5:30110 (rubis-web — qui proxie /api/* vers rubis-api en interne K3s)
# Note : pas de hostname `api.rubis.pro` exposé. L'API est servie via app.rubis.pro/api/*.
``` ```
**Ressources externes (LXC Proxmox, hors K3s)** : PostgreSQL (`PG_HOST=10.10.10.3` en clair dans `rubis-api-config`), MinIO (DNS `minio.minio.svc.cluster.local:9000` via le namespace MinIO du même cluster, bucket prod = `rubis-prod-invoices`). Pas de `Service ExternalName` créé, les URLs sont posées directement dans la ConfigMap.
### Pipeline CI Gitea ### Pipeline CI Gitea
``` ```
@ -554,13 +560,14 @@ healthchecks readinessProbe → service public
À trancher avant fin V1, par ordre de priorité : À trancher avant fin V1, par ordre de priorité :
| # | Sujet | Échéance suggérée | | # | Sujet | Statut |
|---|---|---| |---|---|---|
| 019 | **Domain model** (entités, relations, index) | Avant la 1ère migration | | 019 | **Domain model** (entités, relations, index) | ✅ tranché — modèles dans `apps/api/app/models/` |
| 020 | **Provider OCR** (Mindee, Document AI, Textract, Tesseract) | Avant l'implémentation du job ProcessOcr | | 020 | **Provider OCR** | ✅ tranché — Mistral (`OCR_PROVIDER=mistral`, cf. `.env.example` + `k3s/app/api.yml`). ADR à formaliser |
| 021 | **Provider email** (Resend, Postmark, SendGrid, AWS SES) | Avant l'implémentation des relances | | 021 | **Provider email** | ✅ tranché — Resend pour le sortant, OVH MX pour l'entrant (cf. `docs/tech/backend.md` §12.5). ADR à formaliser |
| 022 | **Pricing exact** (Free 5 factures ? Pro 19 €/mois ?) | Avant le payment flow | | 022 | **Pricing exact** | ✅ tranché — Free 5 factures / Pro 19 € / Business 49 € (cf. `apps/api/app/services/billing.ts:34`) |
| 023 | **Endpoint waitlist** (Resend / Formspree / API Adonis) | Au push de la landing en prod | | 023 | **Endpoint waitlist** | ❌ obsolète — la landing pousse directement vers signup, pas de waitlist en prod |
| 024 | **Sentry pour error tracking** | ✅ tranché — voir `docs/decisions.md` ADR-024 |
--- ---

View File

@ -1,18 +1,21 @@
# Guide d'implémentation — Backend # Guide d'implémentation — Backend
> Version : 0.1 · Dernière maj : 2026-05-06 > Version : 0.2 · Dernière maj : 2026-05-09
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-016 (PG), ADR-017 (auth), ADR-018 (storage). > Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-016 (PG), ADR-017 (auth), ADR-018 (storage), ADR-024 (Sentry).
Ce document est le **guide pratique d'implémentation de l'API**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes, snippets, conventions, et — surtout — le **contrat exact** que le SPA attend déjà côté front (les mocks MSW de `apps/web/src/mocks/handlers/` sont la source de vérité du contrat actuel). > ⚠️ **Note de cohérence (audit 2026-05-09)** : ce guide a été rédigé en phase de planification. Plusieurs claims ont divergé du code livré. Pour la **liste exhaustive des routes**, lire **`apps/api/start/routes.ts`** comme source de vérité (~80 routes : auth, factures, clients, plans, billing Stripe, blog admin/public, demo, AI, uploads, KPIs). Les modèles vivants sont dans `apps/api/app/models/`, les services dans `apps/api/app/services/`. Les sections ci-dessous sont valides **dans leur esprit** mais détaillent parfois des choix pré-livraison qui ont évolué.
Ce document est le **guide pratique d'implémentation de l'API**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes, snippets, conventions.
**À lire avant** : **À lire avant** :
- `/CLAUDE.md` — contexte top-level - `/CLAUDE.md` — contexte top-level
- `/docs/produit.md` — flows utilisateur, IN/OUT V1 - `/docs/produit.md` — flows utilisateur, IN/OUT V1
- `/docs/tech/architecture.md` — vue d'ensemble du système - `/docs/tech/architecture.md` — vue d'ensemble du système
- `/docs/tech/frontend.md` — guide d'implémentation du SPA (utile pour comprendre ce que le back doit servir) - `/docs/tech/frontend.md` — guide d'implémentation du SPA
- `/packages/shared/src/` — types et schemas Zod déjà partagés - `/packages/shared/src/` — types et schemas Zod partagés (statuts, tons, plans, billing)
- `/apps/web/src/mocks/handlers/` — **le contrat API tel qu'il est consommé** - `/apps/api/start/routes.ts` — **contrat API actuel (source de vérité)**
- `/apps/api/app/services/` — logique métier (billing, posts, blog_uploads, default_plans, import_batch, etc.)
--- ---
@ -22,12 +25,15 @@ L'API (`apps/api/`) est un **AdonisJS v7** en TypeScript, qui sert :
- **JSON-only** sur `/api/v1/*` (pas de Inertia, pas de Hypermedia — le SPA est un consommateur séparé) - **JSON-only** sur `/api/v1/*` (pas de Inertia, pas de Hypermedia — le SPA est un consommateur séparé)
- **Auth Bearer** stateless via `@adonisjs/auth` access_tokens (cf. ADR-017), avec un refresh token cookie httpOnly géré custom par-dessus - **Auth Bearer** stateless via `@adonisjs/auth` access_tokens (cf. ADR-017), avec un refresh token cookie httpOnly géré custom par-dessus
- **Tuyau** pour générer le client TS typé consommé par le SPA → contrat API ↔ web verrouillé par le compilateur - **Type-sharing** via `packages/shared` (Zod schemas + types TS), consommé en workspace symlink par `apps/web` et `apps/landing`. *Note historique : la doc évoquait Tuyau pour générer un client TS typé — `@tuyau/core` est dans les deps mais n'est pas utilisé en pratique côté SPA, qui consomme l'API via un client `fetch()` minimaliste dans `apps/web/src/lib/api.ts`.*
- **PostgreSQL** comme base relationnelle (cf. ADR-016, instance LXC Proxmox existante) - **PostgreSQL** comme base relationnelle (cf. ADR-016, instance LXC Proxmox existante, IP `10.10.10.3`)
- **MinIO** pour les pièces jointes (cf. ADR-018) - **MinIO** pour les pièces jointes (cf. ADR-018, namespace K3s `minio`, bucket `rubis-prod-invoices`)
- **Provider OCR** externe (à benchmarker, ADR-020) - **OCR** : Mistral (`OCR_PROVIDER=mistral`)
- **Provider Email** externe (à benchmarker, ADR-021) - **Email outbound** : Resend (sub-domaine `send.rubis.pro`)
- **Background jobs** pour l'OCR différé, les relances programmées, les check-ins - **Email inbound** : OVH MX (`contact@rubis.pro`)
- **Background jobs** : BullMQ (Redis), workers inline dans le pod API (pas de pod worker séparé)
- **Billing** : Stripe (checkout, customer portal, webhook `/billing/webhook`)
- **Error tracking** : Sentry (cf. ADR-024)
Le scaffold initial a été créé via `pnpm create adonisjs@latest -- apps/api --kit=api --pkg=pnpm`, kit `api`. Ça nous a déjà donné : Le scaffold initial a été créé via `pnpm create adonisjs@latest -- apps/api --kit=api --pkg=pnpm`, kit `api`. Ça nous a déjà donné :
@ -50,31 +56,33 @@ Tout le reste (org, clients, factures, plans, jobs, OCR, email) est à construir
| Authz | `@adonisjs/bouncer` | Policies pour les permissions (V1 mono-user, prêt V2 multi-user) | | Authz | `@adonisjs/bouncer` | Policies pour les permissions (V1 mono-user, prêt V2 multi-user) |
| Validation | `@vinejs/vine` | Validateurs typés natifs Adonis, mappés sur les schemas Zod de `packages/shared` | | Validation | `@vinejs/vine` | Validateurs typés natifs Adonis, mappés sur les schemas Zod de `packages/shared` |
| Mail | `@adonisjs/mail` | Templates + provider switchable (Resend / Postmark / SES) | | Mail | `@adonisjs/mail` | Templates + provider switchable (Resend / Postmark / SES) |
| Queue | `@rlanz/bull-queue` (BullMQ) | Jobs différés (OCR, envoi email, check-ins) | | Queue | BullMQ direct + ioredis | Jobs différés (envoi relance, envoi confirmation, envoi remerciement paiement). Pas de wrapper Adonis. Code dans `app/services/queue.ts` |
| Cache / queue backend | Redis | Backend de BullMQ + cache des KPIs dashboard | | Cache / queue backend | Redis | Backend de BullMQ + cache des KPIs dashboard |
| Rate-limit | `@adonisjs/limiter` | 5 req/min sur `/auth/*`, 10/h sur upload | | Rate-limit | `@adonisjs/limiter` | 5 req/min sur `/auth/*`, 10/h sur upload |
| Tests | `@japa/runner` + `@japa/api-client` | Tests d'intégration HTTP | | Tests | `@japa/runner` + `@japa/api-client` | Tests d'intégration HTTP |
| Type-sharing front | `@tuyau/core` | Génère `.adonisjs/api.ts` consommé par le SPA | | Type-sharing front | `packages/shared` (workspace) | Zod schemas + types TS (statuts, tons, plans, billing) — consommé par `apps/web` et `apps/landing` |
| HTTP client | `@adonisjs/limiter` + `ky` (côté SPA) | — | | HTTP client | `@adonisjs/limiter` + `ky` (côté SPA) | — |
| Storage | `@adonisjs/drive` (S3 driver MinIO) | Abstraction stockage PDFs | | Storage | `@adonisjs/drive` (S3 driver MinIO) | Abstraction stockage PDFs |
### Dépendances déjà installées par le starter API ### Dépendances installées (cf. `apps/api/package.json`)
Voir `apps/api/package.json`. À ajouter pour V1 : Stack actuelle (V1 livrée) :
```bash ```jsonc
cd apps/api // apps/api/package.json (extrait)
pnpm add @adonisjs/bouncer @adonisjs/mail @adonisjs/limiter @adonisjs/drive "@adonisjs/auth", "@adonisjs/bouncer", "@adonisjs/mail",
pnpm add @rlanz/bull-queue bullmq ioredis "@adonisjs/limiter", "@adonisjs/drive", "@adonisjs/lucid",
pnpm add @aws-sdk/client-s3 # pour MinIO via le driver S3 d'@adonisjs/drive "bullmq", "ioredis", // queues directes, pas de wrapper Adonis
pnpm add resend # ou postmark / @aws-sdk/client-ses selon ADR-021 "@aws-sdk/client-s3", // MinIO via driver S3
node ace add @adonisjs/bouncer "resend", // email outbound
node ace add @adonisjs/mail --providers=resend "@mistralai/mistralai", // OCR
node ace add @adonisjs/limiter "stripe", // billing
node ace add @adonisjs/drive --services=s3 "@anthropic-ai/sdk", // génération IA blog
node ace add @rlanz/bull-queue "@sentry/node", // ADR-024
``` ```
Pas de BullMQ direct (sans wrapper Adonis), pas de `@tuyau/server` (le `@tuyau/core` historique n'est plus utilisé côté SPA).
--- ---
## 3. Repo layout (apps/api) ## 3. Repo layout (apps/api)
@ -185,9 +193,11 @@ apps/api/
│ │ ├── XXXX_create_import_drafts_table.ts │ │ ├── XXXX_create_import_drafts_table.ts
│ │ └── XXXX_create_activity_events_table.ts │ │ └── XXXX_create_activity_events_table.ts
│ ├── seeders/ │ ├── seeders/
│ │ ├── default_plans_seeder.ts # 4 plans pré-fournis (cf. seed.ts MSW) │ │ └── (pas de seeders Adonis : les 4 plans pré-fournis vivent dans
│ │ └── demo_data_seeder.ts # comptes démo (dev seulement) │ │ app/services/default_plans.ts et sont insérés à la création de
│ └── factories/ # Factories Lucid pour les tests │ │ chaque organisation. Le mode démo est servi par
│ │ app/services/demo_simulator.ts, pas par un seeder.)
│ └── factories.ts # Factories Lucid (mono-fichier, pas de dossier — V1)
├── tests/ ├── tests/
│ ├── functional/ # Tests HTTP via @japa/api-client │ ├── functional/ # Tests HTTP via @japa/api-client
│ │ ├── auth.spec.ts │ │ ├── auth.spec.ts
@ -693,12 +703,12 @@ const driveConfig = defineConfig({
services: { services: {
s3: services.s3({ s3: services.s3({
credentials: { credentials: {
accessKeyId: env.get('MINIO_ACCESS_KEY'), accessKeyId: env.get('S3_ACCESS_KEY'),
secretAccessKey: env.get('MINIO_SECRET_KEY'), secretAccessKey: env.get('S3_SECRET_KEY'),
}, },
endpoint: env.get('MINIO_ENDPOINT'), // http://lxc-minio:9000 endpoint: env.get('S3_ENDPOINT'), // http://minio.minio.svc.cluster.local:9000
region: 'fr-par', region: env.get('S3_REGION'),
bucket: 'rubis-invoices', bucket: env.get('S3_BUCKET'), // rubis-prod-invoices en prod
forcePathStyle: true, // requis pour MinIO forcePathStyle: true, // requis pour MinIO
visibility: 'private', visibility: 'private',
}), }),
@ -912,7 +922,7 @@ Inbound : les deux veulent le MX `@`. Plan probable : on garde OVH pour
### 13.1 Stack ### 13.1 Stack
`@rlanz/bull-queue` (Adonis 7 wrapper de BullMQ) + Redis comme backend. BullMQ direct (sans wrapper Adonis) (Adonis 7 wrapper de BullMQ) + Redis comme backend.
```ts ```ts
// config/queue.ts // config/queue.ts
@ -931,12 +941,17 @@ const queueConfig = defineConfig({
### 13.2 Jobs ### 13.2 Jobs
| Job | Trigger | Idempotent | Retry | Réalité V1 (cf. `apps/api/app/jobs/`) :
| Job (fichier) | Trigger | Idempotent | Retry |
|---|---|---|---| |---|---|---|---|
| `ProcessOcrJob` | POST /invoices/upload | oui (status=validated/skipped → no-op) | 3× exponential backoff | | `send_relance_job.ts` | RelanceTask.sendAt | oui (status=sent → no-op) | 5× |
| `SendRelanceJob` | RelanceTask.sendAt | oui (status=sent → no-op) | 5× | | `send_checkin_job.ts` | CheckinTask.sendAt | oui | 3× |
| `SendCheckinJob` | CheckinTask.sendAt | oui | 3× | | `send_payment_thanks_job.ts` | confirmation paiement (step 04 du flow) | oui | 3× |
| `RecomputeKpisJob` | nightly cron + post-mutation | oui | 1× |
**OCR** : pas de job dédié — l'extraction est synchrone via `app/services/import_batch.ts` (appel Mistral bloquant pendant l'upload). Si le volume monte, basculer en job différé.
**KPIs** : calculés on-the-fly à chaque GET dashboard/timeseries (cf. commentaire dans `start/routes.ts`). Pas de `RecomputeKpisJob` — pas de cache Redis pour les KPIs en V1.
### 13.3 Programmation des relances ### 13.3 Programmation des relances
@ -1111,11 +1126,11 @@ REDIS_HOST=lxc-redis.proxmox.local
REDIS_PORT=6379 REDIS_PORT=6379
# MinIO # MinIO
MINIO_ENDPOINT=http://lxc-minio.proxmox.local:9000 S3_ENDPOINT=http://minio.minio.svc.cluster.local:9000
MINIO_ACCESS_KEY=<secret> S3_ACCESS_KEY=<secret>
MINIO_SECRET_KEY=<secret> S3_SECRET_KEY=<secret>
MINIO_INVOICES_BUCKET=rubis-invoices S3_REGION=us-east-1
MINIO_BACKUPS_BUCKET=rubis-backups S3_BUCKET=rubis-prod-invoices # bucket réel en prod (cf. k3s/app/api.yml)
# Mail (Resend) # Mail (Resend)
MAIL_FROM_ADDRESS=relances@rubis.pro MAIL_FROM_ADDRESS=relances@rubis.pro

View File

@ -2,7 +2,7 @@
> Version : 0.1 · 2026-05-06 > Version : 0.1 · 2026-05-06
Tout ce qu'il faut pour faire tourner Rubis en local : services backing (Postgres, Redis, MinIO, Mailhog) + l'API + le SPA. Tout ce qu'il faut pour faire tourner Rubis en local : services backing (Postgres, Redis, MinIO, Mailpit) + l'API + le SPA.
## Prérequis ## Prérequis
@ -68,7 +68,7 @@ Si Docker n'est pas dispo, on peut basculer sur SQLite :
DB_CONNECTION=sqlite DB_CONNECTION=sqlite
``` ```
Les migrations tournent sur les deux. Pour les jobs/storage/mail, il faut quand même Redis/MinIO/Mailhog — donc Docker reste recommandé. Les migrations tournent sur les deux. Pour les jobs/storage/mail, il faut quand même Redis/MinIO/Mailpit — donc Docker reste recommandé.
## 3. Web (apps/web) ## 3. Web (apps/web)
@ -86,7 +86,7 @@ Voir `apps/api/.env.example` — c'est la source de vérité. Récap :
- **PG_*** : connexion Postgres (default ports décalés docker-compose) - **PG_*** : connexion Postgres (default ports décalés docker-compose)
- **REDIS_*** : BullMQ - **REDIS_*** : BullMQ
- **S3_*** : MinIO (driver S3 d'@adonisjs/drive) - **S3_*** : MinIO (driver S3 d'@adonisjs/drive)
- **MAIL_*** + **SMTP_*** : Mailhog en dev, Resend en prod (`MAIL_DRIVER=resend` + `RESEND_API_KEY`) - **MAIL_*** + **SMTP_*** : Mailpit en dev, Resend en prod (`MAIL_DRIVER=resend` + `RESEND_API_KEY`)
- **OCR_PROVIDER** : `mock` en dev, `mistral` quand l'API key est en place - **OCR_PROVIDER** : `mock` en dev, `mistral` quand l'API key est en place
- **ACCESS_TOKEN_TTL_MINUTES** / **REFRESH_TOKEN_TTL_DAYS** : durées d'auth - **ACCESS_TOKEN_TTL_MINUTES** / **REFRESH_TOKEN_TTL_DAYS** : durées d'auth

View File

@ -1,7 +1,12 @@
# Guide d'implémentation — Frontend # Guide d'implémentation — Frontend
> Version : 0.1 · Dernière maj : 2026-05-05 > Version : 0.2 · Dernière maj : 2026-05-09
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-017 (auth). > Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-017 (auth), ADR-024 (Sentry).
> ⚠️ **Note de cohérence (audit 2026-05-09)** : ce guide a été rédigé en phase de planification. Plusieurs choix ont évolué :
> - **Tuyau N'EST PAS utilisé** côté SPA. Le client API vit dans `apps/web/src/lib/api.ts` (fetch + ApiError custom). Le partage de types entre API et SPA passe par `packages/shared` (Zod + types TS), pas par un client typed-RPC. Les sections §6 et §7 ci-dessous, qui décrivent l'install et l'usage de Tuyau, sont **historiques** — gardées pour mémoire mais non applicables au code livré.
> - Les **tokens** de design vivent dans `packages/ui/src/styles/{tokens,base}.css`, pas inline dans `apps/web/src/styles/app.css` comme la doc l'écrit.
> - Les **polices** sont self-hosted via `@fontsource-variable/{bricolage-grotesque,inter}`, pas via `<link>` Google Fonts.
Ce document est le **guide pratique d'implémentation du SPA**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes exactes, snippets de config, conventions de dossier. Ce document est le **guide pratique d'implémentation du SPA**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes exactes, snippets de config, conventions de dossier.
@ -9,16 +14,16 @@ Ce document est le **guide pratique d'implémentation du SPA**. Il complète `ar
- `/CLAUDE.md` — contexte top-level - `/CLAUDE.md` — contexte top-level
- `/docs/produit.md` — flows utilisateur, IN/OUT V1 - `/docs/produit.md` — flows utilisateur, IN/OUT V1
- `/docs/marque.md` — palette, typo, voix, do/don't - `/docs/marque.md` — palette, typo, voix, do/don't
- `/docs/wireframes-mvp.html`les 13 écrans MVP avec annotations - `/docs/wireframes-mvp.html`wireframes low-fi initiaux (13 écrans, à jour partiellement)
- `/docs/tech/architecture.md` — vue d'ensemble du système - `/docs/tech/architecture.md` — vue d'ensemble du système
--- ---
## 1. Vue d'ensemble ## 1. Vue d'ensemble
L'app web (`apps/web/`) est un SPA React 19 buildé par Vite, qui consomme l'API AdonisJS `apps/api/` via un client HTTP type-safe (Tuyau). Le routing client est géré par **TanStack Router** (file-based, type-safe), le state serveur par **TanStack Query**, le styling par **Tailwind CSS v4** avec les tokens de marque issus de `marque.md`. L'app web (`apps/web/`) est un SPA React 19 buildé par Vite, qui consomme l'API AdonisJS `apps/api/` via un client `fetch()` minimaliste dans `apps/web/src/lib/api.ts`. Le routing client est géré par **TanStack Router** (file-based, type-safe), le state serveur par **TanStack Query**, le styling par **Tailwind CSS v4** avec les tokens de marque issus de `packages/ui` (consommé en workspace).
**Périmètre V1** : 13 écrans listés dans `wireframes-mvp.html`. Auth Bearer (cf. ADR-017) avec refresh token httpOnly cookie. Mobile responsive, pas d'app native. **Périmètre V1 livré** : ~15 routes `_app/` (dashboard, factures, clients, plans, paramètres, abonnement, insights, admin/blog) + onboarding 3 étapes + auth (login, signup, reset-password, callbacks SSO Google/Microsoft). Auth Bearer (cf. ADR-017) avec refresh token httpOnly cookie. Mobile responsive, pas d'app native.
**Hors scope V1** : SSR (pas nécessaire pour un SaaS B2B authentifié), i18n (FR uniquement), PWA offline (nice-to-have V2). **Hors scope V1** : SSR (pas nécessaire pour un SaaS B2B authentifié), i18n (FR uniquement), PWA offline (nice-to-have V2).
@ -163,16 +168,22 @@ body {
Importé dans `main.tsx` : `import './styles/app.css'`. Importé dans `main.tsx` : `import './styles/app.css'`.
### Polices Google Fonts ### Polices self-hosted via @fontsource-variable
`index.html` : Les fonts sont bundlées au build (pas de fetch Google Fonts au runtime → pas de FOUT, pas de fuite RGPD vers `fonts.googleapis.com`).
```html ```bash
<link rel="preconnect" href="https://fonts.googleapis.com"> pnpm add @fontsource-variable/bricolage-grotesque @fontsource-variable/inter
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
``` ```
```css
/* apps/web/src/styles/app.css (ou packages/ui/src/styles/base.css selon où tu importes) */
@import "@fontsource-variable/bricolage-grotesque";
@import "@fontsource-variable/inter";
```
**Bonus prod** : sur la landing Astro, on preload les woff2 latin critiques dans le `<head>` pour casser la chaîne HTML→CSS→fonts du critical path (cf. `apps/landing/src/layouts/Layout.astro`).
### Usage typique ### Usage typique
```tsx ```tsx
@ -207,27 +218,31 @@ Référence : les 13 écrans dans `wireframes-mvp.html`.
``` ```
apps/web/src/routes/ apps/web/src/routes/
├── __root.tsx # Layout global, providers, AuthGate ├── __root.tsx # Layout global, providers, AuthGate
├── login.tsx # 1.2 Connexion ├── login.tsx, signup.tsx, reset-password.tsx, accept-invite.tsx
├── signup.tsx # 1.1 Inscription ├── auth/ # callbacks SSO Google + Microsoft
├── _onboarding/ # Layout onboarding (sans sidebar) ├── onboarding.tsx # layout onboarding (segment URL /onboarding)
│ ├── _onboarding.tsx ├── onboarding/ # 3 étapes inscription post-signup
│ ├── compte.tsx # 1.3 step 1 │ ├── compte.tsx
│ ├── entreprise.tsx # 1.3 step 2 │ ├── entreprise.tsx
│ └── signature.tsx # 1.3 step 3 │ ├── signature.tsx
└── _app/ # Layout app authentifiée │ └── index.tsx
├── _app.tsx # Layout : sidebar + topbar + tab bar mobile └── _app/ # Layout app authentifiée (group route, pas de segment)
├── index.tsx # 4.1 Dashboard ├── _app.tsx # sidebar + topbar + tab bar mobile
├── factures.tsx # 2.4 Liste filtrable ├── index.tsx # Dashboard (compteur rubis + KPIs)
├── factures.$id.tsx # 4.2 Détail facture (timeline) ├── factures.tsx # Liste filtrable
├── factures.import.$batchId.tsx # 2.2 Vérification OCR ├── factures_.$id.tsx # Détail facture + timeline relances
├── plans.tsx # 3.1 Bibliothèque ├── factures_.import.tsx # Drag-and-drop OCR (étape 1)
├── plans.$slug.tsx # 3.2 Éditeur (cadence + templates) ├── factures_.import_.$batchId.tsx # Vérification OCR (étape 2)
├── clients.tsx # liste clients ├── clients.tsx, clients_.$id.tsx
└── parametres.tsx # paramètres compte ├── plans.tsx, plans_.$slug.tsx, plans_.nouveau.tsx
├── parametres.tsx, parametres_.abonnement.tsx
├── insights.tsx # KPIs avancés (timeseries)
├── admin.blog.tsx # liste posts (admin only)
└── admin.blog_.$id.tsx # éditeur post + publish workflow
``` ```
Les routes commençant par `_` sont des **layout routes** (n'ajoutent pas de segment URL). Les routes en `_app/*` sont sous **layout group route** (pas de segment URL ajouté). Le suffixe `_` dans `factures_.$id` empêche l'imbrication visuelle dans `factures.tsx` (TanStack Router file-based convention).
### Configuration root ### Configuration root
@ -405,7 +420,9 @@ const markPaidMutation = useMutation({
--- ---
## 6. Tuyau — client HTTP typé pour AdonisJS ## 6. Tuyau — *historique, non utilisé en V1*
> ⚠️ Cette section décrit un choix initial qui n'a pas été retenu côté SPA livré. Le client HTTP réel est un `fetch()` minimaliste dans `apps/web/src/lib/api.ts`, et le partage de types entre l'API et le SPA passe par `packages/shared` (Zod schemas + types TS exportés en workspace). Section gardée pour mémoire et pour un éventuel pivot futur si la surface API grossit suffisamment.
[Tuyau](https://github.com/Julien-R44/tuyau) est l'équivalent tRPC pour AdonisJS. Il génère un client TS qui connaît toutes les routes API, leurs payloads, et leurs réponses — depuis le code Adonis lui-même. [Tuyau](https://github.com/Julien-R44/tuyau) est l'équivalent tRPC pour AdonisJS. Il génère un client TS qui connaît toutes les routes API, leurs payloads, et leurs réponses — depuis le code Adonis lui-même.
@ -676,14 +693,17 @@ VITE_API_URL=http://localhost:3333
VITE_PUBLIC_LANDING_URL=https://rubis.pro VITE_PUBLIC_LANDING_URL=https://rubis.pro
``` ```
Production via secret K3s injecté dans le build Vite : Production : le SPA appelle son API via le **même origin** (`/api/v1/*`), proxifié par le nginx du pod `rubis-web` vers `rubis-api:3333` côté K3s. Pas de hostname `api.rubis.pro`. Le `VITE_API_URL` peut donc être laissé vide ou `'/api/v1'` en prod.
```bash ```bash
VITE_API_URL=https://api.rubis.pro VITE_API_URL= # vide → fetch en relatif
VITE_PUBLIC_LANDING_URL=https://rubis.pro VITE_PUBLIC_LANDING_URL=https://rubis.pro
VITE_USE_MOCKS=false
VITE_SENTRY_DSN_WEB=<dsn> # ADR-024
VITE_APP_VERSION=$GIT_SHA # tag image / sha commit
``` ```
Toutes les vars accessibles côté SPA **doivent être préfixées `VITE_`** (sinon Vite ne les expose pas au bundle). Toutes les vars accessibles côté SPA **doivent être préfixées `VITE_`** (sinon Vite ne les expose pas au bundle). Liste complète : voir `apps/web/src/lib/env.ts`.
--- ---

View File

@ -415,7 +415,7 @@
<header class="page-header"> <header class="page-header">
<h1>Rubis Sur l'Ongle — Wireframes MVP <span style="color:var(--ink-3); font-weight: 400;">v0.1</span></h1> <h1>Rubis Sur l'Ongle — Wireframes MVP <span style="color:var(--ink-3); font-weight: 400;">v0.1</span></h1>
<div class="meta">Low-fidelity · niveaux de gris · 13 écrans · centré sur la promesse "3 clics maximum"</div> <div class="meta">Low-fidelity · niveaux de gris · 13 écrans low-fi initiaux (la SPA livrée en compte ~15 + onboarding multi-étapes — cf. <code>apps/web/src/routes/_app/</code>) · centré sur la promesse "3 clics maximum"</div>
<nav class="toc"> <nav class="toc">
<a href="#onboarding">1. Onboarding &amp; Auth</a> <a href="#onboarding">1. Onboarding &amp; Auth</a>
<a href="#upload">2. Upload &amp; OCR</a> <a href="#upload">2. Upload &amp; OCR</a>