Comment c’est construit
Le trajet d’un document dans Clivio, et quelques choix techniques dont je suis assez content.
- Code applicatif
- 294 439 lignes de TypeScript dans 1 948 fichiers, tests non compris
- Tests
- Environ 7 600 tests unitaires, 258 tests des règles de sécurité, et chaque nuit un parcours complet, de l’inscription au résumé IA
- Surface
- 103 pages, 254 server actions, 57 routes d’API, 26 Cloud Functions
- Langues
- Français et anglais, environ 13 400 textes traduits dans chaque langue
- Décisions
- 41 décisions d’architecture consignées, toutes écrites en octobre 2026
Compté dans le dépôt le 19 octobre 2026.
Vue d’ensemble
Une app Next.js sur Firebase App Hosting, avec Google Cloud derrière et Gemini pour la lecture. Pas de backend à part à maintenir.
Le trajet d’un document
Six étapes entre l’arrivée d’un fichier et le moment où vous pouvez lui poser des questions. Cliquez sur une étape, ou suivez-les dans l’ordre.
Comment les documents arrivent
Un document peut arriver par envoi direct, par e-mail à l’adresse @inbox.clivio.app de l’utilisateur, par WhatsApp, via une page de dépôt publique, via le portail client, depuis le menu Partager d’Android ou une extension de navigateur, ou par l’API REST et le serveur MCP. Quel que soit le canal, le fichier est stocké et sa fiche créée en base avant tout travail d’IA, avec le quota de stockage réservé dans la même transaction. Même en rechargeant la page, on ne peut pas perdre un document.
Dans la file d’attente
Chaque document devient une tâche Cloud Tasks : trois tentatives, trente secondes entre deux essais, dix en même temps. Le worker appelle la route de traitement avec un jeton OIDC Google, et la route vérifie l’émetteur, l’audience et le compte de service avant de réserver le document dans une transaction.
La lecture du document
Les fichiers Office, les tableurs et le texte sont extraits en local, derrière une protection contre les bombes zip. Les PDF et les images partent chez Gemini 2.5 Flash, qui sait les lire directement. Les petits fichiers reviennent en JSON conforme à un schéma : 34 types de documents et, pour les factures, le HT, la TVA, le TTC, les numéros d’entreprise et les lignes. Au-delà de 5 Mo ou 20 pages, les fichiers passent par l’API File de Gemini, dans un mode texte plus léger.
Contrôler le travail du modèle
Quand le PDF contient des données de facture structurées (Factur-X, ZUGFeRD ou UBL), elles l’emportent sur ce que le modèle a lu. Ensuite, une couche déterministe corrige les montants au format français et les centimes des fiches de paie, met les avoirs en négatif, nomme le document selon le modèle choisi par l’utilisateur, et envoie tout ce qui a un montant dans la boîte de réception finance.
L’indexation
Le texte est découpé en morceaux d’environ 1 200 caractères qui se chevauchent sur 150, transformés en vecteurs avec gemini-embedding-2 (768 dimensions), puis stockés dans Firestore. L’indexation est idempotente : un morceau dont le SHA-256 n’a pas changé n’est pas recalculé.
Répondre aux questions
La recherche fait une passe littérale et une passe vectorielle, puis les fusionne par Reciprocal Rank Fusion. L’assistant garde huit passages, pas plus de trois par document, et doit les citer avec des repères numérotés, que l’interface transforme en surlignages dans le PDF. Les questions de totaux, de comptes, d’échéances et de listes ne passent même pas par le modèle, et la réponse s’affiche au fil de l’eau.
- lib/uploads/ingestDocument.ts
- app/api/webhooks/sendgrid/route.ts
- app/api/webhooks/whatsapp/
- app/api/v1/documents
- app/api/mcp/route.ts
Les e-mails entrants sont vérifiés par un jeton partagé, limités en débit par expéditeur et par destinataire, dédoublonnés par empreinte, et chaque pièce jointe est validée par ses octets magiques.
- lib/aiTasks.ts
- functions/src/aiQueue.ts
- app/api/tasks/process-document/route.ts
- functions/src/aiWatchdog.ts
Si la file ne répond pas, le traitement se fait directement. Un watchdog repasse toutes les quinze minutes pour relancer ce qui serait resté coincé.
- app/actions/ai/helpers.ts
- lib/fileExtractors.ts
- lib/ai/thinking.ts
Si le JSON ne passe pas, l’appel est relancé en mode texte au lieu d’échouer.
- lib/facturx.ts
- lib/processing/postExtract.ts
- lib/documentNaming.ts
En un sprint, le parseur Factur-X est passé de 5 fichiers de test réussis sur 16 à 16 sur 16.
- lib/rag/chunking.ts
- lib/rag/embeddings.ts
- lib/rag/indexing.ts
- lib/rag/retrieval.ts
Quand Google a retiré text-embedding-004, la migration vers gemini-embedding-2 a été documentée directement dans le code.
- app/api/search/hybrid/route.ts
- lib/assistant/prompt.ts
- lib/search/aggregation.ts
- app/actions/ai/unified.ts
Si l’index vectoriel manque, la recherche se rabat sur la correspondance littérale au lieu de planter.
Ce que j’ai bien fait
Quand tout marche, on ne voit rien de tout ça. C’est un peu le principe.
Des numéros de facture sans trou
Une facture émise reçoit son numéro légal, F-2026-0001, dans la transaction même qui l’émet : pas de trou, pas de doublon. Ensuite, elle est figée : seul un avoir peut l’annuler, et un PDF perdu est régénéré à l’octet près.
lib/invoicing/numbering.ts, lib/invoicing/facturx/
Un chiffre, un seul calcul
Dans Finance, chaque chiffre de chaque écran sort d’un seul module de calcul. Un document donne une transaction, les avoirs viennent en déduction, et un test vérifie qu’un même mois donne les mêmes chiffres partout. J’y suis arrivé en auditant le module comme j’audite les données d’un client.
lib/finance/aggregate.ts, lib/documents/payment.ts
Pas de double facturation
La file peut livrer deux fois la même tâche. Chaque traitement reçoit donc une clé, calculée à partir du traitement et d’un SHA-256 du fichier, et enregistrée dans la même transaction que le débit des crédits. Si une tâche revient, personne ne paie deux fois et le modèle n’est pas rappelé.
lib/ai/documentRun.ts
Les tâches bloquées sont reprises
Les workers sont appelés avec un jeton OIDC Google et réservent les documents de façon atomique. Si la file ne répond pas, le traitement se fait directement, et un watchdog repasse toutes les quinze minutes pour relancer ce qui est coincé ou n’a jamais démarré.
functions/src/aiWatchdog.ts
Les données de la facture font foi
Quand un PDF contient des données Factur-X, ZUGFeRD ou UBL, ces valeurs l’emportent sur ce que le modèle a lu. Une couche déterministe corrige ensuite les montants au format français et le signe des avoirs.
lib/facturx.ts, lib/processing/postExtract.ts
Les totaux viennent de la base
Une question comme « combien ai-je dépensé en hébergement le mois dernier ? » ne passe jamais par le modèle. Totaux, comptes, échéances et listes sont calculés directement dans la base, avant tout appel au modèle : ils ne coûtent aucune action IA, et personne ne peut les inventer.
lib/search/aggregation.ts
Des citations vérifiables
L’assistant doit citer des passages numérotés. Chaque repère est relié à son morceau de texte, ceux hors limites sont écartés, et les bons ouvrent le PDF au passage surligné.
lib/assistant/prompt.ts
Des limites pour l’agent
L’agent de planification s’arrête au bout de huit étapes et trois crédits. Tout ce qui sort de l’app, comme l’envoi d’un e-mail, attend d’abord le feu vert de l’utilisateur. Les automatisations suivent la même règle : une action qui envoie ou supprime quelque chose attend dans « À valider », sept jours au plus.
lib/agent/
Les envois sont contrôlés
Les types de fichiers sont vérifiés par leurs octets magiques, les archives contre les bombes zip, les URL sortantes contre le SSRF et le DNS rebinding. Les partages caviardés sont rastérisés, pour que le texte masqué ne puisse pas être recopié.
lib/fileExtractors.ts, lib/redactedPdf.ts
Garder les coûts d’IA sous contrôle
Les gros fichiers passent par l’API File de Gemini plutôt qu’en base64, les conversations sur un document utilisent le cache de contexte, et chaque tâche a son budget de réflexion : 1 024 jetons pour extraire, zéro pour classer.
lib/ai/thinking.ts, lib/ai/pricing.ts
Sécurité
Ce qui est en place, et ce que je ne promets pas.
- Les règles de la base et du stockage refusent tout par défaut ; chaque utilisateur n’accède qu’à ses propres données.
- Les clés d’API sont stockées sous forme d’empreintes HMAC-SHA-256 et comparées en temps constant ; scopes, limites de dossiers et expiration sur chaque clé.
- Jetons OAuth et coordonnées bancaires chiffrés en AES-256-GCM, liés à leur propriétaire.
- Double authentification, clés d’accès vérifiées par le serveur, sessions vérifiées contre la révocation, App Check.
- Fichiers partagés servis par des liens signés générés à chaque requête, valables une heure.
- Un audit de sécurité complet en juillet 2026, avec six corrections, puis un audit de production en 146 points en octobre.
Les fonctions serveur tournent dans la région us-central1 de Google Cloud, et les appels d’IA passent encore par l’API Gemini Developer, hors de l’UE. Depuis octobre, le code sait les faire passer par Vertex AI dans une région européenne, avec un script qui vérifie la région avant la bascule et un rapport d’administration qui montre où l’IA tourne vraiment. La bascule n’est pas faite : Clivio ne promet donc pas un traitement 100 % européen. Pas d’engagement de disponibilité publié non plus, pour l’instant.
API et MCP
Clivio a une API REST publique et un serveur MCP, qui reposent sur le même code. Avec une clé, un script ou un agent IA peut chercher, lire et ajouter des documents, leur poser des questions, et lire ou remplir leurs champs. L’extension de navigateur passe aussi par elle, avec une clé qui ne peut qu’ajouter des documents : volée, elle ne permet de rien lire.
{
"mcpServers": {
"clivio": {
"type": "http",
"url": "https://dashboard.clivio.app/api/mcp",
"headers": { "Authorization": "Bearer clv_live_YOUR_KEY" }
}
}
}Hermes, Claude Desktop, Cursor ou n’importe quel client MCP. Extrait de docs/mcp.md.
search_documentssearch:readlist_documentsdocuments:readget_documentdocuments:readcreate_documentdocuments:writeask_documentsdocuments:readlist_field_templatesdocuments:readget_document_fieldsdocuments:readextract_document_fieldsdocuments:writeUne clé ne voit que les outils autorisés par ses scopes. Soixante requêtes par minute et par clé ; les clés expirent au bout de 90 jours par défaut. Mon propre assistant, Hermes, s’en sert justement pour ranger ce qu’on m’envoie.