Appearance
Dual-compte (Pro + Perso) — Spécification produit & technique
Document de référence issu des échanges avec François Rey (mai 2026).
Sources :Heriade_synthese (1).docx+ clarifications orales + spec OpenAPI Console Admin (docs/console-admin-api.json).
À lire avant toute implémentation du système dual-compte ou de la feature contact privé/partagé.
1. Vision produit
Heriade est un gestionnaire de contacts léger (carnet enrichi + suivi relationnel), pas un CRM commercial. Deux niveaux de comptes coexistent :
- Compte entreprise (pro) : collaboratif, propriété des données = l'entreprise.
- Compte personnel (perso) : individuel, propriété des données = la personne.
L'écart de prix entre les deux se justifie par le collaboratif (partage, privatisation des infos, base entreprise structurée) — pas par la synchro automatique, qui est délicate côté salarié (réseau LinkedIn personnel, réticences RGPD).
Effet réseau intentionnel : chaque entreprise cliente génère des utilisateurs perso potentiels, réintroduits dans leur boîte suivante. C'est un moteur d'acquisition intégré.
2. Architecture des comptes
2.1 Règles fondamentales
- Une personne peut avoir deux comptes : un perso (mail perso) et un pro (mail entreprise).
- Un perso est associé à un seul pro à la fois ; un pro peut exister sans perso ; un perso peut exister sans pro.
- Une entreprise cliente nécessite un minimum de 5 utilisateurs (dont 1 admin).
- Chaque compte pro créé donne droit à un compte perso gratuit.
- Les données créées sous compte pro appartiennent à l'entreprise.
- Les données créées sous compte perso appartiennent à l'individu.
- Pas de transfert automatique pro → perso : intentionnel, argument commercial anti-siphonnage auprès des entreprises.
2.2 Tableau de synthèse des droits
| Action | Perso | Utilisateur pro | Admin pro |
|---|---|---|---|
| Import | Oui | Oui | Oui |
| Export | Oui | Non — même ses propres fiches | Oui |
| Créer/gérer des utilisateurs | — | Non | Oui |
| Voir tous les contacts de l'entreprise | — | Oui (sauf échanges privés) | Oui (tout) |
| Changer privé/partagé d'un contact | — | Propriétaire uniquement | Oui |
Règle export pro sans exception : une fiche créée avec une licence pro l'a été dans le cadre du travail. Elle appartient à l'entreprise, même si c'est l'utilisateur qui l'a créée. Zéro exception.
2.3 Transfert perso → pro
Il n'existe pas de bridge direct. L'utilisateur doit passer par "l'extérieur" :
- Exporter ses contacts depuis son compte perso (CSV)
- Importer ce CSV dans son compte pro
C'est une démarche consciente et volontaire — c'est voulu.
2.4 Entreprise ne peut pas être "privée"
Une entreprise (au sens entité dans la base) est toujours visible par tous les membres de l'entreprise cliente. La notion de privé/partagé ne s'applique qu'aux contacts.
3. Cycle de vie des comptes
3.1 Compte perso gratuit lié au pro
Le compte perso est gratuit tant qu'il est associé à un pro actif.
3.2 Période de grâce à la rupture du lien pro
Quand le lien pro est rompu (départ du salarié OU résiliation de l'abonnement entreprise) :
- Le compte perso reste pleinement actif et gratuit pendant 2 mois, calculés en fin de mois.
- Exemple : départ le 20 mai → compte perso devient payant le 1er août (fin mai + 2 mois complets).
- Prélèvement en début de mois, aucun remboursement au prorata.
3.3 Compte perso payant qui rejoint une entreprise cliente
Le compte perso redevient gratuit le mois suivant l'association au pro.
- Exemple : arrivée le 20 mai → pas de prélèvement le 5 juin.
3.4 Départ d'un salarié (compte pro)
Le compte pro du salarié partant reste propriété de l'entreprise. Ses fiches contacts sont rattachées au compte admin de l'entreprise.
3.5 Résiliation entreprise
À la résiliation, un export des données (utilisateurs / contacts / entreprises) est proposé avant confirmation. Suppression ou anonymisation des données applicatives 30 à 90 jours après résiliation.
4. Feature — Contact privé / partagé
4.1 Définition
Un contact peut être en mode partagé (défaut) ou privé. Ce flag s'applique au niveau du contact, pas de l'échange individuel.
Ce que "privé" signifie précisément :
| Élément | Mode partagé | Mode privé |
|---|---|---|
| Fiche contact (nom, coordonnées, poste, entreprise…) | Visible par tous | Visible par tous |
| Échanges | Visibles et modifiables par tous | Masqués — section vide pour les autres |
| Rappels | Visibles et modifiables par tous | Masqués — invisibles pour les autres |
| Changer le mode | — | Propriétaire + admin seulement |
Exemple concret : un utilisateur gère le dossier de licenciement de Gérard avec un avocat. Il passe le contact de l'avocat en privé. Les collègues voient le contact et peuvent appeler l'avocat si besoin, mais ne voient pas les échanges qui révèlent le contexte confidentiel.
Si confidentialité totale nécessaire (contact lui-même invisible) → utiliser le compte perso.
4.2 Visibilité de la section échanges pour les tiers
Quand un contact est privé, les autres utilisateurs voient la section échanges mais elle apparaît vide. Ils ne savent pas qu'il y a des échanges cachés — pas d'indication "échanges privés".
4.3 Valeur par défaut
- Contact importé (CSV, vCard, contacts natifs) → partagé par défaut.
- Contact créé manuellement → l'utilisateur choisit au moment de la création.
4.4 Qui peut modifier le mode
- Le propriétaire du contact (
ownerUserId). - Un admin de l'entreprise.
- Personne d'autre — même un collègue qui a ajouté des échanges ne peut pas changer le mode.
5. UX — Switch pro / perso
5.1 Navigation entre les deux comptes
- En bas à gauche dans le profil : bouton de switch pro ↔ perso.
- Si l'utilisateur n'a pas encore de compte perso : lien "Créer un compte perso" qui renvoie vers une page de création.
- L'utilisateur peut définir quel compte est affiché par défaut à la connexion.
5.2 Différenciation visuelle
Une pastille (couleur ou icône) en haut de l'interface indique sur quel compte l'utilisateur se trouve actuellement (pro ou perso).
5.3 Connexion
L'utilisateur se connecte toujours avec son compte pro. Le switch vers le perso se fait depuis l'interface (pas de reconnexion SSO séparée — à confirmer selon l'implémentation finale, voir section 7).
6. Synchro LinkedIn / mail
- Synchro réservée au compte perso, jamais au compte pro.
- Raison : LinkedIn est un compte personnel (réseau qui appartient au salarié), réticences RGPD, restrictions LinkedIn.
- Mail : le bouton
mailtovers le client par défaut suffit. Une vraie connexion boîte mail pose les mêmes problèmes que LinkedIn pour les salariés. - Statut : feature à terme sur compte perso, pas de date.
7. Architecture technique — Console Admin & portail SSO
L'infrastructure dual-compte est entièrement en place dans la Console Admin. Il n'y a pas de nouveau service à créer côté infra.
7.1 Modèles de données Console Admin
Enterprise — entité centrale
Chaque espace (pro ou perso) est une Enterprise dans la Console Admin. Le type distingue les deux :
EnterpriseDto {
id: number // l'enterpriseId qui circule dans tous les JWT
name: string // nom de la société (pro) ou de la personne (perso, ex: "John Doe")
type: 'enterprise' // → compte pro
| 'personal' // → compte perso
maxUsers: number | null // limite de l'abonnement
environnement: 'shared' | 'private'
}Profile — représentation d'un utilisateur dans un compte pro
ProfileRecordDto {
userId: string // UUID Supabase — pivot du lien pro ↔ perso
enterpriseId: number // ID d'une Enterprise type 'enterprise'
role: { slug: string } // 'admin' | 'user' | 'super_admin'
}PersonalAccount — représentation d'un utilisateur dans un compte perso
PersonalAccountDto {
userId: string // UUID Supabase — MÊME valeur que ProfileRecordDto.userId
enterpriseId: number // ID d'une Enterprise type 'personal', auto-créée à l'inscription
enterpriseName: string // = nom de la personne (ex: "John Doe")
emailConfirmed: boolean
settings: {
enterpriseId: number
code: string | null // code court (ex: "JDO") — usage à confirmer
}
}Le lien pro ↔ perso se fait via le userId Supabase commun. Il n'y a pas de champ linkedProEnterpriseId — la relation est implicite : même userId dans ProfileRecordDto (pro) et dans PersonalAccountDto (perso).
GET /auth/me — contexte résolu
L'appel GET /auth/me (Supabase bearer) retourne :
AuthMeDto {
user: { id: string, email: string } // identité Supabase
profile: { enterprise_id: number, role: … } // contexte actif = quel enterpriseId est en session
permissions: { isAdmin, isSuperAdmin }
}C'est ce profile.enterprise_id qui alimente le JWT Heriade émis par le portail SSO.
7.2 Flux d'authentification complet
Utilisateur
│
▼ 1. S'authentifie sur le portail SSO Heriade (Supabase)
Portail SSO Heriade (staging.auth.heriade.fr)
│
▼ 2. Appelle GET /auth/me → résout enterprise_id actif + role
│ (enterprise_id = ID pro ou ID perso selon le contexte choisi)
│
▼ 3. Génère JWT Heriade HS256 avec claims (enterpriseId, userId, role, app, env…)
│
▼ #app_access_token=<JWT_HERIADE> (hash fragment)
Nuxt 3 SPA ──── X-App-Access-Token ────► NestJS API
│
TenantInterceptor extrait userCtx
PrismaService middleware filtre
automatiquement par enterpriseId
│
PostgreSQL
(heriade_contacts)Pour switcher pro → perso : le portail SSO doit émettre un nouveau JWT Heriade avec l'enterpriseId de l'enterprise type: 'personal' de l'utilisateur. Le mécanisme exact (GET /auth-app/portal + POST /auth-app/authorize) est à confirmer avec l'équipe Console Admin — AuthorizeDto est vide dans la spec, les paramètres sont peut-être en query string.
7.3 Impact sur l'app contacts — le point clé
L'architecture Prisma actuelle n'a pas besoin de changer pour supporter le dual-compte.
Le middleware PrismaService filtre déjà toutes les requêtes par enterpriseId via AsyncLocalStorage. Que ce JWT porte l'enterpriseId d'une enterprise type: 'enterprise' (pro) ou type: 'personal' (perso), le filtre s'applique identiquement. Les données pro et perso sont naturellement isolées dans PostgreSQL puisqu'elles ont des enterpriseId différents.
Seul le frontend doit changer pour le switch : détecter si l'utilisateur a un compte perso et afficher le bouton de bascule.
7.4 Endpoints Console Admin utiles pour l'app contacts
La Console Admin utilise des JWT Supabase (supabase-bearer). L'app contacts utilise des JWT Heriade HS256 (X-App-Access-Token). Ces deux couches d'auth sont distinctes — le portail SSO fait le pont.
| Endpoint | Usage | Auth |
|---|---|---|
POST /personal-accounts | Créer un compte perso (déclenche Supabase signUp + email de confirmation) | Aucune — auto-inscription |
GET /personal-accounts | Vérifier si l'utilisateur courant a un compte perso + récupérer son enterpriseId | Supabase bearer |
PATCH /personal-accounts/me | Mettre à jour prénom/nom/email/mot de passe | Supabase bearer |
DELETE /personal-accounts/me | Supprimer le compte perso | Supabase bearer |
GET /auth-app/portal | Obtenir l'URL du portail SSO pour initier un switch de contexte | Supabase bearer |
POST /auth-app/authorize | Autoriser / switcher de contexte actif | Supabase bearer |
7.5 Feature contact privé — changements Prisma
Ajout d'un champ isPrivate: Boolean @default(false) sur le modèle Contact.
Migration Prisma nécessaire :
prisma
// schema.prisma
model Contact {
// ... champs existants
isPrivate Boolean @default(false)
}sql
ALTER TABLE "Contact" ADD COLUMN "isPrivate" BOOLEAN NOT NULL DEFAULT false;Règle à appliquer dans les resolvers Exchange et Reminder :
- Si
contact.isPrivate = trueETuserCtx.role !== 'admin'ETuserCtx.userId !== contact.ownerUserId→ retourner une liste vide (ne pas lever d'erreur, ne pas révéler l'existence d'échanges cachés).
Ne pas filtrer le contact lui-même — il reste visible. Seuls ses exchanges et rappels sont masqués.
8. Points en suspens (à valider avant implémentation)
| # | Question | Impact | Statut |
|---|---|---|---|
| 1 | Mécanisme exact du switch de contexte : POST /auth-app/authorize avec quels params ? Ou redirection portail complète ? | Frontend switch UX | À confirmer avec équipe Console Admin |
| 2 | Minimum 5 utilisateurs : pack forcé à l'achat ou minimum contractuel ? | Facturation | À clarifier |
9. Ce qui est prêt à implémenter maintenant
La feature contact privé/partagé est entièrement spécifiée et indépendante du dual-compte. Elle peut être développée dès maintenant :
- Migration Prisma :
isPrivate: Boolean @default(false)surContact. - DTO : exposer
isPrivateen lecture/écriture avec restriction (propriétaire + admin pour la modification). - Middleware/service : masquer exchanges et rappels quand
contact.isPrivate = trueet l'utilisateur n'est ni propriétaire ni admin. - Import : forcer
isPrivate = falsepour tous les contacts importés. - Création manuelle : exposer le choix dans le formulaire de création.
- Frontend : indicateur visuel sur la fiche contact, bouton toggle avec guard (propriétaire/admin only).
