logicspike/docs

Communication

Communication Service — Backlog & Scorecard

Last updated: 2026-07-04 (Flows modernization + full revamp shipped) Analyst: Codebase audit (full source + spec + test review)

⚠️ Much of the original backlog below is now RESOLVED. The service was made a full Vlozi Flows citizen and then went through a 4-phase revamp (correctness → hardening → deliverability → quality). See Revamp Status immediately below for what's fixed vs. still open before reading the (historical) Bugs / Areas of Improvement sections.


Revamp Status (2026-07)

Flows modernization (69b2b99): comms emits comms.email.sent/failed/delivered/ bounced/complained + comms.alias.claimed; exposes comms.* MCP tools (send_message, claim_alias, list_logs, get_analytics, check_alias_availability, get/update_sender_settings) + 6 trigger descriptors; on the mcp-gateway.

Revamp — Phase 1 correctness (4272f73), Phase 2 hardening (d86b05d), Phase 3 deliverability (5d55bf8), Phase 4 quality:

Backlog item Status
B1 unverified fromEmail silent 403 Fixed — pre-dispatch DOMAIN_NOT_VERIFIED (422) guardrail (verified-domain set, env-overridable). Full self-serve Resend Domains flow still deferred.
B2 no retry FixedsendViaResend bounded retry on 429/5xx/network + Resend Idempotency-Key so retries can't double-send.
B3 webhook replay double-counts Fixed — dedup on the Svix event id (provider_event_id unique) + replay-window + best-effort side-effects on redelivery.
B4 messageEvents no FK / mislabel Open (documented)message_id holds the provider id, not an FK; comment corrected; column rename is a Phase-3 migration candidate.
B5 SMS fake "sent" Fixed — honest 501.
B6 sender_email not validated Fixed.email() on sender/from; recipient .email() on the email channel.
B7 db handle cached by URL Fixed (guard)getCoreDb asserts CORE_DATABASE_URL != COMMS_DATABASE_URL. Full core-schema rebind deferred.
B8 alias race 409 UX Open — server race-safe; dashboard UX note only.
A1 rate limiting Fixed — native SEND_LIMITER on /v1/send.
A2 backoff + DLQ Partial — retry shipped; DLQ deferred (account at Cloudflare Queues limit; code binding-guarded).
A3 domain verification Partial — guardrail shipped (B1); full self-serve flow deferred.
A4 webhook dedup Fixed (see B3).
A5 audit log Open.
A6 enrich /v1/logs with events Fixed/v1/logs + MCP list_logs join the latest delivery outcome; new GET /v1/stats + MCP get_analytics.
A7 SMS via Twilio Fixed as 501 — honest rejection; real Twilio deferred.
(new) suppression list Fixedcomms_suppression (migration 0005): webhook records hard-bounce/complaint; dispatchSend short-circuits to a suppressed status.
(new) onError exception leak Fixed — generic message; stack logged server-side.
(new) Svix timing oracle / replay Fixed — constant-time compare + ±5min timestamp window.
(new) input size caps Fixed — html/text/metadata caps; content refine; emailHeaders reserved-key guard.
(new) idempotent-replay over-bill Fixed — cached hit returns deduped; accumulator skipped.

Still open / deferred: full self-serve domain verification (Resend Domains API + tenant_sending_domains), real SMS/WhatsApp + multi-provider/BYOP, batch + scheduled sends, audit log (A5), DLQ (queue-limit-blocked), List-Unsubscribe defaulting (needs a routed unsubscribe mailbox), the fuller index.ts routes/services split (pure leaf modules lib/resend.ts + lib/svix.ts + lib/text.ts are extracted).


Scorecard

