Uh oh!
There was an error while loading. Please reload this page.
feat: add ses tenant support for teams - #193
Conversation
WalkthroughA new optional SES tenant ID field was added to the Team and Domain models in the database schema, Prisma schema, and related TypeScript types. The email sending and domain management logic were updated to support passing this SES tenant ID when interacting with AWS SES. Additionally, the AWS SES SDK dependency was updated and a new STS client dependency was added. Changes
Sequence Diagram(s)sequenceDiagram
participant API as Domain API
participant Service as Domain Service
participant SESLogic as SES Email Logic
participant AWS as AWS SES
participant DB as Database (Prisma)
API->>Service: createDomain(teamId, name, region, sesTenantId?)
Service->>SESLogic: addDomain(domain, region, sesTenantId?)
SESLogic->>AWS: CreateEmailIdentity + CreateTenantResourceAssociation (if sesTenantId)
AWS-->>SESLogic: Response
SESLogic-->>Service: Result
Service->>DB: Create domain record (with sesTenantId)
DB-->>Service: Confirmation
Service-->>API: Domain created
API->>Service: deleteDomain(domainId)
Service->>DB: Fetch domain (including sesTenantId)
DB-->>Service: Domain data
Service->>SESLogic: deleteDomain(domain, region, sesTenantId?)
SESLogic->>AWS: DeleteTenantResourceAssociation (if sesTenantId) + DeleteEmailIdentity
AWS-->>SESLogic: Response
SESLogic-->>Service: Result
Service->>DB: Delete domain record
DB-->>Service: Confirmation
Service-->>API: Domain deleted
participant EmailService as EmailQueueService
participant SES as SES Email Logic
EmailService->>DB: Fetch email (include team.sesTenantId)
DB-->>EmailService: Return email + team.sesTenantId
EmailService->>SES: sendRawEmail(..., sesTenantId)
SES->>AWS: SendEmailCommand (TenantName=sesTenantId if provided)
AWS-->>SES: SES response
SES-->>EmailService: Result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File ( |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
apps/web/src/server/aws/ses.ts (1)
127-140: Clarify naming: “Id” vs “Name” (align semantics across codebase).Parameter is named sesTenantId, but later you set TenantName (Line 191). If this value is truly an ID, keep the semantics consistent (e.g., sesTenantId everywhere and tag key tenant_id). If it’s a human-readable tenant name, rename this param to sesTenantName to avoid confusion.
Please confirm which it is intended to represent so we can standardize the naming and tag key accordingly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
apps/web/package.json(1 hunks)apps/web/prisma/migrations/20250808231408_add_ses_tenant_id/migration.sql(1 hunks)apps/web/prisma/schema.prisma(1 hunks)apps/web/src/server/aws/ses.ts(2 hunks)apps/web/src/server/service/email-queue-service.ts(2 hunks)apps/web/src/types/index.ts(1 hunks)
| "@auth/prisma-adapter": "^2.9.0", | ||
| "@aws-sdk/client-s3": "^3.797.0", | ||
| "@aws-sdk/client-sesv2": "^3.797.0", | ||
| "@aws-sdk/client-sesv2": "^3.858.0", |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm SESv2 supports the intended TenantName parameter in this version.
Before shipping, verify that @aws-sdk/client-sesv2@^3.858.0 includes the TenantName (or equivalent) property used in your sendRawEmail call. This avoids runtime type or API errors.
Run this script to confirm usage in-repo and the SDK typings:
🏁 Script executed:
#!/bin/bash# 1) Check our code uses a valid SES property name
rg -n "TenantName|tenantName" -A3 -B3
# 2) Inspect sendRawEmail signature and usage
rg -n "function sendRawEmail|export const sendRawEmail|export async function sendRawEmail" -A20 apps/web/src/server/aws/ses.ts
# 3) If you have node_modules locally, inspect the type definition for SendEmailCommandInput# (This will just print the symbol reference line if available in the sandbox)
rg -n "interface SendEmailCommandInput" node_modules/@aws-sdk/client-sesv2/dist-types/ -A40 ||trueLength of output: 1205
To confirm whether SendEmailCommandInput in @aws-sdk/client-sesv2@3.858.0 actually defines a TenantName property, let’s pull its published type definitions directly from Unpkg and inspect them:
🏁 Script executed:
#!/bin/bash# 1) Fetch the package.json to locate the entrypoint for types
typesPath=$(curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/package.json | jq -r .types)echo"Type definitions file: $typesPath"# 2) Search for the SendEmailCommandInput interfaceecho"--- SendEmailCommandInput definition ---"
curl -s "https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/$typesPath" \
| grep -n "export interface SendEmailCommandInput" -A20 -B2
# 3) Search within that file for TenantNameecho"--- TenantName occurrences in types ---"
curl -s "https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/$typesPath" \
| grep -n "TenantName" -A2 -B2Length of output: 659
To locate the SendEmailCommandInput definition (it’s actually in the models directory), let’s pull that file and extract the interface block, then search for TenantName:
🏁 Script executed:
#!/bin/bash# 1) Fetch and print the SendEmailCommandInput interface from models/index.d.tsecho"=== SendEmailCommandInput interface block ==="
curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/dist-types/models/index.d.ts \
| sed -n '/export interface SendEmailCommandInput/,/^}/p'# 2) Check for TenantName within that same fileecho"=== TenantName occurrences ==="
curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/dist-types/models/index.d.ts \
| grep -n 'TenantName' -A2 -B2 ||echo"No TenantName found"Length of output: 481
SESv2 doesn’t support a TenantName parameter in SendRawEmailCommandInput (v3.858.0)
Our lookup of the published typings for @aws-sdk/client-sesv2@3.858.0 shows no TenantName field. You’ll need to remove or replace it in your SES call:
• File: apps/web/src/server/aws/ses.ts, around line 191
• Remove this line:
TenantName: sesTenantId ? sesTenantId : undefined,• If you’re trying to tag messages by tenant, use the SESv2 Tags (or EmailTags) parameter instead. For example:
{
ConfigurationSetName: configurationSetName,
- TenantName: sesTenantId,+ Tags: sesTenantId+ ? [{ Name: "TenantName", Value: sesTenantId }]+ : undefined,
/* …other properties… */
}🤖 Prompt for AI Agents
In apps/web/src/server/aws/ses.ts around line 191, the SendRawEmailCommandInput
includes a TenantName parameter which is not supported by @aws-sdk/client-sesv2
version 3.858.0. Remove the line setting TenantName and if tenant tagging is
needed, replace it by using the SESv2 Tags or EmailTags parameter to tag
messages appropriately.
There was a problem hiding this comment.
not true you did not check correctly it's not even the right type its SendEmailCommand
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Let's check the actual SendEmailCommand and SendEmailCommandInput definitions in version 3.858.0
# 1) First, let's verify what command is actually being used in the code
echo "=== Checking what command is used in ses.ts ==="
grep -n "SendEmailCommand\|SendRawEmailCommand" apps/web/src/server/aws/ses.ts | head -10
# 2) Fetch the SendEmailCommandInput interface definition from the AWS SDK
echo -e "\n=== SendEmailCommandInput interface definition ==="
curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/dist-types/models/models_1.d.ts \
| sed -n '/export interface SendEmailCommandInput/,/^}/p'
# 3) Check if TenantName exists in the SendEmailCommandInput
echo -e "\n=== Checking for TenantName in SendEmailCommandInput ==="
curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/dist-types/models/models_1.d.ts \
| grep -n "TenantName" -B2 -A2 || echo "No TenantName found in models_1.d.ts"
# 4) Let's also check models_0.d.ts as interfaces might be split across files
echo -e "\n=== Checking models_0.d.ts for SendEmailCommandInput or TenantName ==="
curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/dist-types/models/models_0.d.ts \
| grep -n "SendEmailCommandInput\|TenantName" -B2 -A2 || echo "Not found in models_0.d.ts"
Length of output: 2910
🏁 Script executed:
#!/bin/bash
# Let's get more context around SendEmailCommandInput to confirm TenantName is part of it
# 1) Get the full SendEmailCommandInput interface with more context
echo "=== Full SendEmailCommandInput interface ==="
curl -s https://unpkg.com/@aws-sdk/client-sesv2@3.858.0/dist-types/models/models_1.d.ts \
| sed -n '/export interface SendEmailCommandInput {/,/^}/p' | head -100
# 2) Let's also check the actual usage in the repository
echo -e "\n=== Checking usage in ses.ts ==="
cat apps/web/src/server/aws/ses.ts | grep -A 15 "const command = new SendEmailCommand"
Length of output: 893
@KMKoushik You're absolutely right - I apologize for the confusion. I incorrectly checked for SendRawEmailCommandInput when your code actually uses SendEmailCommand.
The AWS SDK @aws-sdk/client-sesv2 version 3.858.0 does support the TenantName property in SendEmailCommandInput. Your implementation in apps/web/src/server/aws/ses.ts is correct:
constcommand=newSendEmailCommand({Content: {Raw: {Data: finalMessageData,},},ConfigurationSetName: configurationSetName,TenantName: sesTenantId ? sesTenantId : undefined,});The TenantName field is properly defined as an optional string in the SDK types. Thank you for the correction!
✏️ Learnings added
Learnt from: KMKoushik
PR: unsend-dev/unsend#193
File: apps/web/package.json:22-22
Timestamp: 2025-08-09T00:04:24.626Z
Learning: In AWS SDK aws-sdk/client-sesv2 version 3.858.0 and later, the SendEmailCommand supports an optional TenantName property in its input parameters for multi-tenant SES configurations.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| @@ -0,0 +1,2 @@ | |||
| -- AlterTable | |||
| ALTER TABLE "Team" ADD COLUMN "sesTenantId" TEXT; | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion
Follow-up: add a unique constraint if sesTenantId must not collide across teams.
Optional column addition is good. If sesTenantId is a tenant identifier, a unique constraint helps maintain integrity.
Example migration (non-concurrent, Prisma-compatible):
ALTERTABLE"Team"
ADD CONSTRAINT"Team_sesTenantId_key" UNIQUE ("sesTenantId");If the table is large and you need to avoid write locks, consider a manual path with a concurrently-built unique index (outside Prisma migrator).
🤖 Prompt for AI Agents
In apps/web/prisma/migrations/20250808231408_add_ses_tenant_id/migration.sql at
line 2, after adding the sesTenantId column, add a unique constraint on
sesTenantId to ensure no duplicate tenant IDs exist across teams. You can do
this by adding an ALTER TABLE statement to add a UNIQUE constraint on the
sesTenantId column. If the table is large and you want to avoid locking issues,
consider creating a unique index concurrently outside of Prisma migrations.
| isActive Boolean @default(true) | ||
| apiRateLimit Int @default(2) | ||
| billingEmail String? | ||
| sesTenantId String? |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider making sesTenantId unique (and document semantics).
If sesTenantId is used as a tenant identifier, enforce uniqueness and add an index to prevent collisions and speed up lookups.
Suggested Prisma change:
- sesTenantId String?+ sesTenantId String? @uniqueIf you prefer a manual SQL migration, add a unique constraint:
ALTERTABLE"Team"
ADD CONSTRAINT"Team_sesTenantId_key" UNIQUE ("sesTenantId");🤖 Prompt for AI Agents
In apps/web/prisma/schema.prisma at line 109, the sesTenantId field should be
made unique to enforce tenant identifier uniqueness and improve lookup
performance. Modify the sesTenantId field by adding the @unique attribute to
create a unique constraint and index automatically. Additionally, update the
schema documentation to explain the semantics of sesTenantId as a unique tenant
identifier. If you prefer manual migration, add a unique constraint on the
sesTenantId column in the Team table using SQL as suggested.
| TenantName: sesTenantId ? sesTenantId : undefined, | ||
| }); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
TenantName is not a valid SendEmailCommand input; use EmailTags (or X-SES-MESSAGE-TAGS) instead.
AWS SESv2’s SendEmailRequest doesn’t include a top-level TenantName property. This will either fail type-checking or be ignored by the SDK. Use EmailTags (propagates to event data) and/or add the X-SES-MESSAGE-TAGS header in the raw MIME.
Apply this diff to the command input (Line 191):
- ConfigurationSetName: configurationSetName,- TenantName: sesTenantId ? sesTenantId : undefined,+ ConfigurationSetName: configurationSetName,+ EmailTags: sesTenantId ? [{ Name: "tenant", Value: sesTenantId }] : undefined,Optionally, also tag at the MIME layer (earlier in sendMail headers block) so tags survive even if future changes alter the command input:
headers: {// ...
...(sesTenantId ? {"X-SES-MESSAGE-TAGS": `tenant=${sesTenantId}`} : {}),}🌐 Web query:
Does the AWS SDK for JavaScript v3 (@aws-sdk/client-sesv2) SendEmailCommand support a top-level "TenantName" parameter? If not, what are the recommended ways to associate a tenant identifier with emails (e.g., EmailTags or X-SES-MESSAGE-TAGS) when sending Raw content?
💡 Result:
The AWS SDK for JavaScript v3's @aws-sdk/client-sesv2 package does not support a top-level TenantName parameter in the SendEmailCommand. Instead, to associate a tenant identifier with emails, especially when sending raw content, you can utilize the X-SES-TENANT header. This header allows you to specify the tenant through which the email is sent, ensuring that Amazon SES applies the appropriate resources and reputation metrics associated with that tenant.
Here's how you can include the X-SES-TENANT header when sending a raw email using the AWS SDK for JavaScript v3:
import{SESv2Client,SendEmailCommand}from"@aws-sdk/client-sesv2";constsesClient=newSESv2Client({region: "us-east-1"});constsendRawEmailWithTenant=async()=>{constrawEmailData=` From: sender@example.com To: recipient@example.com Subject: Test Email with Tenant Header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 X-SES-TENANT: MyTenant This is a test email sent through the MyTenant tenant. `;constcommand=newSendEmailCommand({Content: {Raw: {Data: Buffer.from(rawEmailData),},},});try{constdata=awaitsesClient.send(command);console.log("Email sent successfully:",data.MessageId);}catch(error){console.error("Error sending email:",error);}};sendRawEmailWithTenant();In this example:
- The
X-SES-TENANTheader is set toMyTenant, associating the email with the specified tenant. - The
SendEmailCommandis used to send the raw email content.
By including the X-SES-TENANT header, Amazon SES can correctly route the email through the specified tenant, applying the associated resources and reputation metrics. This approach is particularly useful in multi-tenant environments where isolating email sending activities is essential.
For more detailed information on tenant management and sending emails through specific tenants in Amazon SES, refer to the Amazon SES documentation on tenants.
Remove unsupported TenantName; use EmailTags or X-SES-TENANT header instead
SESv2’s SendEmailCommandInput doesn’t include a TenantName field—any such property will be ignored or trigger a type error. To associate your application tenant with outgoing mail, switch to using the built-in tagging mechanisms:
• In the SendEmailCommand input, replace the invalid TenantName with an EmailTags array:
--- a/apps/web/src/server/aws/ses.ts+++ b/apps/web/src/server/aws/ses.ts@@ -191,2 +191,4 @@- TenantName: sesTenantId ? sesTenantId : undefined,+ EmailTags: sesTenantId+ ? [{ Name: "tenant", Value: sesTenantId }]+ : undefined,• If you’re sending raw MIME content (via Content.Raw), also inject the X-SES-TENANT header so the tenant routing survives downstream changes:
headers: {// existing headers…
...(sesTenantId&&{"X-SES-TENANT": sesTenantId}),}See AWS SES “Tenants” docs for details:
https://docs.aws.amazon.com/ses/latest/dg/tenants.html
🤖 Prompt for AI Agents
In apps/web/src/server/aws/ses.ts around lines 191 to 192, the
SendEmailCommandInput incorrectly includes a TenantName field which is
unsupported and causes errors. Remove the TenantName property and instead add an
EmailTags array with the tenant ID to the command input. Additionally, if
sending raw MIME content, add an X-SES-TENANT header with the tenant ID to the
email headers to ensure tenant information is preserved downstream.
Uh oh!
There was an error while loading. Please reload this page.
| sesTenantId: email.team.sesTenantId, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Coerce nullable value to undefined when passing to AWS.
Avoid passing null into the SES command input. Pass it only when defined.
- sesTenantId: email.team.sesTenantId,+ sesTenantId: email.team?.sesTenantId ?? undefined,If you adopt sesTenantId?: string in types, this will align naturally with the API’s expectations.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sesTenantId: email.team.sesTenantId, | |
| }); | |
| sesTenantId: email.team?.sesTenantId??undefined, | |
| }); |
🤖 Prompt for AI Agents
In apps/web/src/server/service/email-queue-service.ts around lines 388 to 389,
the sesTenantId value is nullable and may be passed as null to the AWS SES
command, which expects undefined instead of null for optional fields. Update the
code to coerce sesTenantId to undefined when it is null or not defined before
passing it to the SES command. Additionally, consider updating the type
definition of sesTenantId to be optional (sesTenantId?: string) to better align
with the API expectations.
| unsubUrl?: string; | ||
| scheduledAt?: string; | ||
| inReplyToId?: string | null; | ||
| sesTenantId?: string | null; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Prefer sesTenantId?: string over string | null to avoid null propagation.
SES inputs generally expect absent/undefined fields instead of explicit null. Keep the type optional and omit when not present.
- sesTenantId?: string | null;+ sesTenantId?: string;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sesTenantId?: string|null; | |
| sesTenantId?: string; |
🤖 Prompt for AI Agents
In apps/web/src/types/index.ts at line 16, change the type of sesTenantId from
"string | null" to just "string" while keeping it optional. This means replacing
"sesTenantId?: string | null" with "sesTenantId?: string" to avoid using null
and instead rely on the field being undefined when not present.
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
apps/web/src/server/service/domain-service.ts (1)
146-154: Check delete-tenant association result.
DeleteTenantResourceAssociationCommand’s response is discarded, so if the disassociation fails the domain is still deleted locally, causing drift. Inspectresponse.$metadata.httpStatusCode(expect 200) and abort on failure, mirroring theaddDomaincheck.
♻️ Duplicate comments (1)
apps/web/src/server/aws/ses.ts (1)
252-253: Remove unsupportedTenantName; use tagging instead.SendEmailCommandinput has noTenantNamefield – this will type-error or be dropped. UseEmailTagsor addX-SES-TENANTheader (see previous review).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
apps/web/package.json(1 hunks)apps/web/prisma/migrations/20250809070118_add_ses_tenant_id_to_domain/migration.sql(1 hunks)apps/web/prisma/schema.prisma(2 hunks)apps/web/src/server/api/routers/domain.ts(1 hunks)apps/web/src/server/aws/ses.ts(6 hunks)apps/web/src/server/public-api/api/domains/create-domain.ts(1 hunks)apps/web/src/server/service/domain-service.ts(4 hunks)
✅ Files skipped from review due to trivial changes (2)
- apps/web/package.json
- apps/web/prisma/migrations/20250809070118_add_ses_tenant_id_to_domain/migration.sql
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/prisma/schema.prisma
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: KMKoushik
PR: unsend-dev/unsend#193
File: apps/web/package.json:22-22
Timestamp: 2025-08-09T00:04:24.626Z
Learning: In AWS SDK aws-sdk/client-sesv2 version 3.858.0 and later, the SendEmailCommand supports an optional TenantName property in its input parameters for multi-tenant SES configurations.
📚 Learning: 2025-08-09T00:04:24.626Z
Learnt from: KMKoushik
PR: unsend-dev/unsend#193
File: apps/web/package.json:22-22
Timestamp: 2025-08-09T00:04:24.626Z
Learning: In AWS SDK aws-sdk/client-sesv2 version 3.858.0 and later, the SendEmailCommand supports an optional TenantName property in its input parameters for multi-tenant SES configurations.
Applied to files:
apps/web/src/server/public-api/api/domains/create-domain.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/aws/ses.ts
🧬 Code Graph Analysis (1)
apps/web/src/server/api/routers/domain.ts (1)
apps/web/src/server/service/domain-service.ts (1)
createDomain(57-92)
🔇 Additional comments (2)
apps/web/src/server/public-api/api/domains/create-domain.ts (1)
39-44: Looks good – parameter threading is correct.
The extra argument aligns with the updated service signature; no issues spotted.apps/web/src/server/api/routers/domain.ts (1)
28-33: All good – router now forwards the tenant ID.
Signature match verified.
Uh oh!
There was an error while loading. Please reload this page.
Summary by CodeRabbit
New Features
Chores