mailshot

Subscribers

Subscriber profiles, lifecycle states, suppression, and unsubscribe handling.

A subscriber is anyone who receives emails through the system. Subscriber state is stored in a single DynamoDB table using a single-table design.

Profile

Every subscriber has a profile record:

PK: SUB#user@example.com
SK: PROFILE

Fields:

  • email — Primary identifier
  • firstName — Used in template rendering
  • attributes — Arbitrary key-value pairs (platform, country, plan, etc.)
  • unsubscribed — Boolean, set by UnsubscribeFn
  • suppressed — Boolean, set by BounceHandlerFn
  • createdAt — ISO timestamp
  • updatedAt — ISO timestamp

Upsert behavior

When a new event triggers a sequence, SendEmailFn upserts the subscriber profile. The upsert:

  • Creates the profile if it doesn't exist
  • Updates firstName and attributes if it does exist
  • Never overwrites unsubscribed or suppressed flags — only their respective handlers can set these to true

This means a subscriber who unsubscribes can't be accidentally re-subscribed by a new event.

Lifecycle states

                    ┌─────────────┐
  event arrives ──→ │   Active    │ ←── resubscribe (MCP)
                    └──────┬──────┘

              ┌────────────┼────────────┐
              ▼            ▼            ▼
      ┌──────────┐  ┌───────────┐  ┌──────────┐
      │Unsubscribed│  │ Suppressed │  │ Completed │
      └──────────┘  └───────────┘  └──────────┘

Active

Subscriber has a profile and is eligible to receive emails. May have one or more active sequence executions.

Unsubscribed

Subscriber clicked the unsubscribe link. The unsubscribed flag is set to true. All active executions are stopped. The email is added to the SES account-level suppression list. New sequence registrations are blocked and pre-send checks will skip all future emails with { sent: false, reason: "unsubscribed" }.

Suppressed

A permanent bounce or complaint was received. The suppressed flag is set to true and a SUPPRESSION record is created. All active executions are stopped. The email is added to the SES account-level suppression list. New sequence registrations are blocked and pre-send checks will skip all future emails with { sent: false, reason: "suppressed" }.

Completed

A sequence execution finished normally. The execution record is deleted. The subscriber profile remains — they can still receive future sequences or fire-and-forget emails.

Safety checks

The system prevents emails to unsubscribed or suppressed subscribers at two levels:

Registration guard

When a sequence is triggered, the register action checks the subscriber's unsubscribed and suppressed flags before creating the execution. If either flag is true, registration throws an error and the Step Functions execution fails immediately — no execution record is created, no steps run.

This prevents zombie executions that would sit in wait states doing nothing useful.

Pre-send checks

Before every email, SendEmailFn runs these checks in order:

  1. Unsubscribed — If subscriber.unsubscribed === true, return { sent: false }
  2. Suppressed — If subscriber.suppressed === true, return { sent: false }

Pre-send failures never throw. The sequence continues, the email is simply skipped. This catches cases where a subscriber unsubscribes mid-sequence — emails after the unsubscribe are skipped gracefully while the execution completes normally.

SES account-level suppression list

As a final safety net, unsubscribed and suppressed subscribers are also added to the SES account-level suppression list. This is a hard block at the AWS level that prevents email delivery even if the application-level checks are bypassed due to a bug.

The SES API only accepts BOUNCE or COMPLAINT as suppression reasons — there is no UNSUBSCRIBE option. Unsubscribes are stored as COMPLAINT (the closer of the two). This is purely a label on the suppression entry and does not count toward your SES complaint rate or affect sender reputation.

Active executions

When a sequence starts, a register action creates an execution record:

PK: SUB#user@example.com
SK: EXEC#onboarding

This tracks which sequences a subscriber is currently in. If the same sequence is triggered again for the same subscriber, the old execution is stopped and replaced.

The complete action at the end of a sequence deletes this record.

Send log

Every successful send creates a log record:

PK: SUB#user@example.com
SK: SENT#2026-03-17T10:30:00.000Z

Send logs have a 90-day TTL. They're used by the has_been_sent condition check and for auditing.

Suppression records

When BounceHandlerFn processes a permanent bounce or complaint:

PK: SUB#user@example.com
SK: SUPPRESSION

Contains the bounce/complaint type, reason, and timestamp.

Managing subscribers via MCP

The MCP server provides these subscriber tools:

ToolDescription
get_subscriberFull subscriber view: profile, executions, recent sends, suppression
list_subscribersList by status: active, unsubscribed, suppressed
update_subscriberUpdate profile attributes
delete_subscriberRemove all records from both tables
unsubscribe_subscriberMark as unsubscribed, stop executions
resubscribe_subscriberClear unsubscribed flag and suppression record

Unsubscribe tokens

Every email includes an unsubscribe link with an HMAC-SHA256 signed token:

https://<function-url>?token=<base64url-encoded>

Token format: email|sendTimestamp|expiryTimestamp|signature

  • Signed with UNSUBSCRIBE_SECRET from SSM
  • Expires 90 days after the email was sent
  • Validated by UnsubscribeFn before processing