@@ -12,6 +12,9 @@ email provider configuration.
1212 broadcasts.
1313- Email delivery through BullMQ and nodemailer, using each team's configured
1414 ESP.
15+ - Bounce and complaint webhook ingestion (Resend, Postmark) with a durable
16+ receipt inbox, canonical delivery events, and a per-workspace suppression
17+ list enforced on every send path.
1518- Open and click tracking, plus unsubscribe handling.
1619- Better Auth session login, OAuth2 bearer-token authentication, and
1720 team-scoped API key authentication.
@@ -44,20 +47,24 @@ migrations before starting the API. The API image includes:
4447- ` apps/api/drizzle` — generated migration SQL and metadata;
4548- ` apps/api/dist/db/migrate.js` — a one-shot migration runner.
4649
47- The single-server deployment runs this through a Compose ` migrate ` service:
50+ The root self-hosted deployment runs this through a Compose ` init ` service:
4851
4952` ` ` sh
50- cd single-server-setup/sendlit
51- docker compose up
53+ cp .env.example .env
54+ # Fill in the required secrets and SUPER_ADMIN_EMAIL.
55+ docker compose up --build -d
56+ docker compose logs init
5257` ` `
5358
5459Startup order:
5560
56611. Postgres starts and passes its health check.
57- 2. ` sendlit-migrate` runs ` node apps/api/dist/db/migrate.js` .
58- 3. The migration container exits successfully.
59- 4. ` sendlit-api` starts.
60- 5. ` sendlit-web` starts.
62+ 2. Redis starts and passes its health check.
63+ 3. ` init` runs ` node apps/api/dist/db/migrate.js` , then creates the configured
64+ super-admin account, default team, and one-time API key.
65+ 4. The init container exits successfully.
66+ 5. ` api` starts.
67+ 6. ` web` starts.
6168
6269This mirrors an init-container pattern. The API process itself does not apply
6370migrations on every boot; it only checks that the database is reachable. For
@@ -119,7 +126,7 @@ pnpm --filter @sendlit/api run access-token email@address.com --team team_...
119126The command prints only the access token. Use it in REST requests as:
120127
121128```sh
122- curl http://localhost:4000 /contacts \
129+ curl http://localhost:5000 /contacts \
123130 -H "Authorization: Bearer <token>" \
124131 -H "X-Sendlit-Team-Id: team_..."
125132```
@@ -188,12 +195,79 @@ Email automation runs through two loops in `src/automation/start.ts`:
188195Tag and subscriber-added automations are event-driven through
189196` automation/fire-event.ts` .
190197
191- Each team can configure its own SMTP-compatible ESP, such as SendGrid, Mailgun,
192- Postmark, SES, Resend, or a custom SMTP server. Credentials are encrypted at
193- rest with AES-256-GCM in ` src/utils/secret-crypto.ts` and are never returned to
194- clients. Campaign, broadcast, sequence, and ESP test mail require the team to
195- have an ESP configured. Platform SMTP environment variables are reserved for
196- system email such as login OTPs.
198+ Each team can configure one or more of its own SMTP-compatible ESPs (` esp_configs` ),
199+ such as SendGrid, Mailgun, Postmark, SES, Resend, or a custom SMTP server, via
200+ ` GET/POST /settings/esps` and ` GET/PATCH/DELETE /settings/esps/:espId` . One
201+ user ESP is the team' s default; a sequence, broadcast, or transactional send
202+ may pin a different one with `espId`. Credentials are encrypted at rest with
203+ AES-256-GCM in `src/utils/secret-crypto.ts` and are never returned to clients.
204+ `/settings/esp` (singular) remains as a backward-compatible alias over the
205+ team' s default user ESP.
206+
207+ Every sequence and transactional email persists an internal ` deliveryRoute`
208+ (` custom` or ` platform` ) alongside the resolved user ESP' s internal id
209+ (`outboxId`). `custom` is the only route currently reachable — it always
210+ pins a specific team-owned `esp_configs` row and is required before
211+ transactional mail can be queued or a sequence/broadcast can be marked
212+ active; `platform` is reserved for a future deployment-level SendLit-provided
213+ transport (resolved from deployment configuration, never from `esp_configs`)
214+ and isn' t exposed by any endpoint yet. User-managed/custom ESP delivery never
215+ consults or increments the account' s SendLit mail quota — those counters are
216+ reserved for the future platform route.
217+
218+ ## Bounce And Complaint Feedback
219+
220+ Full design in [`docs/bounces-and-complaints.md`](./docs/bounces-and-complaints.md).
221+ Summary of what' s implemented:
222+
223+ - ** Outbound ledger.** Every custom-route send (campaign or transactional)
224+ gets an ` outbound_messages` row before transport, carrying a generated RFC
225+ ` Message-ID` , the pinned ESP snapshot, and (once accepted) the transport' s
226+ response. `src/delivery-feedback/outbound-send.ts` /
227+ `outbound-queries.ts`.
228+ - **Feedback connections.** Each user ESP with a reviewed provider adapter
229+ (Resend, Postmark, SendGrid, and Mailgun — see `feedbackCapableProviders` in
230+ `src/config/constants.ts`) can configure its own webhook connection under
231+ `GET/PUT /settings/esps/:espId/feedback`, `POST .../feedback/rotate`,
232+ `POST .../feedback/test`, `DELETE .../feedback`. Every configuration has
233+ its own opaque `whc_...` connection id/URL, credential, and health —
234+ switching the team' s default ESP never moves or copies feedback config.
235+ Changing an ESP' s provider retires the old connection and a later `PUT`
236+ creates a new one; deleting an eligible ESP retires its connection while
237+ preserving historical events/suppressions.
238+ - **Public webhook route.** `POST /webhooks/esp/:provider/:connectionId`
239+ (`src/delivery-feedback/webhook-route.ts`) is mounted before global JSON
240+ parsing and before any session/API-key middleware — it authenticates
241+ purely via the resolved connection' s provider adapter (Svix signature for
242+ Resend, a shared secret for Postmark, ECDSA Signed Event Webhook for
243+ SendGrid, HMAC over timestamp+token for Mailgun), commits a durable,
244+ encrypted receipt (` esp_webhook_receipts` ) before returning ` 200` , and
245+ never itself normalizes the payload inline.
246+ - ** Async processing.** A BullMQ ` esp-feedback` queue/worker
247+ (` feedback-queue.ts` /` feedback-worker.ts` ) plus a recovery poller
248+ (` poller.ts` , 30s interval) drain receipts through
249+ ` process-receipt.ts` : provider adapter → canonical ` email_delivery_events`
250+ (idempotent per ` (connectionId, providerEventKey)` ) → correlate to an
251+ ` outbound_messages` row (` correlation.ts` ) → delivery-state projection
252+ (` projection.ts` ) → suppression side effects. A committed receipt is
253+ always the recovery source of truth if Redis is unavailable.
254+ - ** Suppression.** ` email_suppressions` is workspace-wide and
255+ route-independent, keyed by an HMAC (` SUPPRESSION_HASH_KEY` ) over the
256+ normalized address — never derived from ` contacts.subscribed` , so it
257+ survives contact deletion/reimport. Checked before enqueue (transactional
258+ ` 422 recipient_suppressed` ; sequence sends skip the recipient without
259+ counting it as sent) and again immediately before transport in the worker.
260+ ` GET/POST /suppressions* ` exposes the list and owner release action;
261+ complaint suppressions can only be released by a ` sendlit_operator` actor.
262+ - ** Retention.** ` retention.ts` (hourly loop) purges raw receipt payloads
263+ after 30 days and delivery events after 13 months, in bounded batches.
264+ - ** Not yet implemented:** the Amazon SES adapter (its own PRD phase,
265+ [` docs/aws-bounces-and-complaints.md` ](./docs/aws-bounces-and-complaints.md) —
266+ SNS signature/subscription handling is materially different from a direct
267+ signed webhook); a provider without an adapter is never presented as
268+ feedback-capable. Observability is limited
269+ to the existing ` captureEvent` /` captureError` posthog pattern — dedicated
270+ dashboards/alerting and load/chaos testing are deployment-level follow-ups.
197271
198272# # MCP Server
199273
@@ -215,7 +289,13 @@ OAuth client registration, authorization, token, introspection, revocation, and
215289userinfo endpoints are served by Better Auth under ` /api/auth/oauth2/* ` .
216290
217291MCP tools live in ` src/mcp/tools/* ` and cover contacts, templates, sequences,
218- ESP settings, teams, and API keys.
292+ ESP settings (both the default-ESP singleton tools and the multi-ESP
293+ collection tools — ` list_esps` /` create_esp` /` get_esp` /` update_esp` /
294+ ` delete_esp` /` test_esp` ), teams, API keys, and bounce/complaint delivery
295+ feedback (` get_esp_feedback_connection` /` upsert_esp_feedback_connection` /
296+ ` test_esp_feedback_connection` /` delete_esp_feedback_connection` ,
297+ ` list_delivery_events` /` get_delivery_event` ,
298+ ` list_suppressions` /` get_suppression` /` release_suppression` ).
219299
220300# # More Context
221301
0 commit comments