Runbooks OrbitRéférence des e-mails

Référence des e-mails

Tous les e-mails d'Orbit (déclencheur, gabarit, langues, expéditeur, idempotence, test), ce qui manque au lancement, l'état prouvé de Resend et le verdict gabarits hébergés ou gabarits dans le code.

Skill transactional-email (dépôt Orbit). Source de vérité du code : le registre apps/web/src/lib/mail/templates/events.ts ; templates.test.ts échoue si un événement n'y figure plus, si l'anglais et le français divergent, ou si un tiret long apparaît. Relecture humaine : pnpm mail:preview écrit chaque e-mail, dans les deux langues, dans .mail-preview/ (rien n'est envoyé). Ce qui reste à faire vit sur Reste à faire.

1. E-mails existants

Tous passent par une mise en page (layout.ts), les jetons de couleur (tokens.ts), une alternative text/plain, et la langue du destinataire (StudioMember.locale, sinon la requête, sinon anglais).

ÉvénementDéclencheur dans le codeDestinataireCatégorie viséeIdempotenceTest
Code de connexion (OTP)auth/otp-flow.tsla personne qui se connecteauthaucune par message : un code remplace le précédent, limites de débitauth.integration.test.ts, registre
Invitation de membremembers/actions.ts (inviteMemberAction)l'invitéworkspaceaucune : un renvoi renvoie un e-mailmembers/actions.test.ts, registre
Paiement reçu (abonnement et crédits)modules/billing/effects.ts, événement Stripe invoice.paid, checkout.session.completedpropriétaire du Workspacebillingtable des evt_ Stripe (une fois)templates.test.ts, billing-lifecycle.integration.test.ts
Paiement échouéinvoice.payment_failedpropriétairebillingidemidem
Action de paiement requiseinvoice.payment_action_requiredpropriétairebillingidemidem
Abonnement terminécustomer.subscription.deletedpropriétairebillingidemidem
Remboursement (nouveau)charge.refunded total sur un lot de créditspropriétairebillingidemeffects.test.ts, credits.integration.test.ts
Litige ouvert (nouveau)charge.dispute.createdopérateur (ADMIN_EMAIL)adminidemeffects.test.ts
Seuil de dépense d'équipecredits/ledger.ts (team_alert)opérateurnotificationsune ligne StudioNotice par moisnotify.integration.test.ts
Coût réel au-delà de l'estimationsettle_overrunopérateurnotificationsune ligne par faitidem
Coût illisiblesettle_failedopérateurnotificationsune ligne par faitidem
Retour (feedback)api/feedback/route.tsopérateuradminlimite de débitroute test

Le retour (feedback) reste en texte brut : message interne, sans mise en page.

Constats : (a) le champ From est unique (Orbit <orbit@easyconnector.app>, products/connectors.json) : les catégories d'expéditeur du skill ne sont pas encore appliquées, le registre les porte à titre déclaratif ; (b) aucun e-mail n'est jamais envoyé avec List-Unsubscribe : normal tant qu'aucun envoi marketing n'existe ; (c) il n'y a pas de route webhook Resend dans l'application (voir section 3).

2. Ce qui manque au lancement

E-mailDéclencheurÉtatEstimation
Bienvenue, premier Spacecréation du premier Space (modules/brain/spaces.ts)à construire : il faut un marqueur « bienvenue envoyé » (migration Prisma)S à M
Quota de lectures MCP/API à 80 %modules/brain/quota.ts (reserveRead)détection possible au franchissement, mais pas de dédoublonnage par mois : table ou colonne et migrationM
Quota de lectures à 100 %même fonction (cas exhausted)idem ; le 402 existe déjà côté APIM (avec la ligne précédente)
Capacité Brain pleinecapacityFull dans modules/brain/ingest.tsidem : une fois par période, destinataire propriétaireS à M
Abonnement activé (confirmation distincte du reçu)startedEffect (subscriptionStarted)le reçu de paiement couvre le besoin ; un e-mail dédié est optionnelS
Remboursement d'abonnement (hold refunded)applyHoldseul le remboursement de crédits envoie un e-mail ; abonnement : à déciderS
Litige perdu ou gagnécreditsDisputeLost, releasenon couvert, l'alerte d'ouverture suffit au lancementS
Confirmation de changement de langue ou de compteaucun déclencheurnon pertinent au lancementà décider
Membre retiré, nouvel appareil, suppression de compte, export prêtaucun déclencheur (ni suppression de compte ni export dans l'application)à construire avec la fonctionnalitéselon la fonctionnalité
Génération terminée (asynchrone)generation/service.tsà déciderM
Webhook Resend (rebonds, plaintes, suppressions)route à créer, signature Svix, idempotence sur svix-idà construireM

3. État de Resend (lecture seule, 2026-10-03)

Prouvé par l'API Resend (outils en lecture) :

  • Domaine easyconnector.app : verified, envoi activé, région eu-west-1, suivi des clics activé, suivi des ouvertures désactivé. Enregistrements : DKIM (resend._domainkey) verified, SPF TXT (send, include:amazonses.com) verified, MX de retour (send) verified.
  • 30 derniers jours sur ce domaine : 16 envoyés, 14 délivrés, 2 rebonds transitoires, 0 plainte. Le taux de rebond (12,5 %) vient d'un volume minuscule : à surveiller, pas un signal.
  • Aucun gabarit hébergé (list-templates vide).
  • Un seul webhook dans le compte, et il pointe vers l'application d'un autre produit (route /api/webhooks/resend d'un hôte qui n'est pas Orbit). Aucun webhook Orbit.
  • Le compte Resend contient aussi les domaines d'autres produits (quatre autres domaines, d'autres produits).

Non prouvé :

  • DMARC (_dmarc.easyconnector.app) : Resend ne l'expose pas et la résolution DNS n'était pas possible depuis l'environnement de travail. À vérifier par le propriétaire (p=none avec rua au minimum).
  • Qu'orbit@easyconnector.app soit la bonne adresse (un domaine vérifié autorise toute adresse).
  • Que la clé RESEND_API_KEY de Vercel soit propre à Orbit : la règle du skill demande un projet Resend, une clé et un secret de webhook propres à Orbit, alors que le domaine vit dans un compte partagé.
  • Qu'un e-mail réel parte en production (aucun envoi n'a été fait pour ce constat).
  • Le sous-domaine d'envoi pour le marketing : il n'existe pas (aucun news.easyconnector.app) ; il n'est requis qu'avec le premier envoi marketing.
  • Le suivi des clics est activé sur l'apex : il réécrit les liens, ce qui est déconseillé pour des mails d'authentification et de facturation (lien de connexion, facture Stripe). À désactiver ou à cantonner.

4. Verdict : gabarits hébergés chez Resend ou gabarits dans le code

Recommandation : garder les gabarits dans le code, ce que le skill a déjà décidé.

CritèreDans le code (actuel)Hébergés chez Resend
Typage et variablesfonctions pures, types TypeScriptvariables en chaîne, erreurs à l'envoi
Deux languesun seul fichier de messages, tests de parité EN/FRun gabarit par langue ou logique à reproduire
Testsrendu, échappement, absence de tiret long, jetons, dans la CIaucun test local
VersionnementGit, relecture en PR, retour arrièreversion publiée dans un compte partagé
Aperçupnpm mail:previewéditeur Resend (agréable)
Isolationpas de dépendance au comptegabarits dans un compte partagé entre produits
Port Mailerinchangél'adaptateur devrait envoyer un identifiant de gabarit
Édition par un non-développeurnonoui

Seul avantage réel des gabarits hébergés : l'édition sans déploiement. Il ne compense pas la perte des tests, de la parité des langues et de l'isolation. Une migration reste possible plus tard (l'adaptateur est le seul point d'envoi) ; elle exige une décision du propriétaire.

5. Blueprint

La garde ajoutée (le registre et ses tests : chaque événement a un gabarit, EN et FR identiques, aucun tiret long, mise en page unique) est inscrite dans la ligne k du Blueprint.