# Parameter Score Notes
1 Testing 9.5 73 test cases across 7 files; cross-tenant isolation, idempotency, alias race condition, gateway key rotation, Svix verification all covered
2 Multi-tenancy 9.5 All queries scoped by tenant_id; alias uniqueness is global across tenants; tests formally prove cross-tenant non-leakage
3 Billing Integration 8.5 Credit gate on /v1/send; accumulator pattern (200 thousandths/email = 5 emails per credit); alias claim charges 20 credits with idempotency key + refund on race
4 Business Logic 8.5 Alias claim with credit charge + unique constraint race-safety + loser-refund; send idempotency; Svix webhook fan-out to newsletter (engagement + bounce kill-switch)
5 Infrastructure 8.5 Service binding to newsletter worker (trusted path); dual Neon databases (comms + core); Workers-compatible Svix HMAC without SDK
6 Data Model 8.5 Clean 3-table schema (logs, settings, events); proper indexes on all filter columns; event table decoupled from log table
7 API Design 8.0 Consistent Hono + Zod patterns; correct HTTP status codes; partial implementation (alias + sender settings + send + webhook + logs)
8 Performance 8.0 Edge Worker execution; indexed idempotency key lookup; idempotency short-circuit avoids re-calling Resend
9 Security 7.5 Gateway key rotation (comma-separated); Svix HMAC-SHA256 verification; INTERNAL_KEY for sibling workers; no rate limiting; no audit log
10 Error Handling 7.0 Hono global error middleware; Zod parse errors surface cleanly; no retry logic — Resend network failure = permanent failure
11 Feature Completeness 6.0 Core send + sender settings + alias + webhook + logs working; SMS is console stub; no batch, no multi-provider, no scheduled sends, no domain verification
12 Observability 5.5 comms_message_logs provides operational history; comms_message_events records delivery feedback; no metrics, no tracing, no alerting
13 Provider Integration 5.0 Resend only (email); console fallback; SMS acknowledged but never delivered; no BYOP, no provider management API
14 Documentation 5.0 8 internal design docs; api-spec and domain-model are aspirational/stale; implementation-plan is 40% accurate; no public docs

Overall: 7.5 / 10


Bugs

B1 — Unverified fromEmail silently fails at Resend with no user explanation

Severity: High Location: src/index.tsdispatchSend(), PUT /v1/sender-settings

The fromEmail field accepts any value via PUT /v1/sender-settings (no verification step). When this address is used in a send, Resend returns a 403 "domain not verified" response. The service logs the error, marks the message failed, and returns a 500 to the caller — but no user-facing explanation distinguishes "domain not verified" from "network error". Users who configure fromEmail and then see failed sends have no visibility into the actual cause.


B2 — No retry logic: transient Resend failures permanently fail the message

Severity: High Location: src/index.tssendViaResend()

A single Resend API call attempt is made. Any network error, timeout, or Resend 5xx immediately marks the message status: "failed" with no retry. There is no queue, no exponential backoff, no dead-letter path. Transient failures (Resend degraded, Worker CPU timeout) are permanent from the caller's perspective.


B3 — Webhook replay creates duplicate messageEvents and double-counts engagement

Severity: Medium Location: src/index.tsPOST /v1/webhooks/resend

Svix replays failed webhook deliveries automatically. Each replay results in a new row in comms_message_events and a second call to NEWSLETTER_SERVICE /internal/event. There is no deduplication on (message_id, status) before insert. An email that is "delivered" and then the webhook is replayed → newsletter sees two engagement events for the same send, inflating opens/clicks metrics.


B4 — messageEvents has no FK to messageLogs (documented as "not enforced")

Severity: Low Location: src/db/schema.ts, migration 0000_warm_winter_soldier.sql

comms_message_events.message_id references comms_message_logs.id in intent but not via DB constraint. A webhook for an unknown or deleted providerMessageId inserts silently. No orphan cleanup, no alert. Over time the events table can accumulate orphan rows that degrade query performance and confuse log viewers.


B5 — SMS channel returns status: "sent" but never sends anything

Severity: Medium (feature is deceptive) Location: src/index.tsdispatchSend() SMS branch

When channel: "sms" is posted, the service logs to console and marks status: "sent". No Twilio call is made. The billing accumulator is NOT charged (accumulator only runs for email), but the call returns 200 OK. Callers (e.g., notification triggers from CI) believe the SMS was delivered. No error is surfaced.


B6 — senderEmail not validated as a syntactically valid email

Severity: Low Location: src/index.tsPUT /v1/sender-settings

The Zod schema for sender_email uses z.string().optional() — no .email() validator. A tenant can store "not an email at all" as their reply-to. Resend may silently strip or reject the reply_to header, causing customer replies to bounce.


