Aller au contenu principal
Références

Rappelio

Un CRM écrit pour un client, que nous avons reconstruit, sécurisé et mis sur le marché comme un produit.

  • Web application
  • Site vitrine
Rappelio sur ordinateur portable et smartphone : la liste des prospects, entourée des étapes d'un contact, carte de visite scannée, prospect créé, rappel programmé.

Au départ, un CRM en JavaScript sans framework, fait pour un seul client. À l'arrivée, un SaaS avec ses abonnements, son IA et son site.

L'outil marchait, mais il n'était pas fait pour grandir : données fragiles, interface de bureau, aucun test. Porte 7 a mené le chantier de bout en bout, sans jamais couper l'accès au client d'origine. Voici ce que nous avons fait, dans l'ordre.

  • Refonte React
  • Sécurité et tests
  • Assistant IA
  • Abonnements Stripe
  • RGPD dans le code
  • Marque et site vitrine
  • Hébergement et CI/CD

Stabiliser avant de toucher à l'interface.

Refaire un front sur des données qui se perdent, c'est déplacer le problème. On a d'abord réparé le socle.

Une synchronisation qui ne perd plus rien
Les contacts vivaient en double, dans le navigateur et dans Strapi. On a écrit une fusion qui garde toujours la version la plus récente, et paginé les appels.
Un stockage local chiffré
Passage de localStorage à IndexedDB, avec une clé de chiffrement dérivée d'un secret serveur plutôt que stockée à côté des données.
Un filet de tests avant la refonte
Quatre-vingt-dix tests Vitest sur les fonctions critiques, posés avant de réécrire quoi que ce soit. Il y en a 145 aujourd'hui, facturation comprise.

Réécrire en React sans jamais couper le service.

React 19, Vite, TypeScript et TanStack Query. Pour que le client continue de travailler pendant la refonte, le serveur sait servir l'ancien front ou le nouveau : une seule variable d'environnement fait la bascule, dans un sens comme dans l'autre.

front/src/server/server.js

const useReactFrontend = process.env.REACT_FRONTEND === 'true';
const clientDir = useReactFrontend
  ? path.join(__dirname, '../../dist') // React build (Vite)
  : path.join(__dirname, '../client'); // Vanilla JS (legacy)
  1. Écran par écran, jusqu'à la parité complète
  2. Recette sur l'ancien et le nouveau côte à côte
  3. Bascule en production, l'ancien front gardé en secours

L'app réécrite, filmée sur une instance locale avec des données fictives.

Penser le terrain parce qu'un contact se prend debout.

Un indépendant rencontre ses prospects sur un salon, pas derrière un bureau. On a repris l'interface pour le téléphone.

L'import multi-format : Excel, CSV ou fiche vCard, colonnes reconnues par leur en-tête.

  • Des cartes au lieu d'un tableau

    Un audit responsive ligne par ligne, puis des cartes contacts pour le mobile : zones tactiles élargies, barre d'outils collante, statut lisible d'un coup d'œil.

  • Une vraie app, sans store

    Rappelio s'installe depuis le navigateur. Une fois installé, il apparaît dans le menu Partager des Contacts du téléphone : un contact s'importe en deux gestes.

  • Un premier import qui guide

    Un compte vide, c'est un utilisateur perdu. L'écran d'accueil explique comment exporter ses contacts, avec les étapes propres à iPhone ou à Android.

Encadrer l'IA pour qu'elle propose sans décider.

Un assistant qui modifie un fichier client tout seul, personne n'en veut. Nous lui avons donné huit outils pour lire et seize pour proposer. Chaque proposition revient en carte Confirmer ou Annuler, et seul ce clic déclenche l'écriture.

Ce qu'il peut lire

  • search_contacts
  • get_contact_details
  • list_reminders
  • list_contacts_by_status
  • list_contacts_by_tag
  • list_stale_contacts
  • list_recent_contacts
  • pipeline_stats

Ce qu'il peut seulement proposer

  • propose_create_contact
  • propose_create_contacts_batch
  • propose_update_contact_field
  • propose_update_status
  • propose_convert_to_client
  • propose_delete_contact
  • propose_add_tag
  • propose_remove_tag
  • propose_add_tag_batch
  • propose_create_reminder
  • propose_reschedule_reminder
  • propose_mark_reminder_done
  • propose_delete_reminder
  • propose_reschedule_reminders_batch
  • propose_add_history_entry
  • propose_update_next_action
La voix, de bout en bout
Dictée par Whisper, réponse lue à voix haute, et micro qui s'arrête seul quand on se tait.
Des quotas tenus par le serveur
L'assistant et les scans de cartes sont bloqués côté serveur selon l'offre, pas seulement masqués dans l'interface.
Un ton écrit
Son prompt fixe sa personnalité : il tutoie, fait court, et n'invente jamais un identifiant de contact.

Monter le socle qu'un SaaS ne peut pas oublier.

Vendre un abonnement, c'est aussi encaisser, vérifier, piloter et mettre à jour sans casser. Rien de visible pour l'utilisateur, tout pour que ça tienne.

  • Paiement Stripe

    Checkout, portail client, et des webhooks idempotents enregistrés en base : un redémarrage ne facture jamais deux fois.

  • Inscription vérifiée

    Double validation par e-mail et piège à robots sur le formulaire, sans captcha pour l'utilisateur.

  • Un panneau super admin

    Utilisateurs, offres, quotas et revenu mensuel. On peut offrir des crédits IA à un client en un clic.

  • Des mises à jour invisibles

    Pendant un redéploiement, le service worker affiche une page de maintenance au lieu d'une erreur.

  • Une app, deux adresses

    Le client d'origine garde easy-crm.agencerjs.com, le produit vit sur app.rappelio.com. Un seul code, un seul déploiement.

S'auditer avant que quelqu'un d'autre ne le fasse.

Une fois l'app en production, on a passé tout le code au crible : serveur, front et back. Une centaine de constats, quinze jugés bloquants, puis les corrections.

Quelques corrections issues de l'audit
PointAvantAprès
Journal d'audit RGPDgardé en mémoire, perdu à chaque redémarrageenregistré dans Strapi
Webhooks Stripeidempotence en mémoire, risque de double abonnementévénements tracés en base
Requêtes vers Strapirelations chargées à la demande du clientliste blanche stricte
Politique de sécuritéscripts inline autorisés, héritage de l'ancien frontunsafe-inline retiré
Routes de debugaccessibles sans authentificationsupprimées

Le RGPD suit la même logique : les durées de conservation sont écrites dans le code et une tâche quotidienne les applique. Contacts supprimés purgés à 30 jours, comptes inactifs prévenus à 12 mois puis supprimés à 24.

Le mettre sur le marché avec un nom, un site et une mise en ligne sûre.

Un nom et une marque
Le projet s'appelait Flow'UP. On l'a rebaptisé Rappelio, dessiné la fusée et renommé l'app, la PWA et les e-mails.
Un site qui ne ment pas
Les maquettes du site vitrine sont redessinées à partir du vrai code de l'app, et les tarifs se synchronisent depuis l'app : une seule source de vérité.
Une mise en production sûre
Images Docker construites en CI, déploiement avec retour arrière automatique, sauvegarde quotidienne de la base et procédure de restauration écrite.
Accueil de rappelio.com : le titre « Relancez. Au bon moment. À chaque fois. », le bouton Commencer gratuitement et l'aperçu d'une liste de rappels.

Le site vitrine, en Next.js statique : nous avons écrit sa promesse, « Relancez. Au bon moment. À chaque fois. »

rappelio.com sur smartphone : le titre, les deux boutons et l'aperçu des rappels empilés.