Tuesday, April 28, 2026

Comms Arc 2: Inbound Email + In-App Conversations Inbox

Volleyball Elite Academy development update
Volleyball Elite Academy
Comms Arc 2: Inbound Email + In-App Conversations Inbox

Comms Arc 2: Inbound Email + In-App Conversations Inbox

Volleyball Elite Academy — Development Update • April 28, 2026

Comms Arc 2: Inbound Email + In-App Conversations Inbox

What & Why

With Arc 1 done, every outbound email has one canonical sender () and invites replies. But today there's nowhere for those replies to go — they'd just sit in a mailbox no one watches. This task captures inbound email at that address, threads each reply against the original outbound that prompted it, links the sender to the family/athlete/staff record when we recognize the email address, and exposes everything as a "Conversations" view inside the Communication Hub. SuperAdmins read and reply directly in the app. Per the user's direction, v1 is app-only (no Gmail/Workspace mailbox in the loop) — part-time staff won't check a separate inbox, so the app is the single source of truth for replies. A future task can layer a Workspace mailbox on top once staff size and reply volume warrant it.

Done looks like

  • Replies sent to arrive in a new "Conversations" tab inside the Communication Hub within seconds of being sent. No external mailbox to log into.
  • Each conversation is a thread: the original outbound from the app (pulled from ) is the first message, followed by all replies in chronological order. Subsequent replies on the same thread (whether from the original recipient or a CC'd person) attach to the same thread automatically via standard email headers (, , ).
  • The thread list shows: sender (with linked family/athlete/staff name when recognized, raw email when not), subject, snippet of the latest message, last-activity timestamp, and an unread badge. Default sort is most-recent-activity-first. A filter toggles "Unread only" / "All".
  • Opening a thread shows the full message history in chronological order — outbound messages from the app rendered with the same branded layout the recipient saw, inbound replies rendered as the sender wrote them (HTML with sanitization, plain-text fallback).
  • A reply composer sits at the bottom of each thread. Sending posts through the existing Resend pipeline using the canonical sender, sets the right and headers so the recipient's mail client threads it correctly, and the new outbound is logged into both (so it shows up in the existing audit) and the conversation thread.
  • When an inbound arrives that we can match to a person (sender email matches a user, family contact, or staff record), the thread shows that person's name and a small link to their profile.
  • When we cannot match the sender, the thread still appears, labeled with just the email address; an admin can manually link it to a person in one click.
  • A small "Unread conversations" count appears on the Communication Hub nav entry so SuperAdmin sees new replies at a glance from anywhere in the app.
  • Attachments on inbound emails are detected and listed by filename + size, but not stored in v1 — the message body shows "[Attachment: filename.pdf — not stored, contact sender for a copy]". This keeps the schema simple and avoids object-storage surface area until we have a real need.
  • Inbound that fails to match any prior outbound (no header or no match found) starts a new thread rather than being dropped. Cold-start emails to are valid customer-initiated conversations.
  • An audit-trail preserved: every inbound webhook payload is stored raw (truncated to a sane size) so a future bug or a misclassified threading decision can be re-derived.

Out of scope

  • AI-drafted replies (Arc 3).
  • Storing or rendering inbound attachment bodies. v1 records the metadata only.
  • Multi-mailbox routing (e.g. vs going to different queues). One mailbox, one queue.
  • Forwarding inbound to a Workspace/Gmail mailbox in parallel. Deferred to a future task per user's preference for v1.
  • A mobile-optimized Conversations UI. Standard responsive treatment is fine; no dedicated mobile flow.
  • Search across thread bodies. v1 sorts and filters; full-text search is a follow-up.
  • Marking threads as resolved/archived/snoozed. v1 has unread/all only.
  • Notifying the SuperAdmin via email or push when a new thread arrives. v1 is the in-app badge only.

Pre-flight (admin action, not code)

  • The user configures Resend Inbound for : DNS MX records pointing inbound to Resend, and a webhook endpoint registered in the Resend dashboard pointing at the new ingest route this task adds.

Steps

1. Schema — Add an table (raw payload, parsed headers, from-email-lower, subject, text-body, html-body, message-id, in-reply-to, references, received-at, thread-id FK) and an table (subject, last-activity-at, linked-person-type/id, linked-person-display-name-cached, unread-count-for-superadmin). The first outbound message of a thread links to its row by id; subsequent inbound and outbound entries reference the same thread id. Use existing Drizzle conventions. No primary-key type changes. 2. Inbound webhook ingest — A new SuperAdmin-internal POST route that accepts Resend's inbound webhook payload, validates the signature with the inbound webhook secret env var, parses headers and bodies, threads the message (find existing thread by Message-ID/In-Reply-To match against either prior inbound message-ids or our outbound ; if no match and the inbound looks like a reply by subject prefix , fall back to subject-based matching scoped to sender; otherwise create a new thread), runs person-matching against users/family-contacts/staff, persists the inbound row, updates thread last-activity-at and unread-count, and ACKs. 3. Outbound thread linking — When the existing wrapper sends an email, it now also writes (or finds) a thread row keyed by the recipient + subject normalization, links the outbound row to that thread, and marks the thread as having app-originated activity. Replies threading back to that outbound find the same thread automatically. This also keeps the existing audit-log behavior intact — nothing about the email_log table changes. 4. Reply send — A new SuperAdmin-only POST that takes a thread id and a body, sends through the existing Resend pipeline with the canonical sender, sets to the most recent message in the thread and to the chain, logs the outbound into , and appends to the thread. 5. Conversations API — SuperAdmin-only GETs for thread list (paginated, filterable by unread, sortable by activity) and thread detail (full message history with both inbound and outbound rendered). A POST to mark a thread as read. 6. Conversations UI — New tab in the Communication Hub. Thread list on the left, thread detail on the right (or stacked on small screens). Inbound rendered through a sanitized HTML viewer (use existing sanitization utilities if present; otherwise a minimal allowlist sanitizer); outbound rendered through Arc 1's shared layout helper for visual continuity. Reply composer with send button and clear "sent as " footer. 7. Person-link UI — When a thread's sender is unmatched, show a small "Link to person" affordance that opens a search/picker tied to existing user/family/staff data. Linking persists the choice on the thread row so future inbounds from the same address auto-link. 8. Unread badge — A small numeric badge on the Communication Hub nav item, sourced from the same SuperAdmin-only endpoint that powers the thread list's unread count. 9. Tests — Vitest for: webhook signature validation rejects bad signatures, threading correctly attaches a reply to its prior outbound via Message-ID, threading falls back to subject-based matching only when scoped to the same sender, person-matching prefers exact email match in the right priority order (user > family contact > staff), reply send sets In-Reply-To and References correctly. RTL test for the thread list and thread detail rendering, inbound HTML sanitization, and reply submission. 10. Docs — Update with an "Inbound email" subsection covering the threading rules, the in-app-only architecture, and a note that the inbound webhook secret env var must be set in production via the deployment Secrets UI (never in chat).

Architectural constraints:

  • Reuse the existing pipeline for all outbound — including replies. No parallel send path. Replies must show up in exactly like any other outbound, just with the thread linkage added.
  • Webhook secret must be a separate env var from (which is for outbound delivery events) — Resend issues a different signing secret per webhook endpoint, and conflating them would silently break one of them.
  • Inbound HTML rendering must sanitize. Never render raw inbound HTML directly into the DOM. A reply that contains or styled tracking pixels must not execute or render in the admin's browser.
  • This task does not touch the existing table or the outbound webhook handler. Those keep working as-is.
  • This task depends on Arc 1 being merged so the canonical sender, reply-to, and branded layout are already in place — threading and rendering both rely on those.

Relevant files

- - - - - -

Volleyball Elite Academy

Reply to this email — we read every reply.

You received this because you have an account with Volleyball Elite Academy.

elitevolleyball.training

No comments:

Post a Comment