Documentation menu

Providers

AWS SES

Uses the Ahena CLI (beta). Install it with npm install -g @ahena/cli, or prefix commands with npx @ahena/cli. See Installation. The dashboard covers projects, connections, Doctor, the Stack Graph, plans and approvals without it.

Overview

Ahena checks your Amazon SES account is ready to send from your domain: production access (out of the sandbox), sending enabled, reputation status, domain verification, DKIM, custom MAIL FROM and configuration sets. It creates a missing domain identity (Easy DKIM) and configuration set, lists the exact DKIM records to publish, and generates the email adapter. Your app sends through SES directly with its own credentials. Ahena never sends email, not even a test.

AWS is not one provider in Ahena: SES and S3 are separate providers with separate connections. Ahena doesn't manage EC2, RDS, Lambda, Route 53, CloudFront, ECS, EKS or IAM.

Package: @ahena/provider-aws-ses · Category: email · API: SES v2 (email.<region>.amazonaws.com/v2/email/…, SigV4 signed with WebCrypto) Capabilities: account-status, domain-identities, dkim, configuration-sets, production-access

Maturity: verified against a fake of the API; not yet verified against the real service.

Connection

Field Secret Notes
AWS_ACCESS_KEY_ID yes Access key of a dedicated IAM user for Ahena.
AWS_SECRET_ACCESS_KEY yes Shown once when the key is created.
AWS_SESSION_TOKEN yes, optional Only for temporary credentials.
AWS_REGION no (setting) The SES Region your app sends from. Identities, quotas and production access are separate per Region.
AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=… ahena connect aws-ses -e production --set AWS_REGION=us-east-1

Supported Regions: us-east-1, us-east-2, us-west-1, us-west-2, ca-central-1, sa-east-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, ap-south-1, ap-northeast-1, ap-northeast-2, ap-northeast-3, ap-southeast-1, ap-southeast-2. Network: only email.<region>.amazonaws.com and sts.<region>.amazonaws.com for those Regions (34 exact hosts, enforced).

Permissions

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AhenaSesRead",
      "Effect": "Allow",
      "Action": [
        "ses:GetAccount",
        "ses:ListEmailIdentities",
        "ses:GetEmailIdentity",
        "ses:ListConfigurationSets",
        "ses:GetConfigurationSet",
        "ses:GetConfigurationSetEventDestinations"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AhenaSesCreate",
      "Effect": "Allow",
      "Action": ["ses:CreateEmailIdentity", "ses:CreateConfigurationSet"],
      "Resource": "*"
    },
    {
      "Sid": "AhenaNeverSends",
      "Effect": "Deny",
      "Action": ["ses:SendEmail", "ses:SendRawEmail", "ses:SendBulkEmail", "ses:SendTemplatedEmail", "ses:SendBulkTemplatedEmail"],
      "Resource": "*"
    }
  ]
}

The Deny statement is optional and makes "Ahena never sends" enforceable by AWS. You can scope AhenaSesCreate to arn:aws:ses:<region>:<account>:identity/<domain> and arn:aws:ses:<region>:<account>:configuration-set/<name>. For read-only use, leave AhenaSesCreate out. Ahena never needs delete, tag, policy or sending actions.

Permission check: checked. ahena connect calls STS GetCallerIdentity, then probes the three account-wide reads, whose IAM resource is * so the answer holds for every resource: GetAccount, ListEmailIdentities (page size 1) and ListConfigurationSets (page size 1). Each is reported granted, missing (AccessDeniedException) or :unknown (throttled or an outage); a missing one stops the connection at configuration_required. ses:GetEmailIdentity, ses:GetConfigurationSet, ses:CreateEmailIdentity and ses:CreateConfigurationSet are always reported :unknown: they can be scoped to resources, and creates can't be tested without creating something. Gaps there show as 403s in Doctor or apply, naming the action.

Capabilities

