Claude Code for SST Ion: Full-Stack Apps on AWS with Infrastructure as TypeScript — Claude Skills 360 Blog
Blog / DevOps / Claude Code for SST Ion: Full-Stack Apps on AWS with Infrastructure as TypeScript
DevOps

Claude Code for SST Ion: Full-Stack Apps on AWS with Infrastructure as TypeScript

Published: February 11, 2027
Read time: 9 min read
By: Claude Skills 360

SST Ion deploys full-stack TypeScript applications to AWS using Pulumi and Ion — no CDK or CloudFormation. sst.config.ts defines infrastructure as TypeScript objects: new sst.aws.Function, new sst.aws.ApiGatewayV2, new sst.aws.Nextjs. sst dev runs a local dev server with live API routes connected to real AWS resources — changes to Lambda code deploy in seconds, not minutes. Resource.link() injects resource ARNs and URLs as typed environment variables — no manual SSM lookups. Secrets from AWS Secrets Manager or SSM Parameter Store are available as new sst.Secret("ApiKey") with automatic injection. Frontends deploy to CloudFront with Lambda@Edge for server-side rendering. Claude Code generates SST Ion configurations, Lambda function handlers, API route definitions, database bindings, and the CI/CD pipeline configurations for production SST deployments.

CLAUDE.md for SST Ion Projects

## SST Ion Stack
- Version: sst >= 3.x (Ion — uses Pulumi, not CDK)
- Config: sst.config.ts — defines app and stacks
- Dev: sst dev — live Lambda with local code changes, real AWS resources
- Resources: new sst.aws.Function, ApiGatewayV2, Nextjs, Bucket, Vector, etc.
- Linking: link: [myBucket, myDb] → available as Resource.MyBucket.name in Lambda
- Secrets: new sst.Secret("Key") — stored in SSM, injected automatically
- Deploy: sst deploy --stage prod — immutable stages for dev/staging/prod

sst.config.ts

/// <reference path="./.sst/platform/config.d.ts" />

export default $config({
  app(input) {
    return {
      name: "order-platform",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
    }
  },

  async run() {
    // Secrets — stored in SSM Parameter Store
    const stripeKey = new sst.Secret("StripeSecretKey")
    const databaseUrl = new sst.Secret("DatabaseUrl")

    // S3 bucket for order attachments
    const uploadsBucket = new sst.aws.Bucket("UploadsBucket", {
      cors: true,
      public: false,
    })

    // DynamoDB for sessions
    const sessionsTable = new sst.aws.Dynamo("SessionsTable", {
      fields: {
        pk: "string",
        sk: "string",
        gsi1pk: "string",
      },
      primaryIndex: { hashKey: "pk", rangeKey: "sk" },
      globalIndexes: {
        gsi1: { hashKey: "gsi1pk", rangeKey: "sk" },
      },
      ttl: "ttl",
    })

    // API Gateway v2 with Lambda routes
    const api = new sst.aws.ApiGatewayV2("OrdersApi", {
      cors: {
        allowOrigins: ["https://app.example.com"],
        allowMethods: ["GET", "POST", "PUT", "DELETE"],
        allowHeaders: ["Authorization", "Content-Type"],
      },
    })

    // Lambda function for order processing
    const processOrderFn = new sst.aws.Function("ProcessOrderFunction", {
      handler: "packages/functions/src/order-processor.handler",
      timeout: "5 minutes",
      memory: "512 MB",
      link: [uploadsBucket, sessionsTable, stripeKey, databaseUrl],
      environment: {
        STAGE: $app.stage,
      },
    })

    // API routes
    api.route("GET /orders", {
      handler: "packages/functions/src/orders.list",
      link: [databaseUrl, sessionsTable],
    })

    api.route("POST /orders", {
      handler: "packages/functions/src/orders.create",
      link: [databaseUrl, sessionsTable, uploadsBucket, stripeKey],
      timeout: "30 seconds",
    })

    api.route("POST /orders/{id}/process", {
      function: processOrderFn,
    })

    // Next.js site deployed to CloudFront + Lambda@Edge
    const site = new sst.aws.Nextjs("OrdersDashboard", {
      path: "packages/web",
      link: [api, uploadsBucket, databaseUrl, stripeKey],
      warm: $app.stage === "production" ? 5 : 0,
      environment: {
        NEXT_PUBLIC_API_URL: api.url,
        NEXT_PUBLIC_STAGE: $app.stage,
      },
    })

    // Output URLs
    return {
      apiUrl: api.url,
      siteUrl: site.url,
    }
  },
})

