Aller au contenu principal

Modèle de données

Le schéma Prisma (prisma/schema.prisma) est la source unique de vérité pour le modèle de données : 35 modèles sur PostgreSQL 18, avec Prisma 7 et un soft delete sur certaines entités (deletedAt).

Depuis Prisma 7, le bloc datasource ne porte plus l'URL de connexion (elle vit dans prisma.config.ts et lib/prisma.ts) et le client est généré dans generated/prisma, hors git. Les modèles du plateau (Board, CardPlacement, Connection, AnimatorSession, Lot, BoardLotDistribution, ModelCardTitle, CardMeta) sont partagés avec LeBoard, qui lit et écrit la même base.

Modèles métier​

Member​

Représente un membre de l'association. Le champ role vaut adherent par défaut ; seuls animateur, formateur et les comptes isAdmin accèdent au Hub.

ChampTypeRôle
idString (CUID)Identifiant unique
emailString (unique)Email de connexion
firstName, lastNameStringIdentité
passwordHashString?Hash bcrypt (credentials auth)
genderString"masculin" / "feminin" / "neutre" (défaut) — appliqué aux templates emails
roleString"adherent" (défaut) / "animateur" / "formateur" — rôle pédagogique
isAdminBooleanAccès espace admin (false défaut) — indépendant du role
habilitationAnimationString?"grand_public" / "professionnelle" / null
habilitationFormationString?"grand_public" / "professionnelle" / null
habilitationExpiryDateTime?Date d'expiration habilitations
linkedinIdString? (unique)ID LinkedIn OAuth
linkedinUrl, websiteUrlString?URLs profil
avatarUrlString?URL avatar custom (fallback Gravatar)
bio, taglineString?Bio et phrase d'accroche « En une ligne »
locationString?Localisation
phoneString?Téléphone
badgesString[]IDs badges obtenus (dénormalisé pour perf)
directoryConsentBooleanOpt-in annuaire public
cotisationStatusString"inactive" (défaut) / "active" / "expired" — synchro HelloAsso webhook
cotisationExpiryDateTime?Date expiration cotisation
licenceTypeString?"professionnelle" / "citoyenne" / "interne" — "interne" exempt cotisation
invitationTokenString? (unique)Token invitation pour nouveaux membres
passwordResetTokenString? (unique)Token reset password (avec expiry)
createdAt, updatedAt, deletedAtDateTimeTimestamps (soft delete)

Relations : crée des Workshop, anime via WorkshopAnimator, rapports via UsageReport, badges via BadgeAssertion, gest. organisations via OrganisationManager, cotisations via CotisationPayment.

Workshop​

Représente un atelier ou formation (public ou privé, grand public ou inter-organisations).

ChampTypeRôle
idString (CUID)Identifiant unique
slugString (unique)Slug URL publique
title, descriptionStringTitre et description
dateDateTimeDate/heure de début
durationIntDurée en minutes
eventTypeString"atelier" (défaut) / "formation"
typeStringType atelier technique (ex: "game", "discussion")
formatStringFormat (ex: "en_ligne", "presentiel", "hybride")
timezoneStringTimezone ("Europe/Paris" défaut)
languageString"fr" (défaut)
location, address, city, postalCode, region, departmentString?Adresse (autocompletée BAN)
themeString?Thème
targetPublicString"grand_public" (défaut) / "inter_organisations"
contextString?Contexte de l'atelier, dont benevole (cadre pro bono, réservé au public associatif)
capacityIntNombre de places
price, priceSolidaireHT, priceClassiqueHT, priceSoutienHTFloatTarifs historiques de l'atelier (HT), utilisés quand priceTiers est vide
priceTiersJson?Grille tarifaire libre [{ id, label, description, amountHT }], 8 tarifs maximum. Prime sur les colonnes ci-dessus. Lue uniquement par lib/price-tiers.ts
statusString"draft" (défaut) / "published" / "cancelled"
createdByIdString?Animateur créateur (relation Member). NULL sur les ateliers importés de Pretix : le responsable est le lead WorkshopAnimator, pas ce champ
organisationIdString?Rattachement organisation (inter-organisations)
waitlistEnabledBooleanListe d'attente active
seoTitle, metaDescription, focusKeyphrase, seoScore, readabilityScoreString?/Int?Champs SEO, distincts du contenu éditorial, exposés à LeSite par l'API publique
createdAt, updatedAt, deletedAtDateTimeTimestamps (soft delete)

