← Back to dashboard

AWS Integration Guide

Text
 █████╗ ██╗    ██╗███████╗
██╔══██╗██║    ██║██╔════╝
███████║██║ █╗ ██║███████╗
██╔══██║██║███╗██║╚════██║
██║  ██║╚███╔███╔╝███████║
╚═╝  ╚═╝ ╚══╝╚══╝ ╚══════╝

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-stack MCP (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

AWS now ships official MCP servers, but none is wired into the house .mcp.json by default. AWS publishes the open-source awslabs/mcp collection plus AWS-managed remote endpoints — the read-only AWS Knowledge MCP Server (https://knowledge-mcp.global.api.aws, no auth) for live AWS docs and API references, and the AWS API MCP Server (https://aws-mcp.us-east-1.api.aws/mcp, preview) alongside Amazon EKS/ECS servers that can actually drive an account. The Knowledge server is a safe read-only companion worth enabling; for anything that mutates a client account, codeAmani still goes through the AWS CLI v2 + the v3 SDK + CDK under the [CA] Cloud Architect gate. If you do enable a write-capable AWS-managed server, note it auto-injects the aws:ViaAWSMCPService / aws:CalledViaAWSMCP IAM context keys, so least-privilege policies can distinguish agent-driven calls from human-initiated ones. 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_KEY pairs 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:

JSON
{
  "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.

TypeScript
// 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.

TypeScript
// 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's PUT must send the x-amz-server-side-encryption: aws:kms header — otherwise S3 returns 403 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.

ConcernHouse default (Neon)AWS (Aurora Serverless v2 / RDS)
ProvisioningInstant, in-console/MCPCDK/Terraform into a VPC
ScalingServerless autoscaleAurora SLv2 ACUs / RDS instance class
BranchingPer-PR DB branchesSnapshots/clones (heavier)
ConnectionPooled HTTP/WebSocket driverStandard pg over VPC; use RDS Proxy for Lambda
HIPAA BAAVia Neon's termsCovered under AWS BAA (Aurora/RDS are HIPAA-eligible)
EncryptionTLS + at-restTLS + 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 docker guide 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.

Runtimes: target a current managed runtime. Lambda's Node.js line on Amazon Linux 2023 is nodejs22.x, nodejs24.x (the console default), and nodejs26.x; the AL2-based nodejs18.x and earlier are deprecated. In the CDK, prefer Runtime.NODEJS_LATEST (or pin NODEJS_24_X) so functions stay on a supported line. On Fargate, pin the base image to a supported Node.js LTS (22 or 24).

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.

Bash
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-1

A minimal, HIPAA-leaning stack — a private, encrypted, versioned bucket plus a scoped secret:

TypeScript
// 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,
      },
    });
  }
}
Bash
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:

  1. 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.
  2. 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.
  3. Encryption at rest — KMS on S3 (SSE-KMS), Aurora/RDS storage encryption, EBS volumes. Use customer-managed keys with rotation for the strongest posture.
  4. Encryption in transit — TLS everywhere; enforceSSL on buckets, rds.force_ssl, HTTPS-only CloudFront. See the encryption guide.
  5. Audit + access — CloudTrail on (immutable log bucket), least-privilege IAM, no public endpoints for PHI stores, VPC isolation for databases.
  6. 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

Bash
# 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.md and 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 expose AWS_SECRET_ACCESS_KEY to 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.

Bash
# 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-enterprise

Automation Workflows

Claude Code slash command: AWS resource audit

.claude/commands/aws-audit.md:

Markdown
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)

YAML
# .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@v7
      - uses: actions/setup-node@v7
        with: { node-version: '24' }
      - uses: aws-actions/configure-aws-credentials@v6
        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 never

Use 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 CaseApproach
Default object storageCloudflare R2 (house default) — use S3 only if a client requires it
Default PostgresNeon (house default) — use Aurora Serverless v2 only for VPC/BAA/residency needs
Default transactional emailResend (house default) — use SES only for in-VPC/high-volume/BAA email
Default app hostingVercel (house default) — use ECS Fargate/Lambda for VPC-resident services
Direct browser uploadS3 presigned PUT (@aws-sdk/s3-request-presigner), 5-min expiry
Time-boxed file downloadS3 presigned GET, short expiry
Infrastructure provisioningAWS CDK v2 (TypeScript), reviewed by [CA]
App/infra secretsAWS Secrets Manager + IAM role (ties to infisical/encryption)
HIPAA PHI storePrivate S3 + SSE-KMS + Aurora-in-VPC, BAA via AWS Artifact
Bursty / event-driven computeLambda + API Gateway (+ RDS Proxy for DB)
Long-running / containerizedECS Fargate (ties to docker guide)

Troubleshooting

IssueFix
SignatureDoesNotMatch on presigned PUTClient must send exactly the signed headers (e.g. x-amz-server-side-encryption); sign only what the client will send
403 AccessDenied on S3 GETCheck the IAM policy ARN matches the bucket, and that bucket policy / OAC isn't blocking; confirm AWS_REGION is correct
ExpiredToken from the SDKSSO/role session expired — re-run aws sso login --profile ... or refresh the assumed role
CLI uses the wrong accountPass --profile (or set AWS_PROFILE); verify with aws sts get-caller-identity
Lambda DB connection exhaustionFront the database with RDS Proxy; don't open a raw pool per invocation
cdk deploy fails on bootstrapRun cdk bootstrap aws://ACCOUNT/REGION once per account+region
SES emails go to spam / sandboxVerify domain DKIM/SPF and request production access to leave the SES sandbox
PHI on a non-eligible serviceStop — move PHI only onto services on the HIPAA-eligible list, and confirm the BAA is signed in AWS Artifact
KMS AccessDeniedExceptionThe IAM role needs kms:GenerateDataKey/kms:Decrypt on the key, and the key policy must allow the role