Skip to content

feat(infra): send Cognito mail from no-reply@branchaccounting.org - #419

Merged
nourshoreibah merged 3 commits into
mainfrom
feat/ses-email
Oct 17, 2026
Merged

nourshoreibah merged 3 commits into
mainfrom
feat/ses-email

Conversation

@nourshoreibah

@nourshoreibah nourshoreibah commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

What

Flips enable_ses_email to true. Cognito's email_configuration changes from COGNITO_DEFAULT (shared no-reply@verificationemail.com, capped at 50 a day, can't authenticate for our domain, so mail lands in spam) to DEVELOPER mode sending as BRANCH <no-reply@branchaccounting.org> through our SES identity.

Preconditions (verified 2026-10-17, account 404813129370, us-east-2)

Check State
SES production access GRANTED (case 179201951700548), 50k/day, 14/s
branchaccounting.org identity verified, DKIM SUCCESS (Easy DKIM), MAIL FROM mail.branchaccounting.org SUCCESS
Account-level suppression list BOUNCE, COMPLAINT
Region SES identity and user pool are both in us-east-2 (supported pairing)

DEVELOPER mode sends through the AWSServiceRoleForAmazonCognitoIdp service-linked role. It doesn't exist yet: Cognito creates it during the apply, using branch-ci-apply (admin, so it has iam:CreateServiceLinkedRole). No SES identity policy is needed; that's only for COGNITO_DEFAULT with a custom From address. (AWS docs)

Compliance (internal, transactional-only tool)

  • Mail type: invitations, verification codes, password resets and expense approve/deny notices only. Recipients are staff entered by an admin; the pool is admin-create-only.
  • No unsubscribe link: CAN-SPAM exempts transactional and relationship messages from opt-out and postal-address rules, and Gmail/Yahoo require one-click unsubscribe only for marketing mail.
  • Authentication: SPF (include:amazonses.com) on the apex and mail., DKIM signed d=branchaccounting.org, DMARC published (p=none). Both DKIM and SPF (through the mail. subdomain, relaxed) align with the From domain, which covers the Gmail/Yahoo/Microsoft bulk-sender rules even though we're far below their 5,000/day threshold.
  • Bounces and complaints: the account-level suppression list stops repeat sends; SES reputation metrics are on by default in the console and CloudWatch.

Also: expense approve/deny emails (SES_FROM_ADDRESS)

The expenditures lambda emails the submitter when an expense is approved or denied (apps/backend/lambdas/expenditures/mailer.ts, sent directly through SES, not Cognito). It reads its sender from SES_FROM_ADDRESS, which Terraform never set, so it fell back to no-reply@branch.org. Every send failed, and the handler's try/catch swallowed the error. From branch-expenditures logs, 2026-10-15:

Failed to send status email: MessageRejected: Email address is not verified. The following identities failed the check in region US-EAST-2: no-reply@branch.org, shoreibah.n@northeastern.edu

The recipient half was the sandbox, which production access fixed. The sender half is fixed here: lambda.tf sets SES_FROM_ADDRESS = "BRANCH <no-reply@branchaccounting.org>" on all six functions (the env block is shared; preview stacks copy it). The lambda role already has ses:SendEmail. No code change or lambda redeploy needed: the apply updates the env and the next invocation picks it up.

Plan should show

Plan: 1 to add, 7 to change, 0 to destroy: aws_ses_domain_identity_verification.app[0] created, aws_cognito_user_pool.branch_user_pool (email_configuration) and the six aws_lambda_function.functions[*] (+ SES_FROM_ADDRESS) updated in place.

After merge

Approve the terraform-apply run, then send a test invite or password reset and approve or deny a test expense, and confirm both emails arrive from BRANCH <no-reply@branchaccounting.org> and the headers show dkim=pass, spf=pass, dmarc=pass.

🤖 Generated with Claude Code

nourshoreibah and others added 2 commits October 17, 2026 07:29
us-east-2 was granted SES production access (case 179201951700548) and the
branchaccounting.org identity reads DKIM and MAIL FROM Verified, so flip
enable_ses_email. Cognito switches from COGNITO_DEFAULT (shared
no-reply@verificationemail.com, 50/day, unauthenticated for our domain) to
DEVELOPER mode sending as "BRANCH <no-reply@branchaccounting.org>", which is
DKIM-signed and DMARC-aligned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
  - Auto-formatted .tf files with terraform fmt
  - Updated README.md with terraform-docs

  Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com>