B7 — DB handle cached by URL only — shared handle if two env vars collide

Severity: Low Location: src/db/client.ts

The drizzle client cache key is COMMS_DATABASE_URL. If a misconfigured deployment sets two workers' COMMS_DATABASE_URL to the same string (e.g., a copy-paste error), they share a drizzle instance within the same Worker isolate. In tests, each test calls freshEnv() to bust the cache — production has no equivalent guard.


B8 — Alias check-then-claim is non-atomic; 409 on race not documented in dashboard UX

Severity: Low Location: src/index.tsGET /v1/aliases/availability, POST /v1/aliases/claim

The availability check and claim are two separate requests with a window between them. Another tenant can claim the slug after the check returns available: true but before the claim is submitted. The race is handled correctly at the DB level (unique constraint → 409 + credit refund), but the 409 is not documented as a possible response in the dashboard UI — users may see an unhandled error state.


Areas of Improvement

A1 — Rate limiting absent on /v1/send

A tenant can POST /v1/send in a tight loop, exhausting the Worker's Resend quota and incurring large credit bills (or hammering the billing accumulator). Native Cloudflare rate limiting (ratelimit binding) should be added with a per-tenant budget (e.g., 60 sends/min).


A2 — Implement exponential backoff + DLQ for Resend failures

Network errors and Resend 5xx responses should be retried with 1s/5s/15s delays via Cloudflare Queues. After 3 attempts, move to DLQ and mark the log status: "failed". This matches the pattern used in content-engine and newsletter services.


A3 — Domain verification flow missing

fromEmail is stored but there is no guided verification flow. The service should expose a POST /v1/sender-settings/verify-domain that calls the Resend domain verification API and surfaces the DNS records the tenant must add. Without this, fromEmail is a footgun.


A4 — Add messageEvents deduplication (idempotency on webhook)

Before inserting into comms_message_events, check if (message_id, status) already exists. Skip insert and newsletter fan-out if so. This makes webhook processing idempotent against Svix retries.


A5 — Add audit log for sensitive operations

Alias release and sender settings changes have no audit trail. A comms_audit_log table with (tenantId, action, actorId, payload, timestamp) rows would allow support to trace who changed what and when — especially important for "my alias was released and claimed by someone else" disputes.


A6 — Enrich /v1/logs with delivery event data

The current /v1/logs response shows the initial send status but not subsequent delivery events (delivered, bounced, opened). The UI has no way to show "email delivered at 10:05am". A JOIN or sub-select from comms_message_events would make the log view much more useful.


A7 — Implement Twilio for SMS

The SMS code path is a console stub. Either implement Twilio with the existing architecture (apiKey in env, same dispatchSend structure) or return 501 Not Implemented clearly so callers don't believe SMS was delivered.


Feature Completeness vs Spec

Feature Spec Code Status
Email send (Resend) ✅ Implemented
Send idempotency ✅ Implemented
Sender settings + brand kit ✅ Implemented
Vlozi alias claim/release ✅ Implemented
Svix delivery webhooks ✅ Implemented
Newsletter fan-out on events ✅ Implemented
Message logs ✅ Implemented
Delivery analytics (/v1/stats, get_analytics) ✅ Implemented (revamp P3)
Suppression list ✅ Implemented (revamp P3, migration 0005)
Flows triggers + MCP tools ✅ Implemented (Flows modernization)
SMS (Twilio) 🟡 Honest 501 (real Twilio deferred)
Multi-provider abstraction ✅ (ADR) ❌ Not implemented
Tenant BYOP (bring your own provider) ✅ (adr-providers.md) ❌ Not implemented
Provider management API (/v1/providers) ✅ (api-spec.md) ❌ Not implemented
Domain verification flow ✅ (aws-ses-migration.md) 🟡 Guardrail only (DOMAIN_NOT_VERIFIED); full self-serve deferred
Batch send endpoint ✅ (api-spec.md) ❌ Not implemented
Scheduled sends ✅ (api-spec.md) ❌ Not implemented
Retry with backoff ✅ (domain-model.md) 🟡 In-request retry ✅; queue+DLQ deferred (queue-limit)
Rate limiting ✅ (scope-definition.md) ✅ Implemented (native SEND_LIMITER)
Communication