← Back to dashboard
awshostingfresh

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

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_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.

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@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 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