@nourshoreibah
nourshoreibah marked this pull request as ready for review October 17, 2026 11:32
@github-actions
github-actions Bot requested review from Rayna-Yu and mehanana October 17, 2026 11:32
github-actions Bot added a commit that referenced this pull request Oct 17, 2026
mailer.ts sends approve/deny notices from process.env.SES_FROM_ADDRESS, which
no Terraform ever set, so it fell back to no-reply@branch.org. SES rejected
every send ("Email address is not verified ... no-reply@branch.org", seen in
branch-expenditures logs on 2026-10-15) and the handler's try/catch swallowed
it, so submitters never got a notice. Point it at the same verified
"BRANCH <no-reply@branchaccounting.org>" sender Cognito now uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
aws_cloudwatch_log_group.lambda["reports"]: Refreshing state... [id=/aws/lambda/branch-reports]
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E1ZI46GY0YEFAD]
aws_cloudwatch_log_group.lambda["expenditures"]: Refreshing state... [id=/aws/lambda/branch-expenditures]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::404813129370:oidc-provider/token.actions.githubusercontent.com]
aws_cloudwatch_log_group.lambda["projects"]: Refreshing state... [id=/aws/lambda/branch-projects]
aws_cloudwatch_log_group.lambda["users"]: Refreshing state... [id=/aws/lambda/branch-users]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20260830181426405600000001]
aws_cloudwatch_log_group.lambda["auth"]: Refreshing state... [id=/aws/lambda/branch-auth]
aws_cloudwatch_log_group.lambda["donors"]: Refreshing state... [id=/aws/lambda/branch-donors]
data.aws_caller_identity.current: Reading...
data.aws_route53_zone.app[0]: Reading...
data.aws_region.current: Reading...
data.aws_region.current: Read complete after 0s [id=us-east-2]
aws_acm_certificate.api[0]: Refreshing state... [id=arn:aws:acm:us-east-2:404813129370:certificate/836032c9-c55f-461a-b4fb-8e5d76fae957]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
aws_dsql_cluster.branch: Refreshing state...
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=btt3bl5139]
aws_ses_domain_identity.app[0]: Refreshing state... [id=branchaccounting.org]
data.aws_caller_identity.current: Read complete after 0s [id=404813129370]
data.infisical_secrets.sentry_folder: Reading...
aws_acm_certificate.frontend[0]: Refreshing state... [id=arn:aws:acm:us-east-1:404813129370:certificate/ff647d49-05a1-4ca4-a652-dc48faec1688]
data.infisical_secrets.sentry_folder: Read complete after 0s
data.infisical_secrets.grafana_folder: Reading...
data.infisical_secrets.grafana_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=1050147292]
data.aws_iam_policy_document.ci_preview_assume: Reading...
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=245163413]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3606114350]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=3235391464]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-404813129370]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-404813129370]
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
data.aws_route53_zone.app[0]: Read complete after 1s [id=Z05958263TSNBU4JSXID1]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
aws_ses_domain_mail_from.app[0]: Refreshing state... [id=branchaccounting.org]
aws_ses_domain_dkim.app[0]: Refreshing state... [id=branchaccounting.org]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-btt3bl5139-DEFAULT_4XX]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-btt3bl5139-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=r6frgh]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=ooaugc]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=x3f6cx]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=j2bjjp]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=5rlpdk]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=fbius2]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_dsql_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/AmazonAuroraDSQLReadOnlyAccess]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20260830181426405600000001]
aws_iam_role_policy.lambda_ses_send: Refreshing state... [id=branch-lambda-role:branch-lambda-ses-send]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_iam_role_policy.lambda_dsql_connect: Refreshing state... [id=branch-lambda-role:branch-lambda-dsql-connect]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_route53_record.dmarc[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1__dmarc.branchaccounting.org_TXT]
aws_route53_record.ses_mail_from_spf[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1_mail.branchaccounting.org_TXT]
aws_route53_record.api_cert_validation["api.branchaccounting.org"]: Refreshing state... [id=Z05958263TSNBU4JSXID1__4fac00187cd968edbe71cbc7664d4601.api.branchaccounting.org._CNAME]
aws_route53_record.spf[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1_branchaccounting.org_TXT]
aws_route53_record.ses_dkim[1]: Refreshing state... [id=Z05958263TSNBU4JSXID1_yka5xbfuragci7thfnkbnpsibjvkkbu7._domainkey.branchaccounting.org_CNAME]
aws_route53_record.ses_dkim[2]: Refreshing state... [id=Z05958263TSNBU4JSXID1_4ngjy5faumusypdxako3jdie73lihmud._domainkey.branchaccounting.org_CNAME]
aws_route53_record.ses_dkim[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1_um5rslhbpotxdh2ojg7ho3ajwkz4ouom._domainkey.branchaccounting.org_CNAME]
aws_route53_record.frontend_cert_validation["branchaccounting.org"]: Refreshing state... [id=Z05958263TSNBU4JSXID1__6e4f9a0f55e3f295054f4aebd1cde0c8.branchaccounting.org._CNAME]
aws_route53_record.ses_verification[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1__amazonses.branchaccounting.org_TXT]
aws_route53_record.ses_mail_from_mx[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1_mail.branchaccounting.org_MX]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=37l1nw]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=2haqg7]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=0s4etn]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=7tjm3k]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=qpfkcg]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=zofyad]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-btt3bl5139-r6frgh-DELETE]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-btt3bl5139-r6frgh-GET]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-btt3bl5139-j2bjjp-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-btt3bl5139-ooaugc-GET]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-btt3bl5139-5rlpdk-POST]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-btt3bl5139-fbius2-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-POST"]: Refreshing state... [id=agm-btt3bl5139-ooaugc-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-btt3bl5139-5rlpdk-OPTIONS]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-btt3bl5139-r6frgh-POST]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-btt3bl5139-x3f6cx-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-btt3bl5139-ooaugc-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-btt3bl5139-x3f6cx-PATCH]
aws_api_gateway_method.lambda_methods["reports-POST"]: Refreshing state... [id=agm-btt3bl5139-fbius2-POST]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-btt3bl5139-x3f6cx-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-btt3bl5139-j2bjjp-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-btt3bl5139-fbius2-GET]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-btt3bl5139-5rlpdk-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-btt3bl5139-x3f6cx-POST]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-btt3bl5139-j2bjjp-GET]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-btt3bl5139-r6frgh-OPTIONS]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-btt3bl5139-r6frgh-PATCH]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-404813129370]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-404813129370]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-404813129370]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-404813129370/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-404813129370/users/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-404813129370/projects/initial.zip]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-404813129370/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-404813129370/reports/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-404813129370/auth/initial.zip]
aws_acm_certificate_validation.api[0]: Refreshing state... [id=2026-10-14 22:54:35.419 +0000 UTC]
aws_acm_certificate_validation.frontend[0]: Refreshing state... [id=2026-10-14 22:54:31.121 +0000 UTC]
aws_api_gateway_method.cors_proxy_options["expenditures"]: Refreshing state... [id=agm-btt3bl5139-2haqg7-OPTIONS]
aws_api_gateway_method.cors_proxy_options["auth"]: Refreshing state... [id=agm-btt3bl5139-7tjm3k-OPTIONS]
aws_api_gateway_method.cors_proxy_options["donors"]: Refreshing state... [id=agm-btt3bl5139-37l1nw-OPTIONS]
aws_api_gateway_method.cors_proxy_options["reports"]: Refreshing state... [id=agm-btt3bl5139-qpfkcg-OPTIONS]
aws_api_gateway_method.cors_proxy_options["projects"]: Refreshing state... [id=agm-btt3bl5139-zofyad-OPTIONS]
aws_api_gateway_method.cors_proxy_options["users"]: Refreshing state... [id=agm-btt3bl5139-0s4etn-OPTIONS]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-btt3bl5139-0s4etn-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-btt3bl5139-2haqg7-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-btt3bl5139-7tjm3k-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-btt3bl5139-zofyad-ANY]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-btt3bl5139-qpfkcg-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-btt3bl5139-37l1nw-ANY]
aws_api_gateway_domain_name.api[0]: Refreshing state... [id=api.branchaccounting.org]
aws_cloudfront_distribution.frontend: Refreshing state... [id=EOTKQTE3WUELO]
aws_api_gateway_integration.cors["auth"]: Refreshing state... [id=agi-btt3bl5139-j2bjjp-OPTIONS]
aws_api_gateway_integration.cors["expenditures"]: Refreshing state... [id=agi-btt3bl5139-x3f6cx-OPTIONS]
aws_api_gateway_integration.cors["auth-proxy"]: Refreshing state... [id=agi-btt3bl5139-7tjm3k-OPTIONS]
aws_api_gateway_integration.cors["reports"]: Refreshing state... [id=agi-btt3bl5139-fbius2-OPTIONS]
aws_api_gateway_integration.cors["users-proxy"]: Refreshing state... [id=agi-btt3bl5139-0s4etn-OPTIONS]
aws_api_gateway_integration.cors["donors"]: Refreshing state... [id=agi-btt3bl5139-ooaugc-OPTIONS]
aws_api_gateway_integration.cors["reports-proxy"]: Refreshing state... [id=agi-btt3bl5139-qpfkcg-OPTIONS]
aws_api_gateway_integration.cors["projects"]: Refreshing state... [id=agi-btt3bl5139-5rlpdk-OPTIONS]
aws_api_gateway_integration.cors["expenditures-proxy"]: Refreshing state... [id=agi-btt3bl5139-2haqg7-OPTIONS]
aws_api_gateway_integration.cors["donors-proxy"]: Refreshing state... [id=agi-btt3bl5139-37l1nw-OPTIONS]
aws_api_gateway_integration.cors["projects-proxy"]: Refreshing state... [id=agi-btt3bl5139-zofyad-OPTIONS]
aws_api_gateway_integration.cors["users"]: Refreshing state... [id=agi-btt3bl5139-r6frgh-OPTIONS]
aws_api_gateway_method_response.cors["reports"]: Refreshing state... [id=agmr-btt3bl5139-fbius2-OPTIONS-200]
aws_api_gateway_method_response.cors["donors"]: Refreshing state... [id=agmr-btt3bl5139-ooaugc-OPTIONS-200]
aws_api_gateway_method_response.cors["donors-proxy"]: Refreshing state... [id=agmr-btt3bl5139-37l1nw-OPTIONS-200]
aws_api_gateway_method_response.cors["expenditures"]: Refreshing state... [id=agmr-btt3bl5139-x3f6cx-OPTIONS-200]
aws_api_gateway_method_response.cors["users"]: Refreshing state... [id=agmr-btt3bl5139-r6frgh-OPTIONS-200]
aws_api_gateway_method_response.cors["auth"]: Refreshing state... [id=agmr-btt3bl5139-j2bjjp-OPTIONS-200]
aws_api_gateway_method_response.cors["users-proxy"]: Refreshing state... [id=agmr-btt3bl5139-0s4etn-OPTIONS-200]
aws_api_gateway_method_response.cors["projects"]: Refreshing state... [id=agmr-btt3bl5139-5rlpdk-OPTIONS-200]
aws_api_gateway_method_response.cors["projects-proxy"]: Refreshing state... [id=agmr-btt3bl5139-zofyad-OPTIONS-200]
aws_api_gateway_method_response.cors["expenditures-proxy"]: Refreshing state... [id=agmr-btt3bl5139-2haqg7-OPTIONS-200]
aws_api_gateway_method_response.cors["reports-proxy"]: Refreshing state... [id=agmr-btt3bl5139-qpfkcg-OPTIONS-200]
aws_api_gateway_method_response.cors["auth-proxy"]: Refreshing state... [id=agmr-btt3bl5139-7tjm3k-OPTIONS-200]
aws_route53_record.api[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1_api.branchaccounting.org_A]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1913669945]
aws_route53_record.frontend[0]: Refreshing state... [id=Z05958263TSNBU4JSXID1_branchaccounting.org_A]
aws_s3_bucket_cors_configuration.reports_bucket_cors: Refreshing state... [id=c4c-branch-generated-reports20260830181426405600000001]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-404813129370]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_ES8vlp7b4]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=26r3n4d9ttjp6fvhdg1erd2eli]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["donors-POST"]: Refreshing state... [id=agi-btt3bl5139-ooaugc-POST]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-btt3bl5139-ooaugc-GET]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-btt3bl5139-5rlpdk-GET]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-btt3bl5139-r6frgh-PATCH]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-btt3bl5139-x3f6cx-PATCH]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-btt3bl5139-j2bjjp-GET]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-btt3bl5139-x3f6cx-GET]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-btt3bl5139-j2bjjp-POST]
aws_api_gateway_integration.lambda_integrations["reports-POST"]: Refreshing state... [id=agi-btt3bl5139-fbius2-POST]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-btt3bl5139-r6frgh-GET]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-btt3bl5139-r6frgh-DELETE]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-btt3bl5139-x3f6cx-POST]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-btt3bl5139-5rlpdk-POST]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-btt3bl5139-r6frgh-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-btt3bl5139-fbius2-GET]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-btt3bl5139-0s4etn-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-btt3bl5139-7tjm3k-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-btt3bl5139-37l1nw-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-btt3bl5139-2haqg7-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-btt3bl5139-zofyad-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-btt3bl5139-qpfkcg-ANY]
aws_api_gateway_integration_response.cors["users-proxy"]: Refreshing state... [id=agir-btt3bl5139-0s4etn-OPTIONS-200]
aws_api_gateway_integration_response.cors["projects"]: Refreshing state... [id=agir-btt3bl5139-5rlpdk-OPTIONS-200]
aws_api_gateway_integration_response.cors["expenditures-proxy"]: Refreshing state... [id=agir-btt3bl5139-2haqg7-OPTIONS-200]
aws_api_gateway_integration_response.cors["reports-proxy"]: Refreshing state... [id=agir-btt3bl5139-qpfkcg-OPTIONS-200]
aws_api_gateway_integration_response.cors["donors-proxy"]: Refreshing state... [id=agir-btt3bl5139-37l1nw-OPTIONS-200]
aws_api_gateway_integration_response.cors["reports"]: Refreshing state... [id=agir-btt3bl5139-fbius2-OPTIONS-200]
aws_api_gateway_integration_response.cors["donors"]: Refreshing state... [id=agir-btt3bl5139-ooaugc-OPTIONS-200]
aws_api_gateway_integration_response.cors["users"]: Refreshing state... [id=agir-btt3bl5139-r6frgh-OPTIONS-200]
aws_api_gateway_integration_response.cors["expenditures"]: Refreshing state... [id=agir-btt3bl5139-x3f6cx-OPTIONS-200]
aws_api_gateway_integration_response.cors["projects-proxy"]: Refreshing state... [id=agir-btt3bl5139-zofyad-OPTIONS-200]
aws_api_gateway_integration_response.cors["auth-proxy"]: Refreshing state... [id=agir-btt3bl5139-7tjm3k-OPTIONS-200]
aws_api_gateway_integration_response.cors["auth"]: Refreshing state... [id=agir-btt3bl5139-j2bjjp-OPTIONS-200]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=pdxko8]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-btt3bl5139-prod]
aws_api_gateway_base_path_mapping.api[0]: Refreshing state... [id=api.branchaccounting.org/]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # aws_cognito_user_pool.branch_user_pool will be updated in-place
  ~ resource "aws_cognito_user_pool" "branch_user_pool" {
        id                         = "us-east-2_ES8vlp7b4"
        name                       = "branch-user-pool"
        tags                       = {
            "Environment" = "development"
            "ManagedBy"   = "terraform"
        }
        # (18 unchanged attributes hidden)

      ~ email_configuration {
          ~ email_sending_account  = "COGNITO_DEFAULT" -> "DEVELOPER"
          + from_email_address     = "BRANCH <no-reply@branchaccounting.org>"
          + source_arn             = (known after apply)
            # (2 unchanged attributes hidden)
        }

        # (9 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["auth"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-auth"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "SES_FROM_ADDRESS"            = "BRANCH <no-reply@branchaccounting.org>"
                # (13 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["donors"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-donors"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "SES_FROM_ADDRESS"            = "BRANCH <no-reply@branchaccounting.org>"
                # (13 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["expenditures"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-expenditures"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "SES_FROM_ADDRESS"            = "BRANCH <no-reply@branchaccounting.org>"
                # (13 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["projects"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-projects"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "SES_FROM_ADDRESS"            = "BRANCH <no-reply@branchaccounting.org>"
                # (13 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["reports"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-reports"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "SES_FROM_ADDRESS"            = "BRANCH <no-reply@branchaccounting.org>"
                # (13 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["users"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-users"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "SES_FROM_ADDRESS"            = "BRANCH <no-reply@branchaccounting.org>"
                # (13 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_ses_domain_identity_verification.app[0] will be created
  + resource "aws_ses_domain_identity_verification" "app" {
      + arn    = (known after apply)
      + domain = "branchaccounting.org"
      + id     = (known after apply)
      + region = "us-east-2"
    }

Plan: 1 to add, 7 to change, 0 to destroy.

Changes to Outputs:
  + ses_from_address                    = "no-reply@branchaccounting.org"

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Pushed by: @nourshoreibah, Action: pull_request

@nourshoreibah nourshoreibah added the no-review The PR review bot won't run label Oct 17, 2026
@nourshoreibah
nourshoreibah merged commit ea438ab into main Oct 17, 2026
17 checks passed
github-actions Bot added a commit that referenced this pull request Oct 17, 2026
@nourshoreibah
nourshoreibah deleted the feat/ses-email branch October 17, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-review The PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant