Skip to content

Add Resend organization invitation email notifications#579

Merged
PierreLeGuen merged 2 commits into
mainfrom
invitation-email-notifications
May 15, 2026
Merged

Add Resend organization invitation email notifications#579
PierreLeGuen merged 2 commits into
mainfrom
invitation-email-notifications

Conversation

@PierreLeGuen
Copy link
Copy Markdown
Contributor

@PierreLeGuen PierreLeGuen commented May 8, 2026

Summary

  • send organization invitation emails through Resend when invitations are created
  • persist invitation email delivery status, Resend email id, send timestamp, and sanitized errors
  • expose email delivery fields in invitation API responses and document Resend env config

Review follow-up

  • added a 15s Resend send timeout so invitation requests do not hang indefinitely on email delivery
  • hoisted inviter lookup out of the per-invitation loop so batch invites do one inviter query instead of N
  • kept the email CTA pointed at the existing Cloud UI invitations inbox because nearai-cloud-ui does not currently expose a token landing page; linking to /v1/invitations/{token} would send users to API JSON rather than a usable UI

Deployment notes

  • set INVITATION_EMAIL_ENABLED=true, INVITATION_EMAIL_FROM, RESEND_API_KEY_FILE or RESEND_API_KEY, and CLOUD_UI_BASE_URL
  • Resend rejected NEAR AI Cloud <no-reply@near.ai> in dev because the near.ai domain is not verified in that Resend account yet
  • live smoke delivery to Pierre succeeded with Resend's test sender; email id 50b86c6f-1276-4b29-a977-b0f27594e3aa

Validation

  • cargo test -p services email --lib
  • cargo test -p services create_invitations --lib
  • cargo test -p config invitation_email
  • cargo check -p services -p database -p api
  • cargo test -p api --lib
  • cargo test -p database --lib
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo fmt --check
  • git diff --check

Follow-up issues

@PierreLeGuen PierreLeGuen requested a review from Evrard-Nil May 8, 2026 17:58
@PierreLeGuen PierreLeGuen temporarily deployed to Cloud API test env May 8, 2026 17:58 — with GitHub Actions Inactive
@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

Claude Code Review — PR #579 (Invitation Email Notifications)

Reviewed the diff in full; no prior reviews/comments to build on. Overall the structure is clean (port/adapter for EmailSender, sanitized errors, dedicated email_status lifecycle, NoopSender fallback, good test coverage). Below are the critical issues to address before merge.

⚠️ Critical / Should-fix

1. AWS SES client + config rebuilt on every email sendcrates/services/src/email.rs:108-116 (SesEmailSender::client)

async fn client(&self) -> aws_sdk_sesv2::Client {
    let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest());
    ...
    let shared_config = loader.load().await;
    aws_sdk_sesv2::Client::new(&shared_config)
}

aws_config::defaults().load().await performs credential resolution (IMDS, sts, env chain, etc.) on every invitation. Combined with send_invitation_email being called sequentially inside the per-recipient loop in create_invitations_impl (crates/services/src/organization/mod.rs:867-870), batch invitations of N users do N×(credential lookup + connection setup). Build the SES client once in SesEmailSender::new (or lazily via OnceCell) and store it on self.

2. No timeout on SES send_emailcrates/services/src/email.rs:135-138

let response = request.send().await.map_err(...)?;

A hung SES call holds the invitation HTTP request open. The default SDK timeout is permissive. Wrap with tokio::time::timeout (e.g. 10–15s) and convert elapsed → EmailError. This is especially important since send_invitation_email is awaited synchronously inside the request handler — the API caller waits for SES.

3. Per-invitation N+1 fetch of the inviter usercrates/services/src/organization/mod.rs:709-720
send_invitation_email calls user_repository.get_by_id(requester_id) for every recipient in a batch, but the inviter is identical for the whole batch. Hoist the lookup out of the loop in create_invitations_impl and pass inviter_name/inviter_email into send_invitation_email. (Bonus: makes the function pure-ish and easier to test.)

4. .expect() on email-sender init panics the API on startupcrates/api/src/lib.rs:104-106

let email_sender = services::email::sender_from_config(&config.invitation_email)
    .expect("Failed to initialize invitation email sender");

