Skip to content

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

ActionPersoUtilisateur proAdmin pro
ImportOuiOuiOui
ExportOuiNon — même ses propres fichesOui
Créer/gérer des utilisateursNonOui
Voir tous les contacts de l'entrepriseOui (sauf échanges privés)Oui (tout)
Changer privé/partagé d'un contactPropriétaire uniquementOui

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" :

  1. Exporter ses contacts depuis son compte perso (CSV)
  2. 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émentMode partagéMode privé
Fiche contact (nom, coordonnées, poste, entreprise…)Visible par tousVisible par tous
ÉchangesVisibles et modifiables par tousMasqués — section vide pour les autres
RappelsVisibles et modifiables par tousMasqués — invisibles pour les autres
Changer le modeProprié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 mailto vers 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.

EndpointUsageAuth
POST /personal-accountsCréer un compte perso (déclenche Supabase signUp + email de confirmation)Aucune — auto-inscription
GET /personal-accountsVérifier si l'utilisateur courant a un compte perso + récupérer son enterpriseIdSupabase bearer
PATCH /personal-accounts/meMettre à jour prénom/nom/email/mot de passeSupabase bearer
DELETE /personal-accounts/meSupprimer le compte persoSupabase bearer
GET /auth-app/portalObtenir l'URL du portail SSO pour initier un switch de contexteSupabase bearer
POST /auth-app/authorizeAutoriser / switcher de contexte actifSupabase 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 = true ET userCtx.role !== 'admin' ET userCtx.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)

#QuestionImpactStatut
1Mé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
2Minimum 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 :

  1. Migration Prisma : isPrivate: Boolean @default(false) sur Contact.
  2. DTO : exposer isPrivate en lecture/écriture avec restriction (propriétaire + admin pour la modification).
  3. Middleware/service : masquer exchanges et rappels quand contact.isPrivate = true et l'utilisateur n'est ni propriétaire ni admin.
  4. Import : forcer isPrivate = false pour tous les contacts importés.
  5. Création manuelle : exposer le choix dans le formulaire de création.
  6. Frontend : indicateur visuel sur la fiche contact, bouton toggle avec guard (propriétaire/admin only).