Leads : création, édition, suppression (unitaire et en masse), filtres,
pagination, import de fichiers (voir Import avancé), assignation à un
employé.
Entreprises : organisations rattachées aux leads (companies,
leads.companyId). Rattachement : par numéro d'immatriculation, par
numéro de TVA (données d'entreprise explicites), puis par domaine de
l'e-mail (x@acme.fr → entreprise existante de domaine acme.fr ; un
lead n'a pas forcément d'entreprise, aucune n'est créée à partir d'un
e-mail ; les messageries grand public sont exclues). Dans le formulaire de
lead, la correspondance par domaine est proposée à l'enregistrement
(« Rattacher ce lead à cette entreprise ? » Oui / Non) et jamais appliquée
sans réponse ; seul l'import CSV rattache par domaine automatiquement. Le
numéro d'immatriculation dépend du pays de l'entreprise
(SIRET vérifié dans la base Sirene pour la France, texte libre ailleurs) via
un registre d'inputs par pays extensible (src/lib/countryInputs).
Adresses par pays : l'adresse (objet imbriqué partagé par leads,
entreprises et employés) est pilotée par son country (code ISO) : ordre des
champs, libellés (État / province / préfecture, code ZIP…), champs requis,
format du code postal et liste des régions viennent des métadonnées
libaddressinput de Google (convex/_lib/validators/addressFormats.generated.ts,
régénérées par bun run scripts/generate-address-formats.ts). Même saisie
pour tous les pays : recherche d'adresse en haut (BAN pour la France, Photon
/ OpenStreetMap ou Google Places ailleurs, selon le pays), champs imbriqués
du pays en dessous. Le numéro de TVA des entreprises est
validé par pays (format + clé via jsvat) et vérifié en direct dans VIES
pour l'UE.
Transactions et pipelines : transactions (montant, devise, date de
clôture, propriétaire, lead, campagne d'origine) dans des
pipelines configurables (Paramètres → Pipelines : stades ordonnés,
stades gagné/perdu, plusieurs pipelines). Les transitions autorisées
entre stades forment un graphe (pipelines.transitions) : par défaut
(liste absente) chaque stade mène au suivant et peut revenir au précédent,
le dernier stade en cours ↔ gagné / perdu (defaultTransitions). La fiche
d'un pipeline en montre un aperçu ; « Modifier » ouvre un éditeur plein
écran (infos et stades à gauche, graphe @xyflow/react à droite) : un nœud
par stade, une flèche par transition dans les deux sens, « × » sur une
flèche pour l'interdire, glisser un stade sur un autre pour en ajouter une,
« Tout autoriser » (graphe complet) et « Transitions linéaires » (retour au
défaut) ; stades et flèches se déplacent à la souris et la disposition est
enregistrée avec le pipeline (pipelines.layout), l'aperçu la reprend telle
quelle ; les stades gagné/perdu sont des puits sauf flèches de
réouverture. Un validateur pur (analyzePipelineGraph) tourne à chaque
modification, côté éditeur et côté serveur : stade inaccessible et impasse
(aucun chemin vers gagné/perdu) sont signalés en avertissement. Chaque
déplacement passe par moveDealToStage, qui refuse une flèche absente
(deal_transition_forbidden) ; le Kanban grise les colonnes inaccessibles
pendant le glisser, le stepper désactive les stades interdits, et
l'activation d'un workflow vérifie l'étape « Changer le stade » quand le
déclencheur fixe le stade de départ. Vue Kanban
(glisser-déposer) et vue liste ; historique des stades (dealStageHistory) ;
compteurs et montants par stade via agrégats. Chaque stade peut porter des
étiquettes (stages[].tags, clés stables ; sur « Perdue » ce sont les
motifs de perte) : entrer dans un stade étiqueté propose les étiquettes (au
moins une exigée si le stade l'impose, tagsRequired) et un commentaire, stockés sur la transaction (stageTags, stageComment) et dans
la ligne d'historique, affichés sur la fiche, la carte Kanban et le fil du
lead, filtrables (« Étiquettes de stade ») ; l'étape de workflow « Changer le
stade » peut poser des étiquettes fixes. Déclencheurs de workflow
deal_created / deal_stage_changed / deal_won / deal_lost et étapes
« Créer une transaction » / « Changer le stade d'une transaction ». Une transaction
gagnée fait passer son lead au statut « Client ».
Tâches et activités : appels, réunions, tâches, e-mails, notes
(activities) rattachés à un lead, une entreprise ou une transaction, avec
échéance, propriétaire et résultat. Vue « Mes tâches » par période (en
retard / aujourd'hui / cette semaine / plus tard / sans date, compteurs par
agrégat), compte-rendu rapide d'un appel depuis les fiches (avec rappel
optionnel), étape de workflow « Créer une tâche ». Les rappels à l'échéance
arrivent avec le système de notifications (lot 5) ; dueAt en est le point
d'accroche.
Équipes, rôles et accès : chaque lead, entreprise et transaction a
zéro, un ou plusieurs propriétaires (ownerIds, le premier est le
propriétaire principal, espace de noms des agrégats par propriétaire). Les
collaborateurs peuvent appartenir à plusieurs équipes (teams, Paramètres
→ Équipe) ; une tâche peut être confiée à une personne et/ou à une équipe
(toute l'équipe la voit ; sans l'un ni l'autre, tout le monde). Les rôles
sont des données (roles, Paramètres → Rôles et accès) : une grille
rôles × modules dont chaque case vaut none / own / team / all, plus
un interrupteur « Paramètres ». Valeurs par défaut : admin tout, manager
son équipe, member ses fiches — « Mes fiches » et « Mon équipe » incluent
les fiches sans propriétaire (le pool). Rôles personnalisés (créer, renommer,
supprimer avec réaffectation) ; admin est verrouillé et un rôle ne peut pas
se retirer « Paramètres ». Le périmètre est appliqué dans les wrappers
_lib/auth.ts (row-level security convex-helpers sur toutes les tables de
chaque module, enfants compris — lecture et écriture), jamais par requête ;
les compteurs par agrégat se restreignent via ctx.visibility, les
destinataires d'une campagne sont résolus dans le périmètre de son auteur.
Fichiers joints : devis, scans, contrats… déposés (glisser-déposer) sur
une fiche lead, entreprise ou transaction (attachments), rangés dans une
arborescence de dossiers par fiche, aperçu des images et PDF, téléchargement.
La suppression n'est pas définitive : le fichier passe dans la corbeille de la fiche
(deletedAt / deletedBy / purgeAt, clé et blob inchangés), d'où il se
restaure tel quel ou s'efface définitivement ; la suppression planifie sa
propre purge (ctx.scheduler.runAt(purgeAt), première rétention de
l'application), qui n'efface ligne et blob que si le fichier est toujours
dans la corbeille pour cette même date — une restauration puis une nouvelle
suppression laissent l'ancienne tâche sans effet. Taille maximale et durée
de rétention (30 jours par
défaut, 1 à 365) configurables dans Paramètres → Fichiers
(appConfig.attachments.maxSizeBytes / retentionDays), la taille étant
appliquée côté serveur à la demande d'URL d'envoi puis sur le blob stocké. Les octets
vivent dans Convex Storage derrière l'interface FileStore
(convex/lib/attachments/storage.ts) ; chaque ligne porte déjà la clé
type/identifiant/dossier/nom d'un stockage objet, pour migrer vers S3 en
copiant les blobs clé par clé.
Conservation des données : une purge nocturne (convex/crons.ts, 03:30 UTC,
features/retention) efface ce qui dépasse les durées configurées dans
Paramètres → Conservation (appConfig.retention) : les fiches supprimées
(leads, entreprises, transactions, activités, formulaires ; 30 jours par défaut, 1 à 365)
avec tout ce qui s'y rattache (notes, envois et événements de campagne, liens
suivis, enrôlements et étapes de workflow, historique de statut, soumissions
de formulaires, appartenances aux listes, paires de doublons, fichiers joints
et leurs blobs ; une transaction
ou une activité encore vivante perd seulement son lien), les événements de
campagne, le journal des étapes de workflow et les liens suivis des campagnes
terminées (365 jours par défaut, 30 à 3650), le journal d'audit (730 jours par
défaut, 90 à 3650), ainsi que les invitations expirées et les clés
d'idempotence de l'API périmées. Les compteurs des pages hébergées
(landingPageStats, huit lignes par page et par jour, aucune donnée
personnelle) ne sont pas purgés. La purge travaille par lots bornés (un budget de 2 000 écritures par lot, 20
fiches, 500 lignes par table, 200 lignes rattachées par fiche et par lot,
chaque requête lue directement dans une plage d'index) et s'enchaîne jusqu'à
épuisement, la politique et l'instant de référence étant figés au premier lot ; une fiche aux centaines de lignes rattachées
est vidée sur plusieurs lots avant de disparaître. Chaque exécution laisse une
ligne d'audit (retention / purge) avec ses compteurs, affichée sur la page,
et relance le recalcul complet des listes dynamiques par sécurité. Une
extension peut différer la purge (beforeScheduledWork, retention_purge).
Pages : des pages hébergées par le déploiement (GET /p/<slug>,
servies sous une politique de sécurité stricte, écrite dans le document
lui-même pour que l'aperçu de l'éditeur se comporte comme la page, limitées
en débit par adresse et en vues comptées pour tout le déploiement),
composées de blocs
(bandeau, texte, image, formulaire, appel à l'action) à partir d'un modèle,
avec titre et description pour les moteurs de recherche ; brouillon ou
publiée (seule une page publiée répond, un brouillon est introuvable),
aperçu avant publication, adresse unique parmi les pages vivantes. Le
formulaire embarqué dit de quelle page il vient : chaque envoi lui est
attribué. Les vues sont comptées côté serveur, sans cookie (les robots
exclus), par jour (landingPageStats, huit lignes par jour pour qu'une
affluence ne se dispute pas un document) ; la page montre vues, envois et
conversion sur trente jours. Test A/B : une version B de la page (ses
propres blocs) montrée à une part des visiteurs (1 à 99 %), toujours la
même pour un visiteur donné (sa place parmi cent, tirée de son adresse et
de son navigateur, rien de stocké) ; un envoi est attribué à la version que
ce tirage, refait côté serveur, lui montre, jamais à ce que dit le
navigateur. Chaque version compte ses vues et ses envois pour ce test
seulement (changer B ou la part commence un nouveau test, à zéro), publier
vérifie B comme A, et l'on garde la gagnante, dont les blocs deviennent
ceux de la page. Quand le suivi web est activé, la page charge son script,
et l'origine du déploiement compte comme site suivi.
Formulaires de capture (Paramètres → Formulaires) : formulaires publics
composés de champs standard ou de propriétés personnalisées, à intégrer par
un script (GET /forms/<id>/embed.js, l'élément cible par data-target,
indispensable avec un gestionnaire de balises) ou en iframe
(GET /forms/<id>, servie avec une CSP). Chaque champ a une clé publique
stable (slug de son libellé) ; les valeurs soumises sont validées côté
serveur avec les validateurs des propriétés (téléphone compris) et tronquées.
Ce qu'une soumission peut écrire, l'adresse n'étant la preuve de rien :
un e-mail inconnu (ou absent) crée un contact avec le consentement e-mail
coché (simple opt-in : la case cochée vaut consentement, sur ce contact
seulement, avec ses événements de consentement : le canal et les abonnements
que le formulaire nomme, source form, hash de l'adresse) ; un contact vivant déjà connu n'est que complété (champs vides
remplis, rien d'écrasé, aucun consentement posé, aucun déclencheur de
propriété ou de consentement) et form_submitted est le seul déclencheur
de workflow qu'un formulaire tire sur lui ; son compteur de soumissions ne
bouge que si le navigateur est celui qui l'a créé (jeton de visiteur) ; un
contact supprimé est un inconnu (un nouveau contact est créé). L'entreprise
n'est rattachée que par le domaine de l'e-mail (jamais créée ni trouvée par
le nom tapé, qui reste dans la soumission et dans le commentaire du nouveau
contact). Profilage progressif : un jeton de visiteur (localStorage) fait
sauter les champs déjà connus, seulement pour un formulaire avec e-mail et
pour la même adresse. Chaque écriture est auditée (source: form).
Anti-abus : honeypot (réponse factice, jeton compris), temps de remplissage
minimum sur un horodatage signé, limites par IP, par formulaire et pour le
déploiement (lib/security/rateLimits.ts), passage par le seam beforeLeadCreate
(source form). L'adresse IP est pseudonymisée (HMAC sous une clé
secrète, FORM_IP_HASH_SALT ou dérivée de BETTER_AUTH_SECRET) et suit la
rétention du contact ; un formulaire supprimé suit celle des fiches
supprimées.
Suivi web (Paramètres → Suivi web) : un script servi par le
déploiement (GET /track.js, à insérer sur le site), un cookie de visiteur
_wapv posé sur le domaine du site après l'accord du visiteur (treize mois
à compter de la première visite, propre à chaque hôte : www. et le domaine
nu comptent pour deux navigateurs) et retiré si l'accord l'est. L'accord
vient du bandeau fourni (libellé selon le mode, lien vers la politique de
confidentialité, redemandé tous les six mois et au passage en nominatif) ou
du gestionnaire de consentement du site (window.wapTracking = { consent: true }, window.wapTrack.consent(ok)). « Global Privacy Control » et « Do
Not Track » sont respectés par le script comme par la route.
Les balises de pages vues (POST /track, URL, titre, référent) ne sont
acceptées que des sites suivis (appConfig.tracking.allowedOrigins,
en-tête Origin et URL de chaque page), corps borné à 64 Ko, limitées par
IP, par visiteur et pour tout le déploiement (600 pages vues par minute ;
un dépassement est signalé, daté, sur la page des réglages).
Tables pageViews (par visiteur et par contact) et webVisitors.
Deux modes : anonyme (rien n'est rattaché à un contact) ou
nominatif (un formulaire soumis depuis ce navigateur, y compris en
iframe, ou un lien de campagne cliqué rattache ses pages vues au contact,
celles d'avant comprises, par lots planifiés ; les suivantes lui arrivent
directement et la fiche est mise à jour au plus une fois par minute). Le
lien de campagne ne transmet jamais son jeton : la redirection vers un site
suivi porte une valeur à usage unique (wapl), valable dix minutes, que le
script retire de la barre d'adresse ; un e-mail transféré identifie donc
celui qui clique comme le destinataire d'origine. Le mode nominatif demande
une base légale, une information claire et une politique de confidentialité
(obligatoire dans les réglages), à valider avant de l'activer. Une
opposition au profilage détache aussitôt navigateurs et pages vues du
contact ; quitter le mode nominatif détache tout (désactiver le suivi
arrête la collecte sans rien détacher) ; une fusion de doublons
les reporte sur la fiche conservée. Sur le contact : pageViewCount,
lastPageViewAt et visitedPages (les 50 derniers chemins distincts),
d'où le filtre « Pages visitées (chemin) » des listes dynamiques et des
règles de scoring (« contient » un texte, « égal à » un chemin), et la
fiche affiche les pages vues dans l'historique (filtre « Navigation »).
Conservation propre (appConfig.tracking.retentionDays, 7 à 395 jours, 90
par défaut) appliquée par la purge nocturne, qui recalcule aussi ces champs
du contact d'après les pages vues restantes ; l'export RGPD contient les
pages vues, l'effacement les emporte.
Doublons : détection des leads en double par téléphone normalisé
(E.164), e-mail, nom + code postal et distance de Levenshtein sur le nom
(clés dedupe estampillées par le trigger des leads, index dédiés). Analyse
par lots planifiés (duplicateScans, leadDuplicates), écran « Doublons
potentiels » avec comparaison côte à côte et fusion champ par champ : les
notes, activités, transactions, envois, workflows et listes du doublon sont
rattachés à la fiche conservée (mutation sous triggers, agrégats et recherche
exacts), entrée d'audit merge. La simulation d'un import signale les
correspondances hors e-mail (doublons probables) et l'import peut mettre à
jour la fiche existante.
Import avancé : page « Importer » (/import, bouton Importer sur les
leads, entreprises, transactions et tâches, et sur les listes) pour un fichier
CSV (,; ou tabulation, BOM accepté) ou Excel .xlsx (première feuille,
lue dans le navigateur par src/features/imports/lib/readXlsx.ts, dates
reconnues par leur format). Les colonnes sont associées aux champs de
l'entité (registres src/features/imports/lib/*Fields.ts, en-têtes usuels
reconnus, propriétés personnalisées incluses) ; une correspondance peut
être enregistrée par source (importMappings) et se réapplique d'elle-même
aux fichiers qui ont les mêmes en-têtes. Le fichier devient un job
(importJobs, lignes dans importRows) traité côté serveur par lots de 200
(features/imports, runBatch) : d'abord une simulation qui dit, ligne
par ligne, création, mise à jour, doublon probable ou erreur sans rien
écrire, puis l'import qui applique exactement les mêmes règles
(plan/apply d'un même importeur par entité, features/imports/entities).
Règles de rapprochement : lead par e-mail (fiche vivante d'abord, sinon
ravivée), sinon doublon probable par téléphone, nom + code postal ou nom
proche, tranché par une politique à l'import (mettre à jour ou créer) ;
entreprise par n° d'immatriculation, TVA, domaine puis nom exact ;
transaction par contact (e-mail) + titre, pipeline et étape par leur nom ;
activité par contact + titre + date, entreprise par nom. Un lot qui échoue
(Convex ne réessaie pas) laisse le job interrompu avec l'erreur ; Reprendre
repart de ce lot, les lots précédents étant validés et les lignes écrites
supprimées au fur et à mesure, donc jamais réécrites. Rapport : compteurs,
lignes en erreur avec leurs cellules d'origine et export CSV des lignes en
erreur (colonnes source + ligne + erreur) ; les jobs terminés se suppriment
depuis le rapport, et la purge de rétention les efface avec les événements
(eventDays). La mutation importLeads reste disponible (mêmes règles,
lib/leads/import.ts). Le consentement marketing n'est jamais importable.
Historique unifié : la fiche lead affiche notes, activités, envois et
événements de campagne, inscriptions aux workflows, changements de statut,
transactions et modifications de la fiche dans un seul fil chronologique,
filtrable par type (features/timeline). Chaque source est paginée sur son
propre index et fusionnée par curseur (lib/timeline/pagination.ts) : charger la suite
ne relit jamais une table entière.
Propriétés personnalisées : champs définis par un admin (9 types :
texte, nombre, e-mail, liste, choix unique/multiple, date, boolean, RPPS)
sur les leads, entreprises, transactions et activités (propertyDefinitions,
entityType), valeurs stockées dans customProperties de chaque fiche.
Validation partagée front/back (_lib/validators/properties.ts,
lib/properties/definitions.ts), colonnes optionnelles dans les listes, formulaires et
filtres avancés génériques par entité (_lib/validators/filters.ts,
features/filters). Chaque type est décrit une fois de chaque côté par un
registre : convex/_lib/validators/propertyTypes.ts (forme stockée,
règles de validation, rendu des paramètres de campagne — le validateur
propertyTypeValidator et la liste des types à options en dérivent) et
src/features/properties/lib/propertyTypes.tsx (libellé, composant de
saisie, affichage, type de filtre, coercition CSV). Ajouter un type = une clé
dans PROPERTY_TYPE_KEYS et un descripteur dans chaque registre ; les
Record<PropertyType, …> font échouer typecheck s'il en manque un, et
tests/backend/propertyTypes.test.ts vérifie la cohérence. Les propriétés calculées (score, dernière activité…)
sont prévues via le drapeau computed.
Statuts : position du lead dans le parcours marketing → commercial
(lifecycleStage : abonné → lead → MQL → SQL → opportunité → client →
ambassadeur), configurable dans Paramètres → Statuts (statuts,
statut par défaut, interdiction du retour en arrière). Chaque changement est
journalisé dans lifecycleStageHistory ; les workflows disposent d'une
étape « Changer le statut ». Le placeholder {{ params.status }} des
campagnes renvoie le libellé du statut.
Campagnes : création de campagnes email Brevo (template + destinataires
filtrés), suivi des envois (campaignSends), statuts. Envoi programmé
(scheduledAt, heure lue dans le fuseau de l'organisation, Paramètres →
E-mail & SMS) annulable et reprogrammable tant que la campagne n'est pas
partie, les destinataires étant résolus au départ ; heure d'envoi
optimisée par destinataire (l'heure de la journée où il ouvre le plus, dans
les 24 h qui suivent le départ) ; test A/B sur l'objet ou le contenu d'un
e-mail rédigé dans l'éditeur (part testée, fenêtre, ouvertures ou clics), la
variante gagnante partant d'elle-même au reste des destinataires, avec les
résultats par variante sur la page de la campagne.
Modèles d'e-mail et blocs de texte (Paramètres → Modèles, ouvert à
tout employé) : des modèles (emailTemplates : nom, objet, corps HTML
de l'éditeur avec ses {{ params.x }}, catégorie, propriétaire, visibilité
private / team / organization, suppression douce purgée avec la
conservation) lus selon leur visibilité par les règles de périmètre (le sien,
ceux de ses coéquipiers, ceux de l'organisation ; un accès complet lit tout),
modifiables par leur auteur ou un détenteur du droit Paramètres, dupliqués
en copie privée ; trois fournis à l'installation (« Bienvenue », « Relance »,
« Invitation », à l'organisation, au premier administrateur) et un bouton les
recrée. « Charger un modèle » dans l'éditeur de campagne (qui peut aussi
« Enregistrer comme modèle » l'e-mail écrit) et dans l'étape « Envoyer un
e-mail » d'un workflow : le contenu est copié, l'identifiant gardé
(campaigns.templateId, templateId du nœud) pour les chiffres d'usage
du modèle, calculés à la lecture dans le périmètre du lecteur : campagnes
envoyées depuis lui (les cent dernières), leurs envois, ouvertures et clics
sommés des compteurs de campagne, et les étapes de workflow qui le chargent
(un e-mail de workflow n'est pas suivi : il compte, sans plus). Des blocs de texte (snippets :
nom, raccourci /devis, texte, communs à l'organisation) s'insèrent dans
l'éditeur par leur raccourci : taper / ouvre le menu, le texte choisi
remplace la saisie, une ligne en ligne, plusieurs en paragraphes.
Abonnements et centre de préférences : des types d'abonnement
(Paramètres → Abonnements, subscriptionTypes : nom, description, canal,
base légale consentement ou intérêt légitime, actif, ordre ; trois
fournis à l'installation, « Newsletter », « Informations produit », « Invitations »,
tous sous consentement, l'intérêt légitime restant au choix du locataire qui
sait s'il s'applique, et un bouton les recrée sur un déploiement antérieur).
Le contact porte ses choix explicites (subscriptions : type,
inscrit/désinscrit, source, date) à côté des canaux (marketingConsent).
Rien sans le consentement au canal, quelle que soit la base : un canal
refusé couvre chacun de ses abonnements, ceux créés plus tard aussi. Avec le
canal, le choix explicite fait foi ; sans choix, un abonnement
« consentement » vaut non inscrit (un consentement se donne par sujet) et un
abonnement « intérêt légitime » vaut inscrit jusqu'à opposition. Les contacts
existants gardent leur canal et ne sont inscrits à aucun sujet tant qu'ils ne
l'ont pas choisi ; une campagne sans abonnement visé continue de cibler le
canal. Refuser un canal désinscrit explicitement de chacun de ses abonnements
; s'inscrire à un abonnement accorde son canal. Un formulaire nomme les
abonnements e-mail que sa case accorde (subscriptionTypeIds) : le contact
créé y est inscrit, et à rien d'autre. Chaque changement écrit un événement
de consentement (consentEvents, jamais modifié : type ou canal, de → vers,
source, hash de l'adresse IP quand la personne a cliqué elle-même), montré
dans le fil du contact (filtre « Consentement »), joint à l'export RGPD, purgé
avec l'effacement. Une fusion de doublons garde un retrait de canal (un
événement « out ») sur l'accord de l'autre fiche et le choix le plus récent
par abonnement, et signe ses changements d'événements merge. Le centre de
préférences/consent/:token (public, le jeton est le secret) montre un
interrupteur par abonnement actif avec sa description et sa base légale, plus
« Tout désinscrire » ; il enregistre par POST /consent/<token> (JSON
{ types: { id: bool }, unsubscribeAll }), la seule voie d'écriture, qui
hache l'adresse et borne : dix écritures par minute et par jeton, un seau
global pour les jetons inconnus, mille écritures par heure pour le
déploiement. Une campagne marketing vise un
abonnement (subscriptionTypeId, un type actif de son canal) : la
préparation écarte les contacts qui n'y sont pas inscrits quel que soit le
filtre, le composeur pose la règle « Est inscrit à » pour que le compte
annoncé soit le bon, et le lien de désinscription mène au centre de
préférences ; une campagne transactionnelle ne vise rien. Le filtre « Est
inscrit à » des leads lit les types actifs (refusé dans les règles de
scoring et les listes dynamiques, qui ne les chargent pas) ; le déclencheur
consent_updated peut viser un abonnement ; un choix réenregistré à
l'identique n'écrit rien et ne déclenche rien.
Droits des personnes (RGPD) : sur la fiche d'un lead, carte « Droits de la
personne » réservée aux détenteurs du droit Paramètres (features/rgpd),
chaque action consignée dans le journal d'audit et dans la table
rgpdRequests (type, demandeur, contact, date, résultat ; l'identifiant du
contact y survit à son effacement, sans autre donnée). Procédure : la
demande arrive par n'importe quel canal ; un administrateur l'exécute depuis
la fiche dans le mois qui suit (article 12 du RGPD), et répond à la
personne avec le résultat. Droit d'accès : « Exporter ses données »
produit une archive JSON de tout ce que le CRM détient sur la personne
(fiche, entreprise, notes, listes, historique de statut, transactions et
activités liées, envois et événements de chaque campagne, enrôlements et
étapes de workflow, soumissions de formulaires, score et règles qui y
contribuent, fichiers joints (métadonnées), journal d'audit), rien sur d'autres personnes ni de technique
(pas de jeton de consentement, de clés de recherche ou de doublons, ni des
responsables) ; 2 000 lignes par table au plus, l'archive dit quelles tables
ont été tronquées. Droit
d'opposition au profilage : l'interrupteur arrête le scoring (score et
détail effacés, plus jamais recalculés) et tout suivi comportemental : les
ouvertures, clics et clics de liens suivis ne sont plus enregistrés du tout
(ni événement de campagne, ni marqueur sur l'envoi, ni compteur, ni
déclencheur de workflow, ni valeur posée par un lien suivi, qui redirige
simplement), les événements de délivrabilité (délivré, rebond, plainte)
restant consignés ; la fiche n'est pas touchée ; l'opposition se lève de la
même façon. Droit à l'effacement : après confirmation, la fiche
et tout ce qui lui appartient sont supprimés définitivement, par étapes
planifiées (même cascade que la purge de rétention : notes, envois, jetons de
liens et événements de campagne, enrôlements et étapes de workflow,
historique de statut, soumissions de formulaires et jeton de visiteur,
appartenances aux listes, paires de doublons, fichiers
et leurs blobs, journal d'audit de la fiche, de ses notes et de ses
enrôlements) ; une transaction ou une activité liée reste, sans lien, car
elle appartient à l'organisation ; il ne reste qu'une ligne d'audit anonyme
(identifiant, date, rgpd: erasure) et la ligne de rgpdRequests. Une
étape qui échoue (Convex ne réessaie pas) laisse la demande « en cours » :
un cron horaire la replanifie. Une fusion de doublons conserve l'opposition
au profilage de la fiche absorbée. Un lead déjà dans la corbeille peut être
effacé ; les sauvegardes s'éteignent avec
leur rétention (docs/). L'API publique n'expose pas ces actions.
Auth : Better Auth (convex/auth.ts) : code envoyé par e-mail, dont le
lien connecte d'un clic, fournisseurs sociaux et SSO OpenID Connect. Seuls
les utilisateurs employee accèdent au CRM.
API publique REST : /api/v1/ sur l'origine .convex.site, clés d'API à
portées (Paramètres → Clés d'API), lecture et écriture des contacts,
entreprises, transactions et activités, listes et propriétés en lecture.