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 registreapps/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énement | Déclencheur dans le code | Destinataire | Catégorie visée | Idempotence | Test |
|---|---|---|---|---|---|
| Code de connexion (OTP) | auth/otp-flow.ts | la personne qui se connecte | auth | aucune par message : un code remplace le précédent, limites de débit | auth.integration.test.ts, registre |
| Invitation de membre | members/actions.ts (inviteMemberAction) | l'invité | workspace | aucune : un renvoi renvoie un e-mail | members/actions.test.ts, registre |
| Paiement reçu (abonnement et crédits) | modules/billing/effects.ts, événement Stripe invoice.paid, checkout.session.completed | propriétaire du Workspace | billing | table des evt_ Stripe (une fois) | templates.test.ts, billing-lifecycle.integration.test.ts |
| Paiement échoué | invoice.payment_failed | propriétaire | billing | idem | idem |
| Action de paiement requise | invoice.payment_action_required | propriétaire | billing | idem | idem |
| Abonnement terminé | customer.subscription.deleted | propriétaire | billing | idem | idem |
| Remboursement (nouveau) | charge.refunded total sur un lot de crédits | propriétaire | billing | idem | effects.test.ts, credits.integration.test.ts |
| Litige ouvert (nouveau) | charge.dispute.created | opérateur (ADMIN_EMAIL) | admin | idem | effects.test.ts |
| Seuil de dépense d'équipe | credits/ledger.ts (team_alert) | opérateur | notifications | une ligne StudioNotice par mois | notify.integration.test.ts |
| Coût réel au-delà de l'estimation | settle_overrun | opérateur | notifications | une ligne par fait | idem |
| Coût illisible | settle_failed | opérateur | notifications | une ligne par fait | idem |
| Retour (feedback) | api/feedback/route.ts | opérateur | admin | limite de débit | route 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
| Déclencheur | État | Estimation | |
|---|---|---|---|
| Bienvenue, premier Space | cré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 migration | M |
| Quota de lectures à 100 % | même fonction (cas exhausted) | idem ; le 402 existe déjà côté API | M (avec la ligne précédente) |
| Capacité Brain pleine | capacityFull dans modules/brain/ingest.ts | idem : une fois par période, destinataire propriétaire | S à M |
| Abonnement activé (confirmation distincte du reçu) | startedEffect (subscriptionStarted) | le reçu de paiement couvre le besoin ; un e-mail dédié est optionnel | S |
Remboursement d'abonnement (hold refunded) | applyHold | seul le remboursement de crédits envoie un e-mail ; abonnement : à décider | S |
| Litige perdu ou gagné | creditsDisputeLost, release | non couvert, l'alerte d'ouverture suffit au lancement | S |
| Confirmation de changement de langue ou de compte | aucun déclencheur | non pertinent au lancement | à décider |
| Membre retiré, nouvel appareil, suppression de compte, export prêt | aucun 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écider | M |
| Webhook Resend (rebonds, plaintes, suppressions) | route à créer, signature Svix, idempotence sur svix-id | à construire | M |
3. État de Resend (lecture seule, 2026-10-03)
Prouvé par l'API Resend (outils en lecture) :
- Domaine
easyconnector.app: verified, envoi activé, régioneu-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-templatesvide). - Un seul webhook dans le compte, et il pointe vers l'application d'un autre produit (route
/api/webhooks/resendd'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=noneavecruaau minimum). - Qu'
orbit@easyconnector.appsoit la bonne adresse (un domaine vérifié autorise toute adresse). - Que la clé
RESEND_API_KEYde 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ère | Dans le code (actuel) | Hébergés chez Resend |
|---|---|---|
| Typage et variables | fonctions pures, types TypeScript | variables en chaîne, erreurs à l'envoi |
| Deux langues | un seul fichier de messages, tests de parité EN/FR | un gabarit par langue ou logique à reproduire |
| Tests | rendu, échappement, absence de tiret long, jetons, dans la CI | aucun test local |
| Versionnement | Git, relecture en PR, retour arrière | version publiée dans un compte partagé |
| Aperçu | pnpm mail:preview | éditeur Resend (agréable) |
| Isolation | pas de dépendance au compte | gabarits dans un compte partagé entre produits |
Port Mailer | inchangé | l'adaptateur devrait envoyer un identifiant de gabarit |
| Édition par un non-développeur | non | oui |
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.