ArchitectureLa boite support — un fil par personne, dans le panel

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

CanalOu il vitPourquoi
formulaire /contact, reponses par emailSupportThread / SupportMessage, /admin/content/supportasynchrone, avec statut et historique
feedback produitFeedback, /admin/content/feedbacksboucle roadmap → changelog
candidatures partenairesNetworkApplication, /admin/revenue/network-applicationslead 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. openSupportThread ecrit avant sendContactMessage. Si la base est indisponible, la route retombe sur l'ancien contrat (mail avec le visiteur en Reply-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 en Reply-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.providerMessageId est 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

CleRoleSans elle
RESEND_SUPPORT_DOMAINdomaine d'envoi ET de reception du deskrepli sur l'apex : l'envoi marche, les reponses client atterrissent dans une boite mail
RESEND_WEBHOOK_SECRETsignature Svix du webhook, deja en placetout 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.