Capability Access Verification Changes Refuses
account-status read-only sending email (not even a test)
domain-identities writable read-back CONFIRMATION_REQUIRED deleting identities; email-address identities (they send a verification email); Bring Your Own DKIM keys
dkim manual
configuration-sets writable read-back CONFIRMATION_REQUIRED deleting configuration sets; changing event destinations
production-access manual

ahena inspect aws-ses shows the account (sandbox, sending enabled, enforcement status, quota, production-access request status), every identity with its verification status, and for the first 25 domains: DKIM status, the DKIM CNAME records, custom MAIL FROM and the default configuration set; and each configuration set's event destinations (name, enabled, event types, destination kind; never ARNs). Contact details from GetAccount, identity sending policies and tags are dropped. ahena lock records production access, sending enabled, the declared identity's verification/DKIM/MAIL FROM and the configuration set's destinations.

Configure

email: {
  provider: "aws-ses",
  domain: { production: "leo.example.com" },
  from: "Leo <hello@leo.example.com>",
  configurationSet: "leo",
  region: "us-east-1",   // optional: Doctor checks the connection uses this Region
},
Change Classification Notes
Create the domain identity (Easy DKIM) CONFIRMATION_REQUIRED SES generates a 2048-bit key and three DKIM tokens. Then publish the three CNAME records (MANUAL, listed by Doctor).
Create the configuration set CONFIRMATION_REQUIRED Name only; default settings, no event destinations.

DKIM records: <token>._domainkey.<domain> CNAME <token>.dkim.amazonses.com (<token>.dkim.ap-northeast-3.amazonses.com in Osaka). The existing ahena configure cloudflare --records-from flow only reads Resend's records today, so even with DNS on Cloudflare these are added by hand (or with ahena configure cloudflare --desired <file> listing them as dnsRecords).

Every apply step re-reads first and creates only if absent; SES's AlreadyExistsException (a race) counts as done. A plan made for one Region is refused after the connection's Region changes. After apply, Ahena plans again: an empty plan is the verification. Domain verification itself completes when SES sees the DNS records; Doctor tracks it.

Refused outright: delete, deleteIdentity, deleteConfigurationSet, send, testEmail, productionAccess, email-address identities.

Generate

ahena generate aws-ses writes the same email interface as the Resend adapter (same exported names and SendEmail fields), so ahena switch email resend aws-ses keeps app code:

  • src/ahena/email/index.ts: email.send({ to, subject, html?, text?, from?, replyTo? }) → { id } (the SES message id), email.raw; sends with SendEmailCommand and, when set, SES_CONFIGURATION_SET
  • src/ahena/providers/aws-ses.ts: an SESv2Client from @aws-sdk/client-sesv2 (credentials from the AWS default chain, Region from AWS_REGION)
  • .env.example.aws-ses

Give the app its own role or user allowed only ses:SendEmail on your identity.

Limitations

  • Opt-in Regions and GovCloud/China aren't supported.
  • Ahena never sends email, deletes identities or configuration sets, changes event destinations, sets up a custom MAIL FROM domain, or uses Bring Your Own DKIM.
  • DKIM records aren't added to DNS automatically, even on Cloudflare (--records-from reads only Resend's records today).
  • Production access is an AWS support request; Ahena gives the steps.
  • An existing identity's default configuration set isn't changed.
  • Dedicated IPs, suppression lists, templates, contact lists and VDM aren't managed.

Manual steps

  1. Create the IAM user and policy above.
  2. Publish the three DKIM CNAME records Doctor lists at your DNS host.
  3. Request production access (out of the sandbox) for production: SES console (your Region) → Account dashboard → Request production access → describe your use case, opt-in and bounce/complaint handling → submit. AWS answers through a support case, usually within a day.
  4. Optional: set up a custom MAIL FROM domain (SES console → Identities → your domain → Custom MAIL FROM domain) so SPF aligns for DMARC.

Doctor checks

