From 04d0f4b12380d8113c648acad643abcacfc7b746 Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sun, 17 May 2026 21:23:28 +1000 Subject: [PATCH] feat: support standard AWS env vars and default credential chain (#401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * fix: align AWS env vars in docker and docs * fix: use alias import for AWS credentials helper --------- Co-authored-by: purva Co-authored-by: Purva Kandalgaonkar <136103488+purva-8@users.noreply.github.com> --- .env.example | 4 ++-- .env.selfhost.example | 8 ++++--- .github/workflows/test-web.yml | 4 ++-- CONTRIBUTION.md | 4 ++-- .../get-started/create-aws-credentials.mdx | 4 ++-- apps/docs/get-started/local.mdx | 4 ++-- apps/docs/get-started/set-up-docker.mdx | 20 ++++++++--------- apps/docs/self-hosting/overview.mdx | 4 ++-- apps/docs/self-hosting/railway.mdx | 4 ++-- apps/web/.env.test.example | 4 ++-- apps/web/src/env.js | 8 +++---- apps/web/src/server/aws/credentials.ts | 22 +++++++++++++++++++ apps/web/src/server/aws/ses.ts | 11 +++------- apps/web/src/server/aws/sns.ts | 6 ++--- apps/web/src/test/setup/setup-env.ts | 4 ++-- docker/README.md | 6 ++--- docker/prod/compose.yml | 6 +++-- turbo.json | 8 ++++++- 18 files changed, 78 insertions(+), 53 deletions(-) create mode 100644 apps/web/src/server/aws/credentials.ts diff --git a/.env.example b/.env.example index 79539fe..eeea5d0 100644 --- a/.env.example +++ b/.env.example @@ -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" diff --git a/.env.selfhost.example b/.env.selfhost.example index d294bd3..976d778 100644 --- a/.env.selfhost.example +++ b/.env.selfhost.example @@ -25,10 +25,12 @@ GITHUB_SECRET="" GOOGLE_CLIENT_ID="" 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="" -AWS_ACCESS_KEY="" +AWS_ACCESS_KEY_ID="" +AWS_SECRET_ACCESS_KEY="" diff --git a/.github/workflows/test-web.yml b/.github/workflows/test-web.yml index d5139ed..c8e751e 100644 --- a/.github/workflows/test-web.yml +++ b/.github/workflows/test-web.yml @@ -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" diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 71ad69c..b9754b3 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -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. diff --git a/apps/docs/get-started/create-aws-credentials.mdx b/apps/docs/get-started/create-aws-credentials.mdx index 4823c10..4b4a0ed 100644 --- a/apps/docs/get-started/create-aws-credentials.mdx +++ b/apps/docs/get-started/create-aws-credentials.mdx @@ -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= - AWS_SECRET_KEY= + AWS_ACCESS_KEY_ID= + AWS_SECRET_ACCESS_KEY= ``` ![create access key](/images/aws/key-6.png) diff --git a/apps/docs/get-started/local.mdx b/apps/docs/get-started/local.mdx index 4e101db..d043956 100644 --- a/apps/docs/get-started/local.mdx +++ b/apps/docs/get-started/local.mdx @@ -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= -AWS_SECRET_KEY= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= ``` diff --git a/apps/docs/get-started/set-up-docker.mdx b/apps/docs/get-started/set-up-docker.mdx index ac9c927..1766c5d 100644 --- a/apps/docs/get-started/set-up-docker.mdx +++ b/apps/docs/get-started/set-up-docker.mdx @@ -48,19 +48,19 @@ docker pull ghcr.io/usesend/usesend ``` docker run -d \ -p 3000:3000 \ - -e NEXTAUTH_URL="" - -e NEXTAUTH_SECRET="" - -e DATABASE_URL="" - -e REDIS_URL="" - -e AWS_ACCESS_KEY="" - -e AWS_SECRET_KEY="" - -e AWS_DEFAULT_REGION="" - -e GITHUB_ID="" - -e GITHUB_SECRET="" + -e NEXTAUTH_URL="" \ + -e NEXTAUTH_SECRET="" \ + -e DATABASE_URL="" \ + -e REDIS_URL="" \ + -e AWS_ACCESS_KEY_ID="" \ + -e AWS_SECRET_ACCESS_KEY="" \ + -e AWS_DEFAULT_REGION="" \ + -e GITHUB_ID="" \ + -e 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. diff --git a/apps/docs/self-hosting/overview.mdx b/apps/docs/self-hosting/overview.mdx index 07b2413..2c1a6ed 100644 --- a/apps/docs/self-hosting/overview.mdx +++ b/apps/docs/self-hosting/overview.mdx @@ -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= - AWS_SECRET_KEY= + AWS_ACCESS_KEY_ID= + AWS_SECRET_ACCESS_KEY= ``` diff --git a/apps/docs/self-hosting/railway.mdx b/apps/docs/self-hosting/railway.mdx index 771325e..0c004dc 100644 --- a/apps/docs/self-hosting/railway.mdx +++ b/apps/docs/self-hosting/railway.mdx @@ -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= -AWS_SECRET_KEY= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= ``` diff --git a/apps/web/.env.test.example b/apps/web/.env.test.example index 1f62dec..58459ee 100644 --- a/apps/web/.env.test.example +++ b/apps/web/.env.test.example @@ -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 diff --git a/apps/web/src/env.js b/apps/web/src/env.js index 9c37cdd..8e73959 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -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, diff --git a/apps/web/src/server/aws/credentials.ts b/apps/web/src/server/aws/credentials.ts new file mode 100644 index 0000000..551dee8 --- /dev/null +++ b/apps/web/src/server/aws/credentials.ts @@ -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 {}; +} diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 0fda94c..5b6e9ab 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -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(), }); } diff --git a/apps/web/src/server/aws/sns.ts b/apps/web/src/server/aws/sns.ts index 0b99b13..9973532 100644 --- a/apps/web/src/server/aws/sns.ts +++ b/apps/web/src/server/aws/sns.ts @@ -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(), }); } diff --git a/apps/web/src/test/setup/setup-env.ts b/apps/web/src/test/setup/setup-env.ts index 96d56b4..c6f5879 100644 --- a/apps/web/src/test/setup/setup-env.ts +++ b/apps/web/src/test/setup/setup-env.ts @@ -4,8 +4,8 @@ const defaultEnv: Record = { 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", diff --git a/docker/README.md b/docker/README.md index dfe16e4..2ce157f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -52,15 +52,15 @@ docker run -d \ -e NEXTAUTH_SECRET="" \ -e DATABASE_URL="" \ -e REDIS_URL="" \ - -e AWS_ACCESS_KEY="" \ - -e AWS_SECRET_KEY="" \ + -e AWS_ACCESS_KEY_ID="" \ + -e AWS_SECRET_ACCESS_KEY="" \ -e AWS_DEFAULT_REGION="" \ -e GITHUB_ID="" \ -e 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. diff --git a/docker/prod/compose.yml b/docker/prod/compose.yml index 89bba50..736ceb3 100644 --- a/docker/prod/compose.yml +++ b/docker/prod/compose.yml @@ -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} diff --git a/turbo.json b/turbo.json index a0d6144..7be1b51 100644 --- a/turbo.json +++ b/turbo.json @@ -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 } } -} \ No newline at end of file +}