Lambda Handler with Resource Linking

// packages/functions/src/orders.ts — Lambda with SST Resource bindings
import { Resource } from "sst"
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import type { APIGatewayProxyHandlerV2 } from "aws-lambda"

// Resource.* provides type-safe names/ARNs injected at deploy time
const dynamo = new DynamoDBClient({})
const s3 = new S3Client({})

export const list: APIGatewayProxyHandlerV2 = async (event) => {
  const customerId = event.queryStringParameters?.customer_id
  if (!customerId) {
    return { statusCode: 400, body: JSON.stringify({ error: "Missing customer_id" }) }
  }

  const db = await import("./lib/db")
  const orders = await db.listOrders(customerId)

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(orders),
  }
}

export const create: APIGatewayProxyHandlerV2 = async (event) => {
  const body = JSON.parse(event.body ?? "{}")
  const { customerId, items } = body

  if (!customerId || !items?.length) {
    return { statusCode: 422, body: JSON.stringify({ error: "customerId and items required" }) }
  }

  // Resource.DatabaseUrl — injected by SST from linked Secret
  const db = await import("./lib/db")
  const order = await db.createOrder(customerId, items)

  // Generate pre-signed S3 URL for attachment
  const uploadUrl = await getSignedUrl(
    s3,
    new PutObjectCommand({
      // Resource.UploadsBucket — injected by SST, fully typed
      Bucket: Resource.UploadsBucket.name,
      Key: `orders/${order.id}/attachment`,
    }),
    { expiresIn: 3600 }
  )

  return {
    statusCode: 201,
    body: JSON.stringify({ order, uploadUrl }),
  }
}

Live Development

# Start SST dev — live Lambda with hot reload
npx sst dev

# First run prompts for AWS credentials and stage name
# Subsequent runs connect to existing stage resources

# sst dev starts:
# - Local API proxy (localhost:4321 → real API Gateway)
# - Live Lambda runtime (code changes deploy in ~1s)
# - Real DynamoDB, S3, RDS — no local emulation

# Check deployed outputs
npx sst output

# Deploy to production
npx sst deploy --stage production

# Remove development environment
npx sst remove --stage dev

CI/CD Pipeline

# .github/workflows/deploy.yml — SST Ion CI/CD
name: Deploy

on:
  push:
    branches: [main]
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC for AWS auth
      contents: read
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
          aws-region: us-east-1

      # PR deployments: ephemeral stage per PR
      - name: Deploy PR preview
        if: github.event_name == 'pull_request'
        run: npx sst deploy --stage pr-${{ github.event.number }}
        env:
          SST_AWS_REGION: us-east-1

      # Main branch: deploy to staging
      - name: Deploy to staging
        if: github.ref == 'refs/heads/main'
        run: npx sst deploy --stage staging
        env:
          SST_AWS_REGION: us-east-1

      - name: Print outputs
        run: npx sst output

Cron and Queue

// sst.config.ts additions — Cron and Queue resources
const orderQueue = new sst.aws.Queue("OrderProcessingQueue", {
  visibilityTimeout: "5 minutes",
})

orderQueue.subscribe({
  handler: "packages/functions/src/queue-processor.handler",
  link: [databaseUrl, stripeKey],
  timeout: "4 minutes",
})

// Cron job for daily reports
new sst.aws.Cron("DailyReportCron", {
  schedule: "cron(0 9 * * ? *)",  // 9am UTC daily
  job: {
    handler: "packages/functions/src/reports.daily",
    link: [databaseUrl],
    timeout: "5 minutes",
  },
})

For the Serverless Framework v4 alternative that also deploys Lambda with a YAML configuration and has a longer history and plugin ecosystem, the serverless functions guide covers Lambda function patterns. For the Pulumi direct approach without SST’s application-layer abstractions when more flexible AWS resource control is needed, see the Pulumi TypeScript patterns for custom resource configuration. The Claude Skills 360 bundle includes SST Ion skill sets covering sst.config.ts, Lambda handlers, and live development. Start with the free tier to try SST Ion configuration generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free