Id Severity Meaning
aws-ses.credential PASS / FAIL STS accepts the credentials.
aws-ses.region PASS / FAIL AWS_REGION unsupported, or differs from email.region.
aws-ses.account.sandbox PASS / FAIL / INFO In the sandbox: FAIL in production (MANUAL, console steps), INFO elsewhere.
aws-ses.account.sending PASS / FAIL Sending disabled for the account in this Region.
aws-ses.account.enforcement PASS / WARNING / FAIL PROBATION (WARNING), SHUTDOWN (FAIL).
aws-ses.account.quota INFO 24-hour quota, rate, sent in the last 24 h.
aws-ses.account FAIL Account status couldn't be read (names the missing action on 403).
aws-ses.domain.undeclared INFO No email.domain, so domain checks are skipped.
aws-ses.domain PASS / FAIL Identity missing (CONFIRMATION_REQUIRED fix) or not verified.
aws-ses.domain.dkim PASS / WARNING / FAIL PENDING/TEMPORARY_FAILURE: WARNING (FAIL in production); FAILED/NOT_STARTED: FAIL. Evidence lists the exact CNAME records.
aws-ses.domain.mail_from INFO Custom MAIL FROM domain and status, or none.
aws-ses.from PASS / WARNING email.from isn't on the verified domain.
aws-ses.configuration_set PASS / WARNING / FAIL Missing (CONFIRMATION_REQUIRED fix), sending disabled on it, or present (event destinations in evidence).

Disconnect behavior

Removes the connection and the credentials Ahena stored. Ahena can't revoke an IAM access key it didn't create: deactivate and delete it in IAM → Users → the Ahena user → Security credentials. Identities, configuration sets, suppression lists and sending history are untouched.

Set up step by step

  1. Create the credential. AWS console → IAM → Users → Create user (e.g. ahena-ses, no console access) → Add permissions → Create inline policy (JSON tab: the policy above) → the user → Security credentials → Create access key → "Application running outside AWS".

  2. Minimum permissions. The six read actions; add the two creates if Ahena should create the identity and configuration set. Keep the Deny on sending.

  3. Connect.

    AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=… ahena connect aws-ses -e production --set AWS_REGION=us-east-1

    Pipe the values or type them at the hidden prompt; never pass them as arguments.

  4. Verify the connection. ahena inspect aws-ses -e production shows the account status, identities with DKIM records, and configuration sets.

  5. Run Doctor. ahena doctor -e production.

  6. Plan. Add the email section above to ahena.config.ts, then ahena plan -e production (or ahena configure aws-ses -e production).

  7. Apply an allowed change. ahena apply -e production creates the configuration set and the domain identity (each needs confirmation). Then publish the three DKIM CNAME records Doctor lists, and request production access if Doctor says the account is in the sandbox.

  8. Verify. ahena plan -e production shows no SES changes, and once DNS propagates ahena doctor -e production shows the domain verified and DKIM PASS. Then ahena generate aws-ses -e production, give the app its own ses:SendEmail credentials, and set .env.example.aws-ses's variables.

  9. Troubleshooting.

    Symptom Cause Fix
    "AWS rejected the credentials (InvalidClientTokenId)" Deleted, inactive or mistyped key Create a new access key and reconnect.
    "missing ses:ListConfigurationSets" at connect The read actions aren't in the policy Attach the policy above and reconnect.
    Doctor: "isn't an SES identity in eu-west-1" Identity created in another Region Connect with the Region you send from (--set AWS_REGION=…).
    DKIM stays pending Records not published, wrong names, or not propagated Check the full names Doctor lists; some DNS hosts append the zone, so enter only the part before your domain.
    aws-ses.account.sandbox FAIL Production access not granted in this Region Request production access (Manual steps).
    aws-ses.account.enforcement WARNING Account under review for bounces/complaints Fix the cause in SES → Reputation metrics and reply to AWS's case.
    "configuration set name" refused Spaces or other characters Use up to 64 letters, digits, - and _.
  10. Disconnect and revoke. ahena disconnect aws-ses -e production, then IAM → Users → ahena-ses → Security credentials → deactivate and delete the access key.