init_auth_services returns nothing fallible, so this is the only place to surface the error — but a panic here means a bad SES config takes the whole API down across the cluster on a rolling deploy. Either propagate the error up to main or, since from_env() already validates required fields when enabled=true, build the SesEmailSender lazily (deferred client creation makes the constructor infallible aside from missing from_email, which is already pre-validated).

Nice-to-have / Non-blocking

  • Sequential sends in batch invites: even after fixing (1)–(3), N recipients still serialize on SES round-trips. Consider futures::stream::iter(...).buffer_unordered(k) or tokio::join! for batches >1. Not blocking, but UX-relevant for bulk invites.
  • escape_html on URL in href=: invitations_url comes from trusted config, so this is fine today, but if the URL ever incorporates user-controlled segments (e.g. invitation token in path), prefer percent-encoding for the URL and HTML-escaping for the visible text separately.
  • email_last_error exposed on OrganizationInvitationResponse: sanitized, but make sure this endpoint is admin/owner-gated — surfacing raw SES error text to non-admin org members could leak operational details.
  • CLAUDE.md privacy check: the new code logs only IDs (invitation_id, organization_id, inviter_id) on the warn paths — ✅ compliant. Just confirming nothing got added at info level that includes recipient emails.

Other notes

  • Migration V0049 looks safe on PG16 (metadata-only ADD COLUMN ... NOT NULL DEFAULT since PG11). ✅
  • New columns + index are backward-compatible with prior code (old SELECTs unchanged). ✅
  • sanitize_error correctly collapses whitespace and truncates by char count. ✅
  • HTML escape order in escape_html is correct (& first). ✅
  • Test coverage of the three email outcomes (Sent/Failed/Skipped) is solid. ✅

⚠️ Issues found — items 1–4 should be addressed before merge.

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5481232092

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/services/src/organization/mod.rs
@PierreLeGuen PierreLeGuen force-pushed the invitation-email-notifications branch from 5481232 to c93eae3 Compare May 8, 2026 18:03
@PierreLeGuen PierreLeGuen temporarily deployed to Cloud API test env May 8, 2026 18:03 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements AWS SES email notifications for organization invitations, including database schema updates to track delivery status and service-level integration. Feedback highlights a critical missing error handler for a fallible builder call, an efficiency concern regarding repeated AWS client initialization, and a performance issue where inviter details are redundantly fetched during batch invitation processing.

Comment thread crates/services/src/email.rs Outdated
Comment thread crates/services/src/email.rs Outdated
Comment thread crates/services/src/organization/mod.rs Outdated
@PierreLeGuen PierreLeGuen force-pushed the invitation-email-notifications branch from c93eae3 to fb0b3bc Compare May 15, 2026 13:04
@PierreLeGuen PierreLeGuen temporarily deployed to Cloud API test env May 15, 2026 13:04 — with GitHub Actions Inactive
@PierreLeGuen PierreLeGuen changed the title Add organization invitation email notifications Add Resend organization invitation email notifications May 15, 2026
@PierreLeGuen PierreLeGuen requested a review from Evrard-Nil May 15, 2026 13:05
@PierreLeGuen PierreLeGuen temporarily deployed to Cloud API test env May 15, 2026 13:23 — with GitHub Actions Inactive
@PierreLeGuen
Copy link
Copy Markdown
Contributor Author

Review triage update:

  • Fixed the relevant runtime items in 9553d3f:
    • added a 15s timeout around Resend sends so invite requests cannot hang indefinitely on email delivery
    • hoisted inviter lookup out of the per-invitation loop; batch invites now load inviter details once, covered by a unit test
    • made disabled invitation email config ignore a stale/missing RESEND_API_KEY_FILE, so disabled email delivery cannot break startup
  • Marked the SES-specific inline comments resolved as outdated because the SES implementation was removed.
  • Did not change the CTA to a token URL: nearai-cloud-ui currently only implements /dashboard/invitations; there is no token landing page, and linking to /v1/invitations/{token} would expose API JSON rather than a usable UI flow.

Validation run after the review fixes:

  • cargo test -p services email --lib
  • cargo test -p services create_invitations --lib
  • cargo test -p config invitation_email
  • cargo check -p services -p database -p api
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test -p api --lib
  • cargo test -p database --lib
  • cargo fmt --check
  • git diff --check

@PierreLeGuen PierreLeGuen merged commit 372f6a5 into main May 15, 2026
7 checks passed
@PierreLeGuen PierreLeGuen deleted the invitation-email-notifications branch May 15, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants