La boite support — un fil par personne, dans le panel
Piliers : app-shell (panel, service), growth-web (/api/contact), platform-ops (modules/email), integrations (webhook Resend), data-platform (schema). Item : backlog/app-shell/2710.
Piliers :
app-shell(panel, service),growth-web(/api/contact),platform-ops(modules/email),integrations(webhook Resend),data-platform(schema). Item :backlog/app-shell/2710.
Le decoupage
| Canal | Ou il vit | Pourquoi |
|---|---|---|
formulaire /contact, reponses par email | SupportThread / SupportMessage, /admin/content/support | asynchrone, avec statut et historique |
| feedback produit | Feedback, /admin/content/feedbacks | boucle roadmap → changelog |
| candidatures partenaires | NetworkApplication, /admin/revenue/network-applications | lead commercial |
La page support porte une tuile « Autres files » qui somme feedback et candidatures en attente : un seul point d'entree pour « qu'est-ce qui attend une reponse », sans fusionner trois tables qui n'ont pas la meme vie.
La boucle
visiteur ──/contact──▶ SupportThread + SupportMessage(inbound, contact_form)
│ puis, best-effort :
├─▶ mail au desk (contact@), Reply-To support+<id>@
└─▶ accuse de reception au visiteur, Reply-To support+<id>@
operateur ──/admin/content/support/[id]──▶ sendSupportReply
│ From support@<RESEND_SUPPORT_DOMAIN>
│ Reply-To support+<id>@, In-Reply-To / References
└─▶ SupportMessage(outbound, admin), statut waiting_on_customer
visiteur ──repond──▶ Resend (MX) ──email.received──▶ POST /api/webhooks/resend
│ getReceivedEmail(id) : corps + en-tetes
│ fil = +<id> du destinataire, sinon In-Reply-To, sinon nouveau
└─▶ SupportMessage(inbound, email), statut open (spam reste spam)
Trois proprietes qui comptent :
- la ligne precede le mail.
openSupportThreadecrit avantsendContactMessage. Si la base est indisponible, la route retombe sur l'ancien contrat (mail avec le visiteur enReply-To) et repond 502 seulement si ce mail-la echoue aussi ; - le fil est dans l'adresse.
support+<threadId>@est ce que chaque mail sortant pose enReply-To. Un client peut repondre a l'accuse de reception avant qu'on lui ait repondu, et depuis n'importe quelle boite un operateur peut transferer un mail a cette adresse pour le rattacher ; - une redelivrance est un doublon.
SupportMessage.providerMessageIdest unique ; le webhook repond 200 sur P2002 et 500 sur tout autre echec, donc Svix rejoue ce qui doit l'etre et rien d'autre.
Ce que le webhook fait, et ne fait pas
email.received est traite AVANT la liste des types de livraison : ce
n'est pas de l'historique de livraison, c'est un client qui ecrit. Le
service (src/services/admin/support/threads.ts) est importe
paresseusement pour que le chemin livraison ne charge pas tout le module
email.
Le corps lu est text, sinon un html grossierement detague. La citation
sous la reponse (« On … wrote: », « Le … a ecrit : », bloc >) est
retiree de ce que l'operateur lit en premier (stripQuotedReply), jamais
supprimee ailleurs : c'est le texte complet qui manque, pas une copie.
Un From forge sur support+<id>@ ajoute un tour au fil vise. C'est
accepte : le tour est visible par le seul operateur, avec l'adresse
reelle de l'expediteur affichee, et un fil spam n'est jamais rouvert.
Reglages operateur
| Cle | Role | Sans elle |
|---|---|---|
RESEND_SUPPORT_DOMAIN | domaine d'envoi ET de reception du desk | repli sur l'apex : l'envoi marche, les reponses client atterrissent dans une boite mail |
RESEND_WEBHOOK_SECRET | signature Svix du webhook, deja en place | tout email.received est refuse, comme toute livraison |
Chez Resend : ajouter le domaine support avec la reception activee (MX),
cocher email.received sur le webhook qui pointe deja sur
/api/webhooks/resend.
Les trois templateKey (contactNotification, supportAck,
supportReply) sont dans /admin/settings/email-templates. Mettre
supportReply en pause bloque la reponse depuis le panel, et l'action le
dit au lieu d'enregistrer un tour que personne n'a recu.