Le modèle de plateau d'un atelier en ligne n'est pas porté par Workshop mais par Board.modelId.

Relations : createdBy (Member), organisation (Organisation), animateurs via WorkshopAnimator, inscriptions via Registration, liste d'attente via WaitlistEntry, plateau via Board, résultats satisfaction via SatisfactionResponse, rapports utilisation via UsageReport, codes avantage via DiscountCode.

Registration​

Représente l'inscription d'un participant à un atelier.

ChampTypeRôle
idString (CUID)Identifiant unique
workshopId, workshopString + FKAtelier
firstName, lastName, email, phoneStringDonnées participant
paymentStatusString"pending" (défaut) / "paid" / "refunded" / "voucher" / "blocked"
helloassoCheckoutIntentIdString?ID intention de paiement HelloAsso
helloassoOrderIdString?ID ordre HelloAsso (webhook)
amountHT, amountTTC, vatAmountFloatMontants facturés
discountCodeIdString?Code avantage appliqué
discountAmountHTFloatRéduction appliquée
invoiceUrlString?Colonne héritée : rien ne l'écrit aujourd'hui, le reçu de paiement est émis par HelloAsso
priceTierLabelString?Nom du tarif choisi, figé à l'inscription : renommer un tarif de l'atelier ensuite ne touche pas les inscriptions existantes
refundedAtDateTime?Remboursement constaté (y compris décidé depuis le back-office HelloAsso, cf. lib/refund-sync.ts)
attendedBoolean?Présence pointée
participantIdString?Lien CRM Participant (créé/mis à jour au webhook)
cancellationTokenString? (unique)Token d'annulation public (lien email)
cancelledAtDateTime?Date annulation
refundMethodString?"refund" / "voucher" / "blocked" — décision remboursement
surveyTokenString? (unique)Token questionnaire satisfaction
surveySentAt, surveyRespondedAtDateTime?Envoi et réponse satisfaction
reminderSentAtDateTime?Rappel J-2 envoyé
createdAt, updatedAtDateTimeTimestamps
Unique[workshopId, email]Une seule inscription par email par atelier

Relations : workshop, participant (CRM), code avantage.

Participant​

Représente un participant dans le CRM (déduplication par email).

ChampTypeRôle
idString (CUID)Identifiant unique
emailString (unique)Email de déduplication
firstName, lastName, phoneStringDonnées contact
tagsString[]Étiquettes texte libre (admin)
notesString?Notes internes admin
firstSeenAtDateTimePremière inscription enregistrée
createdAt, updatedAt, deletedAtDateTimeTimestamps (soft delete)

Relations : inscriptions via Registration, badges via BadgeAssertion.

WorkshopAnimator​

Liaison N-N atelier ↔ animateur avec rôle (lead/co) et tracking « nouveaux inscrits ».

ChampTypeRôle
idString (CUID)Identifiant unique
workshopIdStringAtelier
memberIdStringAnimateur
roleString"lead" / "co" — lead = responsable
lastViewedAtDateTimeTimestamp dernière visite fiche atelier → déclenche le badge "nouveaux inscrits"
Unique[workshopId, memberId]Un seul rôle par animateur par atelier

Organisation​

Fiches organisation pour ateliers inter-organisations (lookup SIRET auto).

ChampTypeRôle
idString (CUID)Identifiant unique
nameString (unique)Nom organisation
typeString?Type (entreprise, association, etc.)
siretString? (unique)SIRET (lookup API recherche-entreprises)
address, city, postalCodeString?Adresse
notesString?Notes internes
createdAt, updatedAtDateTimeTimestamps

Relations : ateliers via Workshop, gestionnaires via OrganisationManager.

OrganisationManager​

Liaison N-N organisation ↔ membre (gestionnaire).

ChampTypeRôle
idString (CUID)Identifiant unique
organisationId, memberIdStringOrganisation et gestionnaire
createdAtDateTimeTimestamp
Unique[organisationId, memberId]Un seul rôle gestionnaire par membre par org

WaitlistEntry​

Liste d'attente pour ateliers complets.

ChampTypeRôle
idString (CUID)Identifiant unique
workshopId, workshopString + FKAtelier
firstName, lastName, email, phoneStringDonnées
positionIntPosition dans la file
notifiedAtDateTime?Notification d'ouverture de place
createdAtDateTimeTimestamp
Unique[workshopId, email]Une seule entrée par email par atelier

CotisationPayment​

Paiements de cotisation tracés (source HelloAsso).

ChampTypeRôle
idString (CUID)Identifiant unique
memberId, memberString + FKAdhérent
helloassoOrderIdString (unique)ID ordre HelloAsso
licenceTypeStringLicence achetée (professionnelle / citoyenne / interne)
amountCentsIntMontant en centimes
paidAtDateTimeDate paiement
createdAtDateTimeTimestamp

BadgeAssertion​

Badges Open Badges 3.0 (VerifiableCredential JWT signé EdDSA).

ChampTypeRôle
idString (CUID)Identifiant unique
typeString"ANIMATOR_PUBLIC" / "ANIMATOR_PRO" / "TRAINER_PUBLIC" / "TRAINER_PRO" / "PARTICIPANT"
memberId, participantIdString?Bénéficiaire (Member ou Participant)
recipientEmail, recipientNameStringDonnées pour assertion
signedJwtString (Text)JWT EdDSA signé (VerifiableCredential OB3)
issuedAtDateTimeDate émission
revokedBooleanRévocation
createdAtDateTimeTimestamp

UsageReport​

Déclarations de droits d'utilisation professionnels (rapports pédagogiques).

ChampTypeRôle
idString (CUID)Identifiant unique
memberId, workshopIdStringAnimateur et atelier référence
usageContext, organisationName, participantCountStringContexte d'utilisation
amountHT, amountTTCFloatMontants facturés
paymentStatusString"pending" / "paid"
helloassoCheckoutIntentId, helloassoOrderIdString?Traçabilité paiement HelloAsso
invoiceUrlString?Colonne héritée, jamais écrite (même remarque que sur Registration)
createdAt, updatedAtDateTimeTimestamps

Article​

Actualités du Hub (publiées sur LeSite).

ChampTypeRôle
idString (CUID)Identifiant unique
slugString (unique)Slug URL
title, excerpt, bodyStringContenu Markdown GFM
categoryStringCatégorie article
authorName, authorRoleStringAuteur (dropdown admin)
imageUrl, imageCaptionString?Image et légende (upload → WebP)
publishedBooleanPublication
publishedAtDateTime?Date publication
wordCountIntNombre de mots (sert à l'analyse SEO)
seoTitle, metaDescription, focusKeyphrase, seoScore, readabilityScoreString?/Int?Champs SEO, distincts de title/excerpt
createdAt, updatedAtDateTimeTimestamps

Resource​

Médiathèque (articles, livres, podcasts, vidéos, rapports).

ChampTypeRôle
idString (CUID)Identifiant unique
title, descriptionStringTitre et description
typeString?Type (article, book, podcast, video, report)
themesString[]Thématiques (multi-select)
categoryString"documentaire" (défaut)
workshopTypeString?Type atelier associé
authorsString[]Auteurs
dateDateTime?Date publication
languageLinks, titleLinksJsonTraductions et liens par langue
isPublicBooleanVisibilité
createdAtDateTimeTimestamp

PedagogicalVersion​

Supports pédagogiques versionnés (section + slot, accès animateur/formateur).

ChampTypeRôle
idString (CUID)Identifiant unique
section, slotStringSection et slot (ex: "formation", "1")
versionStringVersion (ex: "v1.0")
linkStringURL ressource
dateDateTime?Date version
accessString"animateur" (défaut) / "formateur"
isCurrentBooleanVersion courante
notesString?Notes
createdAtDateTimeTimestamp

DiscountCode​

Codes avantage (réduction % ou montant fixe).

ChampTypeRôle
idString (CUID)Identifiant unique
codeString (unique)Code (ex: "NOEL20")
percentOff, amountOffTTCInt/Float?Réduction (l'une ou l'autre)
maxUsesInt?Limite d'utilisations
workshopIdString?Restriction à un atelier (optionnel)
restrictedToEmailString?Restriction email (optionnel)
expiresAtDateTime?Expiration
activeBooleanActivité
createdAt, updatedAtDateTimeTimestamps

SatisfactionResponse​

Réponses au questionnaire post-atelier.

ChampTypeRôle
idString (CUID)Identifiant unique
workshopId, workshopString + FKAtelier
answersJsonRéponses structurées (JSON)
commentString?Commentaire libre
submittedAtDateTimeDate réponse

Settings​

Paramètres globaux (singleton, id='global').

ChampTypeRôle
idString"global" (unique)
workshopModerationOnBooleanFlag modération ateliers
siteChantierModeBooleanMode chantier site public
priceInterneHT, pricePrestationHT, vatRateFloatTarifications legacy
priceSolidaireHT, priceClassiqueHT, priceSoutienHTFloatPrix par défaut (legacy, overridable par PricingPolicy)
IdentitéassociationName, logoPath, address, postalCode, city, contactEmailInfos association
Mentions légalessiret, rna, tvaNumber, tvaApplicableDonnées légales
EmailemailFrom, emailFromName, emailReplyTo, resendApiKeyConfig Resend
emailFromBilling, emailFromSupportAdresses expéditrices par catégorie (lib/email-categories.ts) ; vides, les gabarits partent de emailFrom
HelloAssohelloassoClientId, helloassoClientSecret, helloassoOrgSlug, helloassoEnvIdentifiants HelloAsso (prod)
helloassoSandboxClientId, helloassoSandboxClientSecret, helloassoSandboxOrgSlugIdentifiants HelloAsso (sandbox)
helloassoRefundEnabledRemboursement HelloAsso auto (en attente privilège RefundManagement)
TelegramtelegramBotToken, telegramChatIdNotifs admin
Logo.devlogoDevPublishableKey, logoDevSecretKeyAPI Logo.dev (signatures digitales optionnel)
emailsEnabledJson?Toggle activation/désactivation des gabarits d'e-mail
SEOseoTitleTemplate, seoDefaultDesc, seoDefaultOgImage, seoSearchConsoleTagRéglages de référencement globaux du site public
seoIndexingEnabledAutorise l'indexation de LeSite. false par défaut, et le reste tant que le mode chantier n'est pas levé
updatedAtDateTimeTimestamp
tvaApplicable porte une décision fiscale, pas un détail d'affichage

L'association est assujettie à la TVA mais bénéficie de la franchise en base (art. 293 B du CGI) : elle ne la collecte pas et ne la récupère pas. tvaApplicable est donc à false, et vatRate ne décrit que le taux qui s'appliquerait si la franchise sautait. Tout code qui calcule ou affiche un prix passe par lib/vat.ts, seul endroit qui tranche, et les documents portent la mention « TVA non applicable - article 293 B du CGI ».

PricingPolicy​

Tarification par eventType × publicType (matrice 3-tiers).

ChampTypeRôle
idString (CUID)Identifiant unique
eventType, publicTypeStringClés (ex: "atelier", "grand_public")
solidaireHT, classiqueHT, soutienHTFloatPrix par tier
Unique[eventType, publicType]Une seule politique par combinaison

FormatPricing​

Prix hors taxes par format d'atelier. Table d'appoint très simple, distincte de PricingPolicy : là où PricingPolicy croise un type d'événement et un type de public pour produire trois tarifs, FormatPricing associe un prix unique à un format.

ChampTypeRôle
idString (CUID)Identifiant unique
formatStringFormat d'atelier, unique
priceHTFloatPrix hors taxes pour ce format

Relations : aucune. La table est lue par clé format.

NotificationConfig​

Configuration notifications email/Telegram par type d'événement admin.

ChampTypeRôle
idString (CUID)Identifiant unique
eventTypeString (unique)Type événement (ex: "rapport_utilisation")
emailEnabled, telegramEnabledBooleanCanaux actifs

Liaisons OAuth LinkedIn en attente (TTL 15 min).

ChampTypeRôle
idString (CUID)Identifiant unique
tokenString (unique)Token lien
linkedinId, linkedinName, linkedinEmailStringDonnées LinkedIn
expiresAtDateTimeExpiration
createdAtDateTimeTimestamp

WorkshopModel​

Modèles de plateau LeBoard.

ChampTypeRôle
idString (CUID)Identifiant unique
nameStringNom modèle
languageString"fr" (défaut)
plateauIdString?Plateau du catalogue (lib/plateaux.ts) dont la version suit les étapes
slidesFileId, slidesFileNameString?Support Google Slides de la version : un seul fichier par modèle, dans lequel chaque lot choisit ses diapositives
createdAt, updatedAtDateTimeTimestamps

Relations : lots via Lot, boards via Board, titres cartes via ModelCardTitle, et les trois tables de l'étape Régénération (RegenerationFamily, RegenerationPractice, RegenerationExample).

Lot​

Lots de cartes d'un modèle plateau.

ChampTypeRôle
idString (CUID)Identifiant unique
modelId, modelString + FKModèle
nameStringNom lot
orderIntOrdre
stageIdString?ID étape LeBoard
kindString?Type lot
cardIdsStringIDs cartes (JSON ou CSV)
contentTypeString"cards" (défaut) / "slides"
slidesDataString?Données diapositives

Relations : distributions via BoardLotDistribution.

Board​

Plateau en ligne d'un atelier (propriété LeBoard, accès en écriture du Hub).

ChampTypeRôle
idString (CUID)Identifiant unique
workshopIdString (unique)Atelier
workshopTitleStringTitre (dénormalisé)
tokenString (unique)Token d'accès
languageString"fr" (défaut)
statusString"active" (défaut)
isTrainingBooleanAtelier test (24h expiration)
expiresAtDateTime?Expiration
modelIdString?Modèle plateau
activeStageIdStringID étape active (persisté pour redémarrage)
createdAt, updatedAtDateTimeTimestamps

Relations : placements via CardPlacement, connexions via Connection, distributions via BoardLotDistribution, sessions via AnimatorSession.

CardPlacement​

Positionnement d'une carte sur un plateau.

ChampTypeRôle
idString (CUID)Identifiant unique
boardId, cardIdStringPlateau et ID carte
x, yFloatCoordonnées
rotationFloatRotation en degrés
zIndexIntProfondeur
flippedBooleanVerso retourné
updatedAtDateTimeTimestamp

Connection​

Flèche/connexion entre cartes sur un plateau.

ChampTypeRôle
idString (CUID)Identifiant unique
boardIdStringPlateau
fromId, toIdStringCartes source/destination
labelString?Label flèche
createdAtDateTimeTimestamp

AnimatorSession​

Sessions SSO animateur (token signé, TTL).

ChampTypeRôle
idString (CUID)Identifiant unique
boardId, memberIdStringPlateau et animateur
pseudoStringPseudonyme animateur
expiresAtDateTimeExpiration session
createdAtDateTimeTimestamp

ModelCardTitle​

Titres de cartes personnalisés par modèle.

ChampTypeRôle
idString (CUID)Identifiant unique
workshopModelId, cardIdStringModèle et ID carte
customTitleStringTitre personnalisé
Unique[workshopModelId, cardId]Un seul titre par carte par modèle

BoardLotDistribution​

Distribution des lots sur un plateau (timestamp + mode de lancement).

ChampTypeRôle
idString (CUID)Identifiant unique
boardId, lotIdStringPlateau et lot
modeString"distribute" (défaut) / "popcorn"
distributedAtDateTimeTimestamp distribution
Unique[boardId, lotId]Un seul record par lot par plateau

CardMeta​

Titres de cartes personnalisés legacy (hors modèle).

ChampTypeRôle
idString (CUID)Identifiant unique
cardIdStringID carte
languageStringLangue
customTitleString?Titre personnalisé
Unique[cardId, language]Un seul titre par carte par langue

SeoMeta​

Référencement d'une page statique de LeSite, édité dans /admin/seo et lu par LeSite à chaque rendu. Une ligne par chemin.

ChampTypeRôle
idString (CUID)Identifiant unique
pathString (unique)Chemin public de la page (ex: /offres/ateliers)
seoTitle, metaDescription, focusKeyphraseString?Titre SEO, meta description, mot-clé cible
ogImagePathString?Image OpenGraph propre à la page
noindexBooleanExclut la page de l'indexation
seoScore, readabilityScoreInt?Derniers scores calculés par l'analyse Yoast
createdAt, updatedAtDateTimeTimestamps

Relations : aucune. Le rapprochement avec une page se fait par path, ce qui veut dire qu'une URL renommée côté LeSite laisse sa ligne orpheline.

ContactRequest​

Demandes du formulaire de contact de LeSite, persistées dans le Hub et suivies dans /admin/contact-requests.

ChampTypeRôle
idString (CUID)Identifiant unique
firstName, lastName, email, phoneStringIdentité du demandeur (prénom et nom séparés depuis septembre 2026)
subject, messageStringSujet choisi et message libre
contextJson?Champs de qualification du sujet « Organiser un atelier » (organisation, fonction, effectif, période, format), stockés à part pour être affichés comme des champs. NULL pour les demandes d'avant le 7 septembre 2026, qui gardent leur contexte en tête du message
statusStringnouveau (défaut) / en_cours / traite / archive
sourceStringsite (formulaire public, défaut) / manuel (consignée par un admin)
notesString?Notes internes
handledAtDateTime?Date de traitement
createdAt, updatedAtDateTimeupdatedAt est le point de départ des trois ans de conservation annoncés dans les mentions légales de LeSite

RegenerationFamily, RegenerationPractice, RegenerationExample​

Les trois tables de l'étape « Régénération » du plateau : des familles de pratiques, chaque pratique correspondant à une carte, et des exemples rattachés à une pratique. Elles sont scopées à une version de cartes (workshopModelId), éditées dans l'onglet Régénération de /admin/cards/[id], et lues en direct par LeBoard.

ModèleChampsParticularités
RegenerationFamilyworkshopModelId, nameNom de famille renommable ; suppression en cascade avec la version
RegenerationPracticeworkshopModelId, familyId, cardIdUnique [workshopModelId, cardId] : une carte est une pratique au plus une fois par version
RegenerationExampleworkshopModelId, cardId, practiceId?Unique [workshopModelId, cardId]. practiceId est nullable (SetNull) : un exemple peut n'être rattaché à aucune pratique

Le titre affiché d'une pratique vient des titres de cartes de la version (ModelCardTitle), pas de ces tables.

Diagramme ER (flux principaux)​

SeoMeta et ContactRequest n'apparaissent pas : ce sont deux tables sans relation, rapprochées de l'extérieur par le chemin de page pour la première, par rien du tout pour la seconde.