AWS Integration Guide
█████╗ ██╗ ██╗███████╗
██╔══██╗██║ ██║██╔════╝
███████║██║ █╗ ██║███████╗
██╔══██║██║███╗██║╚════██║
██║ ██║╚███╔███╔╝███████║
╚═╝ ╚═╝ ╚══╝╚══╝ ╚══════╝AWS Integration Guide
Focus: When and how codeAmani uses Amazon Web Services for Enterprise-tier client builds — IAM least-privilege, S3/presigned URLs, RDS & Aurora Serverless v2, Lambda/API Gateway, ECS Fargate, CloudFront, SES, IaC with the AWS CDK (TypeScript), and a HIPAA baseline for the Florida healthcare niche.
Overview
AWS is advertised as infrastructure on motionstackstudios.com and is the host for the agency's Enterprise segment — $50K+ engagements, including a HIPAA-compliant healthcare platform for an APD/AHCA/DCF-licensed provider. It is the heavyweight option in a roster of house hosting guides (vercel, netlify, cloudflare, render, google-cloud), and it complements rather than replaces them.
The house defaults stay the same for almost everything: Neon for Postgres, Cloudflare R2 for object storage, Resend for email, Vercel for app hosting. AWS earns a place only when an enterprise client's contract demands something a default can't give — a signed BAA, data residency in a specific region, a private VPC, VPC-peered databases, or scale/SLA guarantees. The decision flow:
Check first. Before adding any AWS service, query the
codeAmani-tech-stackMCP (search_guides/get_guide) and confirm a house default won't satisfy the requirement. AWS adds operational surface, cost, and compliance obligations — prefer the default unless the client specifically requires AWS.
Official Documentation
| Resource | URL |
|---|---|
| AWS Documentation (root) | https://docs.aws.amazon.com |
| AWS SDK for JavaScript v3 | https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/ |
| AWS CDK v2 Developer Guide | https://docs.aws.amazon.com/cdk/v2/guide/home.html |
| AWS CLI v2 User Guide | https://docs.aws.amazon.com/cli/latest/userguide/ |
| HIPAA-Eligible Services Reference | https://aws.amazon.com/compliance/hipaa-eligible-services-reference/ |
| Architecting HIPAA on AWS (whitepaper) | https://docs.aws.amazon.com/whitepapers/latest/architecting-hipaa-security-and-compliance-on-aws/ |
No first-party AWS MCP server is in the house stack by default. Drive AWS from Claude Code via the AWS CLI v2 and the v3 SDK (below). The
[CA]Cloud Architect agent owns IaC and account topology; loop it in for any new account or VPC design.
Accounts & IAM (least-privilege)
Every enterprise client gets an isolated AWS account (ideally under an AWS Organizations management account) so blast radius, billing, and a HIPAA BAA stay scoped per engagement. The non-negotiables:
- No root access keys, ever. The root user gets a hardware/virtual MFA and is then locked away. All day-to-day work runs through IAM roles.
- Humans assume roles via IAM Identity Center (SSO), not long-lived IAM users.
- Workloads use roles, not keys — Lambda execution roles, ECS task roles, EC2 instance profiles. Long-lived
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYpairs exist only where a role can't be attached (e.g. a Vercel-hosted Next.js app calling S3), and even then they're scoped to one bucket/action. - Least-privilege policies — start from deny, grant the specific actions on the specific ARNs. No
"Action": "*"on"Resource": "*".
A minimal scoped policy for a Vercel app that only needs to put/get objects in one bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ScopedBucketAccess",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::acme-health-intake/*"
}
]
}Secrets Manager
App secrets (DB credentials, third-party API keys) live in AWS Secrets Manager, not in plaintext env files on the instance. This is the AWS-native analogue of the house infisical guide — when a build is on AWS end-to-end, prefer Secrets Manager (it integrates with RDS rotation and IAM); when the app is Vercel-hosted, Infisical/Vercel env vars remain the default and AWS holds only the infrastructure secrets. Encryption is handled by KMS automatically; see the encryption guide for the at-rest/in-transit baseline.
// lib/aws/secrets.ts
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
export async function getSecret<T = Record<string, string>>(secretId: string): Promise<T> {
const res = await client.send(new GetSecretValueCommand({ SecretId: secretId }));
if (!res.SecretString) throw new Error(`Secret ${secretId} has no string value`);
return JSON.parse(res.SecretString) as T;
}S3 & Presigned URLs
S3 is the AWS object store. Cloudflare R2 is the house default (zero egress fees, S3-compatible API), and most builds never leave R2 — but an enterprise/HIPAA client may require S3 specifically (BAA coverage, s3:ObjectLockConfiguration for WORM retention, SSE-KMS with a customer-managed key, or VPC-gated access). R2 speaks the S3 API, so the v3 @aws-sdk/client-s3 code below works against both; only the endpoint and credentials change.
The pattern for client uploads is the presigned URL: the browser uploads directly to S3, the server never proxies the bytes, and the URL expires. For PHI, the bucket is private with SSE-KMS and the presigned PUT carries the encryption header.
// lib/aws/s3-presign.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.S3_BUCKET!;
/** Presigned PUT for a direct browser upload (PHI: SSE-KMS enforced). */
export async function presignUpload(key: string, contentType: string): Promise<string> {
const cmd = new PutObjectCommand({
Bucket: BUCKET,
Key: key,
ContentType: contentType,
ServerSideEncryption: "aws:kms",
SSEKMSKeyId: process.env.S3_KMS_KEY_ID, // customer-managed key for healthcare
});
return getSignedUrl(s3, cmd, { expiresIn: 300 }); // 5 minutes
}
/** Presigned GET for a time-boxed download (e.g. an intake PDF). */
export async function presignDownload(key: string): Promise<string> {
const cmd = new GetObjectCommand({ Bucket: BUCKET, Key: key });
return getSignedUrl(s3, cmd, { expiresIn: 60 });
}Gotcha: A presigned PUT only succeeds if the request matches the signed parameters exactly. If you sign
ServerSideEncryption: "aws:kms", the browser'sPUTmust send thex-amz-server-side-encryption: aws:kmsheader — otherwise S3 returns403 SignatureDoesNotMatch. Sign exactly the headers the client will send, no more.
Databases — RDS & Aurora Serverless v2
Neon is the house default Postgres — serverless, branch-per-PR, generous free tier, and it's already wired into the neon guide and the app's Drizzle setup. Stay on Neon unless an enterprise client needs something Neon can't offer: a signed BAA, a private VPC with no public endpoint, VPC peering to other AWS resources, or a specific data-residency region.
When AWS is required, the recommendation is Aurora Serverless v2 (PostgreSQL-compatible) over plain RDS for most workloads — it autoscales ACUs with load (including scale-to-near-zero on idle), which mirrors Neon's serverless economics while living inside the client's VPC. Reach for provisioned RDS only when a steady, predictable instance class is cheaper or a feature requires it.
| Concern | House default (Neon) | AWS (Aurora Serverless v2 / RDS) |
|---|---|---|
| Provisioning | Instant, in-console/MCP | CDK/Terraform into a VPC |
| Scaling | Serverless autoscale | Aurora SLv2 ACUs / RDS instance class |
| Branching | Per-PR DB branches | Snapshots/clones (heavier) |
| Connection | Pooled HTTP/WebSocket driver | Standard pg over VPC; use RDS Proxy for Lambda |
| HIPAA BAA | Via Neon's terms | Covered under AWS BAA (Aurora/RDS are HIPAA-eligible) |
| Encryption | TLS + at-rest | TLS + KMS at-rest (required for PHI) |
Drizzle still drives it — point the connection string at the Aurora writer endpoint (sourced from Secrets Manager). For Lambda, put RDS Proxy in front to avoid exhausting connections on cold-start storms.
Compute — Lambda + API Gateway & ECS Fargate
Two compute shapes cover the enterprise cases:
- Lambda + API Gateway — event-driven and bursty work: webhook receivers, scheduled jobs, image processing, a thin API. Pairs with RDS Proxy for database access. Cheapest at low/spiky volume.
- ECS on Fargate — long-running or containerized services that don't fit Lambda's 15-minute / payload limits. This is where the
dockerguide pays off: the same image you build for local dev runs on Fargate. Use it for steady traffic, WebSocket servers, or a healthcare platform that must run inside a VPC alongside Aurora.
Both run with a scoped task/execution role (no embedded keys) and read secrets from Secrets Manager at start. A typical healthcare platform is: CloudFront → ALB → Fargate service (in private subnets) → Aurora Serverless v2 (in isolated subnets) → S3 for documents.
CloudFront & SES
- CloudFront is the CDN/edge layer in front of S3 and the ALB — TLS termination, caching, and a WAF attachment point. For PHI, set the bucket to private and serve through CloudFront with Origin Access Control (OAC) so objects are never publicly reachable. (For Vercel-hosted marketing sites, Vercel's own edge/CDN remains the default — CloudFront is for the AWS-resident enterprise stack.)
- SES is the AWS-native transactional email service. Resend is the house default for transactional/marketing email and stays so for nearly everything. SES comes in only when an enterprise client needs email sending to originate from inside their AWS account/VPC, very high volume at AWS pricing, or BAA-covered email infrastructure. Verify the domain (DKIM/SPF), then request production access to leave the SES sandbox.
Infrastructure as Code (AWS CDK in TypeScript)
All AWS infrastructure is defined as code and reviewed by the [CA] Cloud Architect agent before apply — no click-ops in the console for anything that touches client data. The house preference is the AWS CDK v2 in TypeScript (same language as the app, type-safe constructs); Terraform is the alternative when a client standardizes on it or needs multi-cloud.
npm install -g aws-cdk # CDK v2 CLI (the `cdk` command)
npm install aws-cdk-lib constructs # in-project library
cdk bootstrap aws://ACCOUNT_ID/us-east-1A minimal, HIPAA-leaning stack — a private, encrypted, versioned bucket plus a scoped secret:
// lib/intake-stack.ts
import { Stack, StackProps, Duration, RemovalPolicy } from "aws-cdk-lib";
import { Construct } from "constructs";
import {
Bucket,
BucketEncryption,
BlockPublicAccess,
} from "aws-cdk-lib/aws-s3";
import { Key } from "aws-cdk-lib/aws-kms";
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
export class IntakeStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Customer-managed KMS key with rotation (required posture for PHI).
const key = new Key(this, "IntakeKey", {
enableKeyRotation: true,
removalPolicy: RemovalPolicy.RETAIN,
});
// Private, encrypted, versioned bucket — no public access.
new Bucket(this, "IntakeBucket", {
bucketName: "acme-health-intake",
encryption: BucketEncryption.KMS,
encryptionKey: key,
enforceSSL: true,
versioned: true,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [{ expiration: Duration.days(2555) }], // ~7yr retention
});
// DB credentials in Secrets Manager (consumed by Aurora + the app).
new Secret(this, "DbCredentials", {
secretName: "acme/aurora/credentials",
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: "app" }),
generateStringKey: "password",
excludePunctuation: true,
},
});
}
}cdk synth # emit the CloudFormation template (review this in the [CA] gate)
cdk diff # show the delta against the deployed stack
cdk deploy # apply (only after Cloud Architect review)HIPAA on AWS (Florida healthcare niche)
This is the reason AWS exists in the stack. For any build handling PHI for an APD/AHCA/DCF provider, the baseline is non-negotiable:
- Sign the BAA. Accept the AWS Business Associate Addendum via AWS Artifact (Console → Artifact → Agreements) before any PHI lands in the account. No BAA, no PHI — full stop.
- HIPAA-eligible services only. PHI may only flow through services on the HIPAA-eligible services reference. S3, RDS/Aurora, Lambda, ECS/Fargate, API Gateway, CloudFront, SES, Secrets Manager, KMS are all eligible — but verify each one before use; not every AWS service is covered.
- Encryption at rest — KMS on S3 (SSE-KMS), Aurora/RDS storage encryption, EBS volumes. Use customer-managed keys with rotation for the strongest posture.
- Encryption in transit — TLS everywhere;
enforceSSLon buckets,rds.force_ssl, HTTPS-only CloudFront. See theencryptionguide. - Audit + access — CloudTrail on (immutable log bucket), least-privilege IAM, no public endpoints for PHI stores, VPC isolation for databases.
- Retention — lifecycle rules / Object Lock to meet record-retention rules (Florida healthcare retention can run ~7 years).
Compliance gate: PHI work intersects the
[COMPLIANCE]and[PRIVACY]agents. The BAA, encryption posture, and the HIPAA-eligible-services check are human approval gates — surface them, don't assume.
Environment Variables
# Region (required by every v3 SDK client)
AWS_REGION=us-east-1
# Static credentials — ONLY where a role can't be attached (e.g. a Vercel app
# calling S3). Scope them to one bucket/action. Prefer roles everywhere else.
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=... # server-side only — NEVER ship to the client bundle
# Local dev / CLI: assume a role from a named profile instead of static keys
AWS_PROFILE=acme-enterprise
# App-level config (sourced from Secrets Manager in production)
S3_BUCKET=acme-health-intake
S3_KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/...Add these to
ENV_MASTER.mdand each project's.env.example. In production prefer IAM roles (Lambda/ECS/EC2) and Secrets Manager over static keys; static keys are a last resort and must be rotated. Never exposeAWS_SECRET_ACCESS_KEYto the browser — presign on the server.
CLI Integration (AWS CLI v2)
Install the AWS CLI v2 (the v1 line is end-of-life-track; always v2). Authenticate with a named profile or SSO — never paste root keys.
# Configure a named profile (or `aws configure sso` for IAM Identity Center)
aws configure --profile acme-enterprise
# Verify which identity you're operating as before anything destructive
aws sts get-caller-identity --profile acme-enterprise
# Create a private, versioned bucket with default SSE-KMS
aws s3api create-bucket --bucket acme-health-intake --region us-east-1 \
--profile acme-enterprise
aws s3api put-bucket-versioning --bucket acme-health-intake \
--versioning-configuration Status=Enabled --profile acme-enterprise
# Read a secret (JSON) from Secrets Manager
aws secretsmanager get-secret-value --secret-id acme/aurora/credentials \
--query SecretString --output text --profile acme-enterprise
# Tail a Lambda's logs live
aws logs tail /aws/lambda/intake-handler --follow --profile acme-enterpriseAutomation Workflows
Claude Code slash command: AWS resource audit
.claude/commands/aws-audit.md:
Audit the AWS account for the profile: $ARGUMENTS
1. Run `aws sts get-caller-identity --profile $ARGUMENTS` and confirm the account.
2. List S3 buckets and check each for: public-access block ON, default encryption,
and versioning (`aws s3api get-bucket-encryption|get-public-access-block|get-bucket-versioning`).
3. List IAM users and flag any with active access keys older than 90 days.
4. Confirm CloudTrail is enabled in all regions.
5. Cross-check every service touching PHI against the HIPAA-eligible services list.
6. Output a findings table: resource, issue, severity, remediation. Flag anything
that breaks the HIPAA baseline for the [COMPLIANCE] / [PRIVACY] gate.GitHub Actions: CDK deploy via OIDC (no static keys)
# .github/workflows/cdk-deploy.yml
name: CDK Deploy
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC role assumption
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-cdk-deploy
aws-region: us-east-1
- run: npm ci
- run: npx cdk diff
- run: npx cdk deploy --require-approval neverUse GitHub OIDC to assume a deploy role — no long-lived AWS keys in CI secrets. The role is scoped to the CDK CloudFormation stack only.
Common Use Cases
| Use Case | Approach |
|---|---|
| Default object storage | Cloudflare R2 (house default) — use S3 only if a client requires it |
| Default Postgres | Neon (house default) — use Aurora Serverless v2 only for VPC/BAA/residency needs |
| Default transactional email | Resend (house default) — use SES only for in-VPC/high-volume/BAA email |
| Default app hosting | Vercel (house default) — use ECS Fargate/Lambda for VPC-resident services |
| Direct browser upload | S3 presigned PUT (@aws-sdk/s3-request-presigner), 5-min expiry |
| Time-boxed file download | S3 presigned GET, short expiry |
| Infrastructure provisioning | AWS CDK v2 (TypeScript), reviewed by [CA] |
| App/infra secrets | AWS Secrets Manager + IAM role (ties to infisical/encryption) |
| HIPAA PHI store | Private S3 + SSE-KMS + Aurora-in-VPC, BAA via AWS Artifact |
| Bursty / event-driven compute | Lambda + API Gateway (+ RDS Proxy for DB) |
| Long-running / containerized | ECS Fargate (ties to docker guide) |
Troubleshooting
| Issue | Fix |
|---|---|
SignatureDoesNotMatch on presigned PUT | Client must send exactly the signed headers (e.g. x-amz-server-side-encryption); sign only what the client will send |
403 AccessDenied on S3 GET | Check the IAM policy ARN matches the bucket, and that bucket policy / OAC isn't blocking; confirm AWS_REGION is correct |
ExpiredToken from the SDK | SSO/role session expired — re-run aws sso login --profile ... or refresh the assumed role |
| CLI uses the wrong account | Pass --profile (or set AWS_PROFILE); verify with aws sts get-caller-identity |
| Lambda DB connection exhaustion | Front the database with RDS Proxy; don't open a raw pool per invocation |
cdk deploy fails on bootstrap | Run cdk bootstrap aws://ACCOUNT/REGION once per account+region |
| SES emails go to spam / sandbox | Verify domain DKIM/SPF and request production access to leave the SES sandbox |
| PHI on a non-eligible service | Stop — move PHI only onto services on the HIPAA-eligible list, and confirm the BAA is signed in AWS Artifact |
KMS AccessDeniedException | The IAM role needs kms:GenerateDataKey/kms:Decrypt on the key, and the key policy must allow the role |