You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@composurecdk/ses today covers only the receiving path (email identities,
receipt rule sets, filters, actions — #279). The README says as much: "Sending-side
builders (configuration sets, dedicated IP pools, VDM, reputation alarms) will
follow." This issue is that follow-up.
A production sender needs three things the package doesn't yet give you, all of
which are currently raw aws-cdk-lib:
A configuration set — the unit that tracks every send: TLS enforcement,
reputation-metric publishing, suppression, event routing. Without one you fly
blind and can't segment transactional vs marketing reputation.
Event routing for Bounce/Complaint/Reject/… to SNS or EventBridge so a
downstream workflow can suppress bad addresses. AWS requires you to track
bounces and complaints.
Least-privilege send grants to the Lambda/task role that calls SendEmail, and an account-level reputation safety net — the Versioning strategy #1 SES footgun
is an account paused for a bounce/complaint rate over threshold.
Research
CDK L2 support for the sending path (verified against aws-cdk-lib source + the 2.216.0 floor tarball)
All sending constructs live in the AWS::SES::* CFN namespace (there is no AWS::SESv2 CFN namespace — "v2" is only the API). Present and confirmed at our ses floor (2.216.0, gated by route53):
ConfigurationSet / ConfigurationSetProps — tlsPolicy
(ConfigurationSetTlsPolicy.REQUIRE|OPTIONAL), reputationMetrics, sendingEnabled, suppressionReasons
(BOUNCES_AND_COMPLAINTS|BOUNCES_ONLY|COMPLAINTS_ONLY), disableSuppressionList, dedicatedIpPool, vdmOptions (engagementMetrics/optimizedSharedDelivery), customTrackingRedirectDomain, maxDeliveryDuration. Method addEventDestination(id, options). No grant*, no metric*, no Tags.
DedicatedIpPool / ScalingMode (STANDARD|MANAGED) and account-level VdmAttributes — both present (deferred, see Scope).
EmailIdentity — already has configurationSet?: IConfigurationSet prop, and grantSendEmail(grantee) (grants ses:SendEmail + ses:SendRawEmail on the
identity ARN) plus generic grant(grantee, ...actions). Both present at 2.216.0.
No L2 for email templates (AWS::SES::Template is L1-only) — out of scope.
Account-level sandbox→production access, sending quota, account suppression
list have no CFN resource (SDK-only) — out of scope, future domain actions
(ADR-0016).
Sending is available in all commercial regions (unlike receiving), so no
region-gating is needed for these builders.
AWS-recommended alarms & Well-Architected (SES Developer Guide — not the CloudWatch recommended-alarms doc, which omits SES)
Enforcement thresholds SES itself uses (sender reputation / account pausing):
bounce ≥5% → under review, ≥10% → may pause; complaint ≥0.1% → under
review, ≥0.5% → may pause. The recommended alarm thresholds sit at the
review boundary, giving headroom before a pause.
Critical:Reputation.BounceRate / Reputation.ComplaintRate are account-level, dimensionless metrics. An alarm on them is account-scoped —
attaching a configuration-set dimension yields an alarm that never receives data.
This drives design decision #1 below.
Well-Architected mapping: Operational Excellence (publish + alarm on reputation
metrics, route events), Reliability (bounce/complaint handling + suppression
keep you able to send), Security (least-privilege ses:SendEmail scoped by ses:FromAddress; DKIM/SPF/DMARC — DKIM already handled by the identity builder).
Exemplary AWS examples & idiomatic OSS
SESv2 SendEmail is the recommended send API; least-privilege sender policy is ses:SendEmail on the identity ARN, optionally scoped with a ses:FromAddress
StringLike condition (policy examples).
The dominant real-world shape: EmailIdentity (+DKIM) → ConfigurationSet
(TLS require, reputation metrics, VDM) → event destination (SNS/EventBridge) →
bounce/complaint Lambda → suppression list, exposing a grantSendEmail-style
method to the app role. Community constructs
(@seeebiii/ses-verify-identities, mkrn/cdk-ses-template-mailer, markilott/aws-cdk-configure-ses)
converge on this. Separate config sets (and subdomains) per stream so one
stream's reputation can't sink another; tag messages to split CloudWatch metrics.
Idioms worth mirroring: configuration-set association as a prop on the identity;
event destinations via a chainable addEventDestination(id, {destination, events})
with a destination-factory union; suppression/reputation/VDM as config-set
booleans, not separate resources.
Recommended design (for @composurecdk/ses)
import{createConfigurationSetBuilder,snsDestination,eventBusDestination,createReputationAlarmBuilder,identityGrants,createEmailIdentityBuilder,}from"@composurecdk/ses";import{EmailSendingEvent}from"aws-cdk-lib/aws-ses";import{compose,ref}from"@composurecdk/core";compose({// Where bounces/complaints go for suppression processing.events: createTopicBuilder(),// The sending configuration set — TLS + reputation metrics on by default.mailConfig: createConfigurationSetBuilder().addEventDestination("feedback",{destination: snsDestination(ref("events",(r)=>r.topic)),events: [EmailSendingEvent.BOUNCE,EmailSendingEvent.COMPLAINT,EmailSendingEvent.REJECT],}),// Identity associated with the config set so every send is tracked.identity: createEmailIdentityBuilder().domain("mail.example.com").configurationSet(ref("mailConfig",(r)=>r.configurationSet)),// The app that sends — least-privilege ses:SendEmail on the identity.sender: createFunctionBuilder().code(Code.fromAsset("sender")).grant(identityGrants.send(ref("identity",(r)=>r.emailIdentity))),// Account-level reputation safety net (bounce 5% / complaint 0.1%).reputation: createReputationAlarmBuilder(),},{events: [],mailConfig: ["events"],identity: ["mailConfig"],sender: ["identity"],reputation: [],},);
Scope for this PR: configuration-set builder + event-destination helpers +
consumer-side send grants + account reputation-alarm builder + identity↔config-set
wiring. Deferred to follow-ups: dedicated IP pool builder, account VdmAttributes
builder, SES templates (L1-only), production-access/quota/suppression-list domain
actions (SDK-only, ADR-0016). An example stack under packages/examples/ will follow
once the surface lands.
The detailed implementation plan and the key design decisions (each compared to two
alternatives) are in the comment below — those are the points I'd value your input on
before/while the PR is in review.
Problem / use case
@composurecdk/sestoday covers only the receiving path (email identities,receipt rule sets, filters, actions — #279). The README says as much: "Sending-side
builders (configuration sets, dedicated IP pools, VDM, reputation alarms) will
follow." This issue is that follow-up.
A production sender needs three things the package doesn't yet give you, all of
which are currently raw
aws-cdk-lib:reputation-metric publishing, suppression, event routing. Without one you fly
blind and can't segment transactional vs marketing reputation.
Bounce/Complaint/Reject/… to SNS or EventBridge so adownstream workflow can suppress bad addresses. AWS requires you to track
bounces and complaints.
SendEmail, and an account-level reputation safety net — the Versioning strategy #1 SES footgunis an account paused for a bounce/complaint rate over threshold.
Research
CDK L2 support for the sending path (verified against aws-cdk-lib source + the 2.216.0 floor tarball)
All sending constructs live in the
AWS::SES::*CFN namespace (there is noAWS::SESv2CFN namespace — "v2" is only the API). Present and confirmed at oursesfloor (2.216.0, gated by route53):ConfigurationSet/ConfigurationSetProps—tlsPolicy(
ConfigurationSetTlsPolicy.REQUIRE|OPTIONAL),reputationMetrics,sendingEnabled,suppressionReasons(
BOUNCES_AND_COMPLAINTS|BOUNCES_ONLY|COMPLAINTS_ONLY),disableSuppressionList,dedicatedIpPool,vdmOptions(engagementMetrics/optimizedSharedDelivery),customTrackingRedirectDomain,maxDeliveryDuration. MethodaddEventDestination(id, options). Nogrant*, nometric*, no Tags.ConfigurationSetEventDestination/EventDestination— static destinationfactories
snsTopic(ITopic),eventBus(IEventBus),cloudWatchDimensions(...),firehoseDeliveryStream(...); event filter enumEmailSendingEvent(
SEND, REJECT, BOUNCE, COMPLAINT, DELIVERY, OPEN, CLICK, RENDERING_FAILURE, DELIVERY_DELAY, SUBSCRIPTION).DedicatedIpPool/ScalingMode(STANDARD|MANAGED) and account-levelVdmAttributes— both present (deferred, see Scope).EmailIdentity— already hasconfigurationSet?: IConfigurationSetprop, andgrantSendEmail(grantee)(grantsses:SendEmail+ses:SendRawEmailon theidentity ARN) plus generic
grant(grantee, ...actions). Both present at 2.216.0.AWS::SES::Templateis L1-only) — out of scope.list have no CFN resource (SDK-only) — out of scope, future domain actions
(ADR-0016).
Sending is available in all commercial regions (unlike receiving), so no
region-gating is needed for these builders.
AWS-recommended alarms & Well-Architected (SES Developer Guide — not the CloudWatch recommended-alarms doc, which omits SES)
SES is not in the CloudWatch "Best Practice Alarm Recommendations" doc. The
authoritative source is the SES Developer Guide,
Creating reputation monitoring alarms using CloudWatch:
AWS/SES)Reputation.BounceRate>=Reputation.ComplaintRate>=Enforcement thresholds SES itself uses (sender reputation /
account pausing):
bounce ≥5% → under review, ≥10% → may pause; complaint ≥0.1% → under
review, ≥0.5% → may pause. The recommended alarm thresholds sit at the
review boundary, giving headroom before a pause.
Critical:
Reputation.BounceRate/Reputation.ComplaintRateareaccount-level, dimensionless metrics. An alarm on them is account-scoped —
attaching a configuration-set dimension yields an alarm that never receives data.
This drives design decision #1 below.
Well-Architected mapping: Operational Excellence (publish + alarm on reputation
metrics, route events), Reliability (bounce/complaint handling + suppression
keep you able to send), Security (least-privilege
ses:SendEmailscoped byses:FromAddress; DKIM/SPF/DMARC — DKIM already handled by the identity builder).Exemplary AWS examples & idiomatic OSS
SendEmailis the recommended send API; least-privilege sender policy isses:SendEmailon the identity ARN, optionally scoped with ases:FromAddressStringLike condition (policy examples).
(TLS require, reputation metrics, VDM) → event destination (SNS/EventBridge) →
bounce/complaint Lambda → suppression list, exposing a
grantSendEmail-stylemethod to the app role. Community constructs
(
@seeebiii/ses-verify-identities,mkrn/cdk-ses-template-mailer,markilott/aws-cdk-configure-ses)converge on this. Separate config sets (and subdomains) per stream so one
stream's reputation can't sink another; tag messages to split CloudWatch metrics.
event destinations via a chainable
addEventDestination(id, {destination, events})with a destination-factory union; suppression/reputation/VDM as config-set
booleans, not separate resources.
Recommended design (for
@composurecdk/ses)Scope for this PR: configuration-set builder + event-destination helpers +
consumer-side send grants + account reputation-alarm builder + identity↔config-set
wiring. Deferred to follow-ups: dedicated IP pool builder, account
VdmAttributesbuilder, SES templates (L1-only), production-access/quota/suppression-list domain
actions (SDK-only, ADR-0016). An example stack under
packages/examples/will followonce the surface lands.
The detailed implementation plan and the key design decisions (each compared to two
alternatives) are in the comment below — those are the points I'd value your input on
before/while the PR is in review.