feat: support standard AWS env vars and default credential chain (#401)

* feat: support standard AWS env vars and default credential chain

Replace non-standard AWS_ACCESS_KEY / AWS_SECRET_KEY with the AWS-standard
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY. The old names are kept as
fallbacks in the runtimeEnv for backward compatibility.

Both vars are now optional. When omitted, the credentials object is not
passed to SESv2Client, STSClient, or SNSClient — the AWS SDK then falls
back to its default provider chain (IAM roles, ECS task roles, instance
profiles, etc.), which is the recommended approach for cloud-native deployments.

Closes #316

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* refactor: extract shared getAwsCredentialOptions helper and add partial-config guard

- Move the credential spread logic into a single credentials.ts helper
  so SESv2Client, STSClient, and SNSClient all share one implementation
- Throw a clear error if only one of AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
  is set, preventing silent fallback to the default provider chain with a
  half-configured environment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: align AWS env vars in docker and docs

* fix: use alias import for AWS credentials helper

---------

Co-authored-by: purva <purvahk08@gmail.com>
Co-authored-by: Purva Kandalgaonkar <136103488+purva-8@users.noreply.github.com>
This commit is contained in:
KM Koushik
2026-05-17 21:23:28 +10:00
committed by GitHub
parent 31a49fbdca
commit 04d0f4b123
18 changed files with 78 additions and 53 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ SMTP_USER=test_userdadad@example.com # Example SMTP user
AWS_DEFAULT_REGION="us-east-1"
AWS_SECRET_KEY="some-secret-key"
AWS_ACCESS_KEY="some-access-key"
AWS_ACCESS_KEY_ID="some-access-key"
AWS_SECRET_ACCESS_KEY="some-secret-key"
AWS_SES_ENDPOINT="http://localhost:3003/api/ses"
AWS_SNS_ENDPOINT="http://localhost:3003/api/sns"
+5 -3
View File
@@ -25,10 +25,12 @@ GITHUB_SECRET="<your-github-client-secret>"
GOOGLE_CLIENT_ID="<your-google-client-id>"
GOOGLE_CLIENT_SECRET="<your-google-client-secret>"
# AWS details - required
# AWS details
# Provide static credentials OR rely on the AWS default credential chain
# (IAM role, ECS task role, instance profile, etc.) by omitting these vars.
AWS_DEFAULT_REGION="us-east-1"
AWS_SECRET_KEY="<your-aws-secret-key>"
AWS_ACCESS_KEY="<your-aws-access-key>"
AWS_ACCESS_KEY_ID="<your-aws-access-key-id>"
AWS_SECRET_ACCESS_KEY="<your-aws-secret-access-key>"
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
NEXTAUTH_SECRET: test-secret
DATABASE_URL: postgresql://usesend:password@127.0.0.1:5432/usesend_test
REDIS_URL: redis://127.0.0.1:6379/15
AWS_ACCESS_KEY: test-access-key
AWS_SECRET_KEY: test-secret-key
AWS_ACCESS_KEY_ID: test-access-key
AWS_SECRET_ACCESS_KEY: test-secret-key
AWS_DEFAULT_REGION: us-east-1
NEXT_PUBLIC_IS_CLOUD: "true"
API_RATE_LIMIT: "2"
+2 -2
View File
@@ -82,8 +82,8 @@ GITHUB_SECRET=your_client_secret
If you want to send real emails, add:
```env
AWS_ACCESS_KEY=your_access_key
AWS_SECRET_KEY=your_secret_key
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
```
> You can skip this by using the `local-sen-sns` image for local-only email development.
@@ -28,8 +28,8 @@ description: Step by step guide to create AWS credentials to self-host useSend.
Copy the access key ID and secret access key to your `.env` file.
```env
AWS_ACCESS_KEY=<access-key-id>
AWS_SECRET_KEY=<secret-access-key>
AWS_ACCESS_KEY_ID=<access-key-id>
AWS_SECRET_ACCESS_KEY=<secret-access-key>
```
![create access key](/images/aws/key-6.png)
+2 -2
View File
@@ -142,8 +142,8 @@ Once the app is added you can add the Client ID under `GITHUB_ID`and CLIENT SECR
Next, we need to add in the [AWS credentials](https://docs.usesend.com/get-started/create-aws-credentials). Follow the detailed guide to get the AWS credentials with accurate permissions and add them in:
```
AWS_ACCESS_KEY=<access-key-id>
AWS_SECRET_KEY=<secret-access-key>
AWS_ACCESS_KEY_ID=<access-key-id>
AWS_SECRET_ACCESS_KEY=<secret-access-key>
```
</Step>
+10 -10
View File
@@ -48,19 +48,19 @@ docker pull ghcr.io/usesend/usesend
```
docker run -d \
-p 3000:3000 \
-e NEXTAUTH_URL="<your-nextauth-url>"
-e NEXTAUTH_SECRET="<your-nextauth-secret>"
-e DATABASE_URL="<your-next-private-database-url>"
-e REDIS_URL="<your-next-private-redis-url>"
-e AWS_ACCESS_KEY="<your-next-private-aws-access-key-id>"
-e AWS_SECRET_KEY="<your-next-private-aws-secret-access-key>"
-e AWS_DEFAULT_REGION="<your-next-private-aws-region>"
-e GITHUB_ID="<your-next-private-github-id>"
-e GITHUB_SECRET="<your-next-private-github-secret>"
-e NEXTAUTH_URL="<your-nextauth-url>" \
-e NEXTAUTH_SECRET="<your-nextauth-secret>" \
-e DATABASE_URL="<your-next-private-database-url>" \
-e REDIS_URL="<your-next-private-redis-url>" \
-e AWS_ACCESS_KEY_ID="<your-next-private-aws-access-key-id>" \
-e AWS_SECRET_ACCESS_KEY="<your-next-private-aws-secret-access-key>" \
-e AWS_DEFAULT_REGION="<your-next-private-aws-region>" \
-e GITHUB_ID="<your-next-private-github-id>" \
-e GITHUB_SECRET="<your-next-private-github-secret>" \
usesend/usesend
```
Replace the placeholders with your actual database and aws details.
Replace the placeholders with your actual database and AWS details.
1. Access the useSend application by visiting the URL you provided in the `NEXTAUTH_URL` environment variable in your web browser.
+2 -2
View File
@@ -21,8 +21,8 @@ useSend depends on AWS SES to send emails and SNS to receive email status. Along
Add the following environment variables.
```env
AWS_ACCESS_KEY=<access-key-id>
AWS_SECRET_KEY=<secret-access-key>
AWS_ACCESS_KEY_ID=<access-key-id>
AWS_SECRET_ACCESS_KEY=<secret-access-key>
```
<Tip>
+2 -2
View File
@@ -32,8 +32,8 @@ useSend depends on AWS SES to send emails and SNS to receive email status. The R
Add the following environment variables in Railway.
```env
AWS_ACCESS_KEY=<access-key-id>
AWS_SECRET_KEY=<secret-access-key>
AWS_ACCESS_KEY_ID=<access-key-id>
AWS_SECRET_ACCESS_KEY=<secret-access-key>
```
<Tip>
+2 -2
View File
@@ -5,8 +5,8 @@ NEXTAUTH_SECRET=test-secret
DATABASE_URL=postgresql://usesend:password@127.0.0.1:54329/usesend_test
REDIS_URL=redis://127.0.0.1:6380/15
AWS_ACCESS_KEY=test-access-key
AWS_SECRET_KEY=test-secret-key
AWS_ACCESS_KEY_ID=test-access-key
AWS_SECRET_ACCESS_KEY=test-secret-key
AWS_DEFAULT_REGION=us-east-1
NEXT_PUBLIC_IS_CLOUD=true
+4 -4
View File
@@ -31,8 +31,8 @@ export const env = createEnv({
),
GITHUB_ID: z.string().optional(),
GITHUB_SECRET: z.string().optional(),
AWS_ACCESS_KEY: z.string(),
AWS_SECRET_KEY: z.string(),
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
USESEND_API_KEY: z.string().optional(),
UNSEND_API_KEY: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(),
@@ -99,8 +99,8 @@ export const env = createEnv({
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
GITHUB_ID: process.env.GITHUB_ID,
GITHUB_SECRET: process.env.GITHUB_SECRET,
AWS_ACCESS_KEY: process.env.AWS_ACCESS_KEY,
AWS_SECRET_KEY: process.env.AWS_SECRET_KEY,
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY || process.env.AWS_SECRET_KEY,
USESEND_API_KEY: process.env.USESEND_API_KEY,
UNSEND_API_KEY: process.env.UNSEND_API_KEY,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
+22
View File
@@ -0,0 +1,22 @@
import { env } from "~/env";
export function getAwsCredentialOptions() {
const hasKey = !!env.AWS_ACCESS_KEY_ID;
const hasSecret = !!env.AWS_SECRET_ACCESS_KEY;
if (hasKey !== hasSecret) {
throw new Error(
"AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must both be set or both be omitted"
);
}
if (hasKey) {
return {
credentials: {
accessKeyId: env.AWS_ACCESS_KEY_ID!,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY!,
},
};
}
return {};
}
+3 -8
View File
@@ -17,6 +17,7 @@ import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
import { generateKeyPairSync } from "crypto";
import nodemailer from "nodemailer";
import { env } from "~/env";
import { getAwsCredentialOptions } from "~/server/aws/credentials";
import { EmailContent } from "~/types";
import { logger } from "../logger/log";
import { buildHeaders } from "~/server/utils/email-headers";
@@ -30,10 +31,7 @@ async function getAccountId(region: string) {
const stsClient = new STSClient({
region: region,
credentials: {
accessKeyId: env.AWS_ACCESS_KEY,
secretAccessKey: env.AWS_SECRET_KEY,
},
...getAwsCredentialOptions(),
});
const command = new GetCallerIdentityCommand({});
const response = await stsClient.send(command);
@@ -50,10 +48,7 @@ function getSesClient(region: string) {
return new SESv2Client({
region: region,
endpoint: env.AWS_SES_ENDPOINT,
credentials: {
accessKeyId: env.AWS_ACCESS_KEY,
secretAccessKey: env.AWS_SECRET_KEY,
},
...getAwsCredentialOptions(),
});
}
+2 -4
View File
@@ -5,15 +5,13 @@ import {
DeleteTopicCommand,
} from "@aws-sdk/client-sns";
import { env } from "~/env";
import { getAwsCredentialOptions } from "~/server/aws/credentials";
function getSnsClient(region: string) {
return new SNSClient({
endpoint: env.AWS_SNS_ENDPOINT,
region: region,
credentials: {
accessKeyId: env.AWS_ACCESS_KEY,
secretAccessKey: env.AWS_SECRET_KEY,
},
...getAwsCredentialOptions(),
});
}
+2 -2
View File
@@ -4,8 +4,8 @@ const defaultEnv: Record<string, string> = {
NEXTAUTH_SECRET: "test-secret",
DATABASE_URL: "postgresql://usesend:password@127.0.0.1:54329/usesend_test",
REDIS_URL: "redis://127.0.0.1:6380/15",
AWS_ACCESS_KEY: "test-access-key",
AWS_SECRET_KEY: "test-secret-key",
AWS_ACCESS_KEY_ID: "test-access-key",
AWS_SECRET_ACCESS_KEY: "test-secret-key",
AWS_DEFAULT_REGION: "us-east-1",
NEXT_PUBLIC_IS_CLOUD: "true",
API_RATE_LIMIT: "2",
+3 -3
View File
@@ -52,15 +52,15 @@ docker run -d \
-e NEXTAUTH_SECRET="<your-nextauth-secret>" \
-e DATABASE_URL="<your-database-url>" \
-e REDIS_URL="<your-redis-url>" \
-e AWS_ACCESS_KEY="<your-aws-access-key-id>" \
-e AWS_SECRET_KEY="<your-aws-secret-access-key>" \
-e AWS_ACCESS_KEY_ID="<your-aws-access-key-id>" \
-e AWS_SECRET_ACCESS_KEY="<your-aws-secret-access-key>" \
-e AWS_DEFAULT_REGION="<your-aws-region>" \
-e GITHUB_ID="<your-github-client-id>" \
-e GITHUB_SECRET="<your-github-client-secret>" \
usesend/usesend
```
Replace the placeholders with your actual database and aws details.
Replace the placeholders with your actual database and AWS details.
1. Access the useSend application by visiting the URL you provided in the `NEXTAUTH_URL` environment variable in your web browser.
+4 -2
View File
@@ -54,8 +54,10 @@ services:
- DATABASE_URL=${DATABASE_URL:?err}
- NEXTAUTH_URL=${NEXTAUTH_URL:?err}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY:?err}
- AWS_SECRET_KEY=${AWS_SECRET_KEY:?err}
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY:-}
- AWS_SECRET_KEY=${AWS_SECRET_KEY:-}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:?err}
- GITHUB_ID=${GITHUB_ID:?err}
- GITHUB_SECRET=${GITHUB_SECRET:?err}
+7 -1
View File
@@ -21,11 +21,17 @@
"GITHUB_SECRET",
"AWS_SECRET_KEY",
"AWS_ACCESS_KEY",
"AWS_SECRET_ACCESS_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_DEFAULT_REGION",
"AWS_SES_ENDPOINT",
"AWS_SNS_ENDPOINT",
"NEXTAUTH_SECRET",
"NODE_ENV",
"VERCEL_URL",
"VERCEL",
"SKIP_ENV_VALIDATION",
"DOCKER_OUTPUT",
"PORT",
"UNSEND_API_KEY",
"USESEND_API_KEY",
@@ -57,4 +63,4 @@
"cache": false
}
}
}
}