Documentation — Comment ça marche
Dernière mise à jour : 6 août 2026
1. Vue d'ensemble du flux
Un rendez-vous est créé par un organisateur connecté, puis transmis à un invité via un lien public unique. L'invité n'a besoin d'aucun compte : il répond depuis une page publique, et la réponse remonte immédiatement sur le dashboard de l'organisateur.
Organisateur (connecté)
└─ crée un rendez-vous ──▶ appointments (status = pending, invite_token généré)
├─ « Envoyer par email » ──▶ server fn sendInviteEmail ──▶ Resend
└─ « Copier le lien » ──▶ /invite/<token>
Invité (sans compte)
└─ ouvre /invite/<token> ──▶ RPC get_invite(_token) (lecture publique filtrée)
└─ Accepter / Refuser ──▶ RPC respond_to_invite(_token, _accept, _message)
└─ status = accepted | declined, responded_at = now()
Organisateur
└─ dashboard : badge de statut, date de réponse, message de l'invité, statistiques2. Contenu de l'email d'invitation
L'email est composé côté serveur dans la fonction sendInviteEmail et envoyé via Resend. Expéditeur : valeur de RESEND_FROM (par défaut « Cadence <onboarding@resend.dev> »). Objet : « Rendez-vous : {{title}} ».
Structure du corps, dans l'ordre :
- Titre « Proposition de rendez-vous ».
- Salutation « Bonjour {{guestName}}, ».
- Encart message personnalisé (organisateurs Business uniquement).
- Titre du rendez-vous en gras, puis date et créneau formatés en français.
- Lieu et description, chacun affiché seulement s'il est renseigné.
- Bouton d'action vert « Confirmer ou refuser » pointant vers le lien d'invitation.
- Rappel du lien en texte brut, pour les clients mail qui bloquent les boutons.
- Bloc « Avis Google » (Business) : nom de l'entreprise, note, lien avis, lien Maps.
Toutes les valeurs dynamiques sont échappées en HTML avant insertion : aucun contenu saisi par l'organisateur ne peut injecter de balise dans l'email.
Variables disponibles
| Variable | Source | Utilisation |
|---|---|---|
| {{guestName}} | contacts.name | Ligne d'accroche « Bonjour {{guestName}}, ». Vide si aucun contact n'est rattaché. |
| {{title}} | appointments.title | Objet de l'email « Rendez-vous : {{title}} » et titre en gras dans le corps. |
| {{dateRange}} | appointments.starts_at / ends_at | Formaté en français : « mardi 12 août, 15:00 → 15:30 » (Intl.DateTimeFormat fr-FR). |
| {{location}} | appointments.location | Ligne « Lieu : … ». Le bloc entier est omis si le champ est vide. |
| {{description}} | appointments.description | Paragraphe libre affiché sous la date. Omis si vide. |
| {{inviteLink}} | origin + /invite/ + appointments.invite_token | Bouton « Confirmer ou refuser » + rappel du lien en clair en bas de l'email (copier/coller). |
| {{inviteMessage}} | profiles.invite_message (Business) | Encart vert de message personnalisé, affiché uniquement pour un organisateur Business. |
| {{businessName}} / {{googleRating}} / {{googleReviewUrl}} / {{googleMapsUrl}} | profiles.* (Business) | Bloc « Avis Google » en pied d'email et sur la page d'invitation, si l'option est activée. |
Cas d'envoi non aboutis
- no_email : le contact n'a pas d'adresse email. L'organisateur doit copier le lien et l'envoyer par un autre canal.
- no_api_key : la clé d'envoi n'est pas configurée. Le lien d'invitation est tout de même renvoyé à l'interface pour copie manuelle.
- provider_error : refus du fournisseur d'email (adresse invalide, domaine non vérifié…). Le statut du rendez-vous reste inchangé.
- En cas de succès, invite_sent_at est horodaté : c'est ce champ qui alimente le suivi « liens envoyés » des statistiques.
3. Écrans de confirmation (page /invite/<token>)
Page publique, sans authentification, rendue à partir de la fonction sécurisée get_invite qui ne renvoie que les champs nécessaires à l'affichage.
- Chargement : squelette animé (calendrier) tant que la réponse n'est pas arrivée.
- Lien invalide ou expiré : message d'erreur, aucune information sur l'organisateur ni sur le rendez-vous n'est divulguée.
- État « en attente » : objet, date et créneau, durée, lieu, nom de l'organisateur, message personnalisé (Business), champ libre facultatif (500 caractères max), puis deux boutons « J'accepte » et « Je refuse ».
- Après acceptation : badge vert « Accepté », confirmation à l'écran, rappel des informations pratiques et bloc avis Google si l'organisateur est Business.
- Après refus : badge rouge « Refusé », message indiquant que l'organisateur est prévenu ; l'invité peut rouvrir le lien et changer sa réponse tant que le rendez-vous n'est pas annulé.
- Rendez-vous annulé : les boutons de réponse n'ont plus d'effet, le statut affiché reste « Annulé ».
Le message libre saisi par l'invité est tronqué à 500 caractères côté client et 1 000 côté base, puis affiché à l'organisateur sur la fiche du rendez-vous.
4. Statuts des rendez-vous
| Statut | Libellé | Déclencheur | Effet |
|---|---|---|---|
| pending | En attente | À la création du rendez-vous (valeur par défaut en base). | Le lien est actif, l'invité peut répondre. Compté dans « réponses attendues ». |
| accepted | Accepté | L'invité clique sur « J'accepte » — appel de respond_to_invite(_accept = true). | responded_at et response_message sont renseignés ; badge vert sur le dashboard. |
| declined | Refusé | L'invité clique sur « Je refuse » — respond_to_invite(_accept = false). | Même enregistrement que l'acceptation, badge rouge. Le créneau reste visible pour relance. |
| cancelled | Annulé | Action de l'organisateur depuis son dashboard. | Le lien n'accepte plus de réponse (respond_to_invite ne met à jour que les autres statuts). |
5. Flux côté serveur
- Création / modification / suppression : effectuées par le client authentifié ; l'isolement des données est garanti par les règles d'accès en base.
- sendInviteEmail : fonction serveur protégée. Elle recharge le rendez-vous en le filtrant sur l'identifiant du demandeur, refuse si le rendez-vous ne lui appartient pas, compose l'email, appelle Resend puis horodate l'envoi.
- get_invite(_token) : fonction en base à privilèges élevés, seule voie de lecture publique. Elle renvoie uniquement les champs d'affichage, et masque les informations de marque pour un organisateur non Business.
- respond_to_invite(_token, _accept, _message) : met à jour le statut, le message et l'horodatage de réponse. Elle n'agit que sur la ligne correspondant au jeton et ignore les rendez-vous annulés.
- get_plan_usage() : renvoie le plan, la consommation du mois et l'indicateur administrateur pour l'affichage des quotas.
6. Logique d'autorisation
- Organisateur : accès complet en lecture et écriture à ses propres contacts et rendez-vous, et à eux seuls.
- Invité non connecté : aucun accès direct aux tables. Il ne peut que lire un rendez-vous via son jeton et y répondre. Le jeton est aléatoire (36 caractères hexadécimaux) et ne donne accès à rien d'autre.
- Rôles : stockés dans une table dédiée (utilisateur, modérateur, administrateur) et vérifiés côté serveur ; ils ne sont jamais lus depuis le navigateur pour accorder un droit.
- Administrateur : accès à l'espace /admin (tous les comptes, contacts et rendez-vous) et exemption des quotas du plan Free-mium.
- Plan : le changement de palier ne peut pas être forcé depuis le client ; seul le processus de paiement Stripe ou un administrateur peut le modifier.
- Quotas : appliqués en base avant chaque création — 10/15 (Free-mium), 30/50 (Basique), 50/100 (Standard), 250/500 (Economic) contacts/rendez-vous par mois calendaire, illimité en Business et pour les administrateurs.
7. Check-list pour l'équipe d'édition
- Modifier un texte d'email : agir sur le gabarit HTML de la fonction d'envoi, pas sur la page d'invitation.
- Modifier un écran de confirmation : agir sur la page publique d'invitation.
- Toute nouvelle variable doit provenir d'un champ déjà exposé par la fonction de lecture publique.
- Ne jamais afficher sur la page publique une donnée d'un autre rendez-vous ou d'un autre contact.
- Tester systématiquement les trois états : en attente, accepté, refusé.