Claude Skill

infra-iac-sst

SST (Ion) infrastructure-as-code — TypeScript-first serverless on AWS with Pulumi, resource linking, and live Lambda dev

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_infra-iac-sst_skills_infra-iac-sst-3a51ef5.zip · 18 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-iac-sst/skills/infra-iac-sst
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SST (Ion) Patterns

Quick Guide: SST v3 (Ion) is TypeScript-first infrastructure-as-code for AWS, powered by Pulumi/Terraform (not CDK/CloudFormation). Define your entire app in sst.config.ts using high-level components (sst.aws.Function, sst.aws.ApiGatewayV2, sst.aws.Bucket, sst.aws.Dynamo, etc.). Use resource linking (link: [bucket] + Resource.MyBucket.name) for type-safe, permission-aware access between components. Use sst dev for live Lambda development with sub-10ms reloads. Use $app.stage for multi-environment isolation. Use transform to customize underlying Pulumi resources.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use resource linking (link + Resource.*) to connect components — NEVER hardcode ARNs, table names, or bucket names)

(You MUST use $app.stage for environment isolation — NEVER share resources across stages without explicit intent)

(You MUST use sst dev for local development — it provides live Lambda proxying with sub-10ms reloads against real AWS resources)

(You MUST use sst secret set for secrets — NEVER put secrets in sst.config.ts, .env files committed to git, or environment variables)

(You MUST use transform to customize underlying resources — NEVER reach for raw Pulumi resources when an SST component exists)

</critical_requirements>


Examples

  • Core Patterns — sst.config.ts structure, Function, resource linking, $app globals, secrets, multi-stage
  • API & Data — ApiGatewayV2, Dynamo, Bucket, Queue, Topic, Cron, authorization
  • Deployment & DevOps — sst deploy, CI/CD, removal policies, transforms, Vpc, Cluster, frontend frameworks
  • Quick Reference — CLI commands, component cheat sheet, global helpers, named constants

Auto-detection: SST, sst.config.ts, sst.aws.Function, sst.aws.ApiGatewayV2, sst.aws.Bucket, sst.aws.Dynamo, sst.aws.Queue, sst.aws.Topic, sst.aws.Cron, sst.aws.Nextjs, sst.aws.Remix, sst.aws.Astro, sst.aws.StaticSite, sst.aws.Vpc, sst.aws.Cluster, sst.aws.Postgres, sst.aws.Router, sst.Linkable, Resource from sst, sst dev, sst deploy, sst remove, sst secret, $app.stage, $transform, $concat, $interpolate, resource linking, live Lambda, Ion

When to use:

  • Defining AWS infrastructure in TypeScript with high-level components
  • Deploying serverless applications (Lambda, API Gateway, DynamoDB, S3, SQS, SNS)
  • Deploying full-stack apps (Next.js, Remix, Astro, SvelteKit, SolidStart on AWS)
  • Setting up live Lambda development with real AWS resources
  • Managing multi-stage environments (dev, staging, production)
  • Connecting infrastructure components with type-safe resource linking

When NOT to use:

  • Multi-cloud infrastructure spanning many providers (SST is AWS-focused with limited Cloudflare support)
  • Existing Terraform/Pulumi codebases where SST abstraction adds no value
  • Projects that need container-only deployments without serverless components

Key patterns covered:

  • sst.config.ts structure (app() + run() functions)
  • Resource linking: link property + Resource.* SDK
  • Live Lambda development with sst dev
  • AWS components: Function, ApiGatewayV2, Dynamo, Bucket, Queue, Topic, Cron
  • Frontend deployments: Nextjs, Remix, Astro, StaticSite
  • Multi-stage isolation with $app.stage
  • Transforms for customizing underlying Pulumi resources
  • Secrets management with sst secret
  • Custom linkables with sst.Linkable and Linkable.wrap
  • Global helpers: $app, $dev, $concat, $interpolate, $resolve, $transform



<decision_framework>

Decision Framework

Choosing an SST Component

What are you building?
  |
  +-- HTTP API
  |     +-- Simple routes with Lambda handlers --> sst.aws.ApiGatewayV2
  |     +-- Need WebSocket support --> sst.aws.ApiGatewayWebSocket
  |     +-- URL routing / CDN --> sst.aws.Router
  |
  +-- Data storage
  |     +-- Key-value / document data --> sst.aws.Dynamo
  |     +-- Relational data with SQL --> sst.aws.Postgres
  |     +-- File/blob storage --> sst.aws.Bucket
  |
  +-- Async processing
  |     +-- Point-to-point messaging --> sst.aws.Queue (SQS)
  |     +-- Fan-out to multiple subscribers --> sst.aws.Topic (SNS)
  |     +-- Scheduled tasks --> sst.aws.Cron
  |
  +-- Compute
  |     +-- Serverless function --> sst.aws.Function
  |     +-- Container workload --> sst.aws.Cluster + sst.aws.Service
  |     +-- Long-running background job --> sst.aws.Function (up to 15min)
  |
  +-- Full-stack frontend
        +-- Next.js --> sst.aws.Nextjs
        +-- Remix --> sst.aws.Remix
        +-- Astro --> sst.aws.Astro
        +-- SvelteKit --> sst.aws.SvelteKit
        +-- SolidStart --> sst.aws.SolidStart
        +-- Static HTML/JS --> sst.aws.StaticSite

When to Use Transforms vs Raw Pulumi

Need to set a property on an SST component?
  |
  +-- Property exists on the SST component args --> Use the SST property directly
  |
  +-- Property exists only on the underlying AWS resource --> Use transform
  |
  +-- Need to set a default across ALL instances of a component --> Use $transform()
  |
  +-- No SST component exists for this AWS service --> Use raw Pulumi resource
        +-- Need to link it? --> Use sst.Linkable.wrap() or new sst.Linkable()

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Hardcoding ARNs, table names, or bucket names instead of using resource linking (link + Resource.*) — defeats SST's type-safe wiring and breaks across stages
  • Sharing resource names across stages without $app.stage prefix — causes resource conflicts and accidental cross-stage access
  • Putting secrets in sst.config.ts or committed .env files instead of using sst secret set — secrets leak to version control
  • Using sst dev for shared environments (staging, production) — stubs proxy to a single developer's machine, breaking for everyone else
  • Creating raw Pulumi resources when an equivalent sst.aws.* component exists — loses SST's linking, permissions, and defaults

Medium Priority Issues:

  • Not setting removal: "retain" and protect: true for production stages — accidental sst remove deletes all data
  • Missing /// <reference path="./.sst/platform/config.d.ts" /> at top of sst.config.ts — loses type checking for $app, $transform, etc.
  • Using .env files for secrets instead of sst secret — .env files aren't encrypted and must be managed manually per stage
  • Not running sst deploy after finishing sst dev session — stubs remain deployed and Lambda invocations timeout

Common Mistakes:

  • Forgetting that sst dev stubs persist after you stop the process — always redeploy or re-run sst dev
  • Using $app.stage in runtime code — it's only available in sst.config.ts, use Resource.* or env vars for runtime stage awareness
  • Trying to use SST resource linking in client-side frontend code — links are server-side only (SSR functions, API routes)
  • Expecting sst dev to emulate AWS locally — it doesn't; it proxies to real AWS resources in your account

Gotchas & Edge Cases:

  • Pulumi Outputs cannot be used directly in string templates — use $concat() or $interpolate instead of template literals
  • sst dev multiplexer starts frontends automatically — you don't need to run next dev or vite dev separately
  • FIFO queues require .fifo suffix in names — SST handles this automatically but be aware when referencing externally
  • .env and .env.<stage> files are loaded automatically — .env takes precedence over stage-specific files
  • Frontend framework links (Next.js, Remix) only work server-side — client components cannot access Resource.*
  • Layers specified in Function config are not applied during sst dev — local execution skips layers
  • The removal setting in app() controls what happens when you run sst remove — "remove" deletes resources, "retain" keeps them, "retain-all" keeps everything including logs

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use resource linking (link + Resource.*) to connect components — NEVER hardcode ARNs, table names, or bucket names)

(You MUST use $app.stage for environment isolation — NEVER share resources across stages without explicit intent)

(You MUST use sst dev for local development — it provides live Lambda proxying with sub-10ms reloads against real AWS resources)

(You MUST use sst secret set for secrets — NEVER put secrets in sst.config.ts, .env files committed to git, or environment variables)

(You MUST use transform to customize underlying resources — NEVER reach for raw Pulumi resources when an SST component exists)

Failure to follow these rules will cause cross-stage resource conflicts, leaked secrets, broken type safety, and unnecessary infrastructure complexity.

</critical_reminders>

Files (skills)
  • examples
    • api-data.md 8.1 KB
      # SST (Ion) — API & Data Patterns
      
      > API Gateway, DynamoDB, S3, Queue, Topic, and Cron patterns. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Related examples:**
      
      - [Core Patterns](core.md) — sst.config.ts, resource linking, live dev, secrets
      - [Deployment & DevOps](deployment.md) — CI/CD, transforms, removal policies, VPC
      
      ---
      
      ## ApiGatewayV2 — HTTP API
      
      ```typescript
      async run() {
        const table = new sst.aws.Dynamo("Notes", {
          fields: { userId: "string", noteId: "string" },
          primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
        });
      
        const api = new sst.aws.ApiGatewayV2("Api", {
          cors: true,
          domain: $app.stage === "production" ? "api.example.com" : undefined,
          accessLog: { retention: "1 month" },
        });
      
        // Add routes — handler string or object with config
        api.route("GET /notes", {
          handler: "src/notes/list.handler",
          link: [table],
        });
        api.route("POST /notes", {
          handler: "src/notes/create.handler",
          link: [table],
          memory: "512 MB",
        });
        api.route("GET /notes/{id}", "src/notes/get.handler");
        api.route("$default", "src/fallback.handler"); // Catch-all route
      
        return { apiUrl: api.url };
      }
      ```
      
      **Why good:** Each route gets its own Lambda function (fine-grained scaling and permissions), `$default` catches unmatched routes, custom domain only in production, access logs for debugging
      
      ---
      
      ## ApiGatewayV2 — Authorization
      
      ### JWT Authorization
      
      ```typescript
      const api = new sst.aws.ApiGatewayV2("Api");
      
      const jwtAuth = api.addAuthorizer({
        name: "jwt",
        jwt: {
          issuer: "https://auth.example.com/",
          audiences: ["https://api.example.com"],
        },
      });
      
      api.route("GET /public", "src/public.handler"); // No auth
      api.route("GET /private", "src/private.handler", {
        auth: { jwt: { authorizer: jwtAuth.id } },
      });
      api.route("GET /admin", "src/admin.handler", {
        auth: { jwt: { authorizer: jwtAuth.id, scopes: ["admin:read"] } },
      });
      ```
      
      ### IAM Authorization
      
      ```typescript
      api.route("POST /internal", "src/internal.handler", {
        auth: { iam: true },
      });
      ```
      
      ### Lambda Authorizer
      
      ```typescript
      const customAuth = api.addAuthorizer({
        name: "custom",
        lambda: { function: "src/authorizer.handler" },
      });
      
      api.route("GET /protected", "src/protected.handler", {
        auth: { lambda: customAuth.id },
      });
      ```
      
      ---
      
      ## Dynamo — DynamoDB Table
      
      ```typescript
      const table = new sst.aws.Dynamo("Orders", {
        fields: {
          orderId: "string",
          customerId: "string",
          createdAt: "number",
        },
        primaryIndex: { hashKey: "orderId" },
        globalIndexes: {
          CustomerIndex: {
            hashKey: "customerId",
            rangeKey: "createdAt",
          },
        },
        stream: "new-and-old-images",
        deletionProtection: $app.stage === "production",
      });
      
      // Subscribe to stream events
      table.subscribe("OrderProcessor", "src/process-order.handler", {
        filters: [{ eventName: ["INSERT"] }],
      });
      ```
      
      **Why good:** Global secondary index for querying by customer, stream captures all changes, subscriber filters to only process new inserts, deletion protection in production
      
      ```typescript
      // BAD: No indexes, no stream, no protection
      const table = new sst.aws.Dynamo("Orders", {
        fields: { orderId: "string" },
        primaryIndex: { hashKey: "orderId" },
      });
      ```
      
      **Why bad:** No secondary indexes means table scans for any non-PK query, no stream means no reactive processing, no deletion protection in production
      
      ---
      
      ## Dynamo — TTL (Auto-Expiry)
      
      ```typescript
      const sessions = new sst.aws.Dynamo("Sessions", {
        fields: { sessionId: "string" },
        primaryIndex: { hashKey: "sessionId" },
        ttl: "expiresAt", // DynamoDB auto-deletes when expiresAt < now
      });
      ```
      
      ```typescript
      // Runtime — set TTL value
      const SESSION_TTL_SECONDS = 3_600;
      
      await client.send(
        new PutItemCommand({
          TableName: Resource.Sessions.name,
          Item: {
            sessionId: { S: id },
            expiresAt: {
              N: String(Math.floor(Date.now() / 1000) + SESSION_TTL_SECONDS),
            },
          },
        }),
      );
      ```
      
      ---
      
      ## Bucket — S3 Storage
      
      ```typescript
      const uploads = new sst.aws.Bucket("Uploads", {
        cors: {
          allowOrigins: ["https://example.com"],
          allowMethods: ["GET", "PUT"],
          allowHeaders: ["content-type"],
          maxAge: "1 day",
        },
      });
      
      // Subscribe to object events
      uploads.notify({
        notifications: [
          {
            name: "ImageProcessor",
            function: "src/process-image.handler",
            events: ["s3:ObjectCreated:*"],
            filterPrefix: "images/",
          },
        ],
      });
      ```
      
      ### Public Bucket
      
      ```typescript
      // Public read access for all objects (static assets, public downloads)
      const assets = new sst.aws.Bucket("Assets", {
        access: "public",
      });
      ```
      
      ---
      
      ## Queue — SQS
      
      ```typescript
      const dlq = new sst.aws.Queue("EmailDLQ");
      
      const emailQueue = new sst.aws.Queue("EmailQueue", {
        visibilityTimeout: "5 minutes",
        dlq: { queue: dlq.arn, retry: 3 },
      });
      
      // Subscribe a handler to process messages
      emailQueue.subscribe("EmailSender", "src/send-email.handler");
      ```
      
      ### FIFO Queue
      
      ```typescript
      const orderQueue = new sst.aws.Queue("OrderQueue", {
        fifo: { contentBasedDeduplication: true },
      });
      
      orderQueue.subscribe("OrderProcessor", "src/process-order.handler");
      ```
      
      ### Sending Messages (Runtime)
      
      ```typescript
      import { Resource } from "sst";
      import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
      
      const sqs = new SQSClient({});
      
      export async function handler() {
        await sqs.send(
          new SendMessageCommand({
            QueueUrl: Resource.EmailQueue.url,
            MessageBody: JSON.stringify({ to: "user@example.com", subject: "Hello" }),
          }),
        );
      }
      ```
      
      ---
      
      ## Cron — Scheduled Tasks
      
      ```typescript
      // Rate-based schedule
      new sst.aws.Cron("DailyCleanup", {
        function: "src/cleanup.handler",
        schedule: "rate(1 day)",
      });
      
      // Cron expression (UTC)
      new sst.aws.Cron("WeeklyReport", {
        function: {
          handler: "src/report.handler",
          timeout: "5 minutes",
          memory: "2048 MB",
        },
        schedule: "cron(0 9 ? * MON *)", // Every Monday at 9:00 AM UTC
      });
      
      // Disable in dev
      new sst.aws.Cron("HourlySync", {
        function: "src/sync.handler",
        schedule: "rate(1 hour)",
        enabled: $app.stage === "production",
      });
      ```
      
      **Why good:** Cron disabled in non-production stages to avoid unnecessary executions, custom timeout/memory for heavy report, rate expression for simple intervals
      
      ---
      
      ## Function — Direct Lambda
      
      ```typescript
      // Function with URL endpoint (no API Gateway)
      const webhook = new sst.aws.Function("StripeWebhook", {
        handler: "src/webhook.handler",
        url: {
          cors: {
            allowOrigins: ["https://stripe.com"],
            allowMethods: ["POST"],
          },
        },
        timeout: "60 seconds",
        link: [ordersTable],
      });
      
      // Streaming response
      const streamer = new sst.aws.Function("StreamResponse", {
        handler: "src/stream.handler",
        url: true,
        streaming: true,
      });
      
      // ARM64 for cost savings (~20% cheaper, often faster)
      const compute = new sst.aws.Function("Compute", {
        handler: "src/compute.handler",
        architecture: "arm64",
        memory: "2048 MB",
      });
      ```
      
      ---
      
      ## Combining Components
      
      ```typescript
      async run() {
        // Data layer
        const table = new sst.aws.Dynamo("Tasks", {
          fields: { taskId: "string", status: "string" },
          primaryIndex: { hashKey: "taskId" },
          globalIndexes: {
            StatusIndex: { hashKey: "status" },
          },
          stream: "new-image",
        });
      
        const bucket = new sst.aws.Bucket("Files");
      
        // Async processing
        const queue = new sst.aws.Queue("TaskQueue");
        queue.subscribe("TaskWorker", {
          handler: "src/worker.handler",
          link: [table, bucket],
          timeout: "5 minutes",
        });
      
        // API layer
        const api = new sst.aws.ApiGatewayV2("Api");
        api.route("GET /tasks", {
          handler: "src/tasks/list.handler",
          link: [table],
        });
        api.route("POST /tasks", {
          handler: "src/tasks/create.handler",
          link: [table, queue],
        });
        api.route("POST /tasks/{id}/upload", {
          handler: "src/tasks/upload.handler",
          link: [bucket],
        });
      
        // React to changes
        table.subscribe("TaskNotifier", "src/notify.handler", {
          filters: [{ eventName: ["INSERT", "MODIFY"] }],
        });
      
        return { apiUrl: api.url };
      }
      ```
      
      **Why good:** Each handler gets only the permissions it needs via `link`, async work offloaded to queue, stream subscriber reacts to data changes, single `return` exposes the API URL
      
    • core.md 8 KB
      # SST (Ion) — Core Patterns
      
      > Core configuration, resource linking, live dev, and secrets patterns. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Related examples:**
      
      - [API & Data](api-data.md) — ApiGatewayV2, Dynamo, Bucket, Queue, Topic, Cron
      - [Deployment & DevOps](deployment.md) — CI/CD, transforms, removal policies, VPC, frontend frameworks
      
      ---
      
      ## sst.config.ts Structure
      
      ```typescript
      /// <reference path="./.sst/platform/config.d.ts" />
      
      export default $config({
        app(input) {
          return {
            name: "my-app",
            home: "aws",
            removal: input.stage === "production" ? "retain" : "remove",
            protect: input.stage === "production",
            providers: {
              aws: { region: "us-east-1" },
            },
          };
        },
        async run() {
          // All resources defined here
          const bucket = new sst.aws.Bucket("Uploads");
      
          const api = new sst.aws.Function("Api", {
            handler: "src/api.handler",
            link: [bucket],
            url: true,
          });
      
          // Returned values become outputs in .sst/outputs.json
          return {
            apiUrl: api.url,
            bucketName: bucket.name,
          };
        },
      });
      ```
      
      **Why good:** `app()` configures metadata and stage-aware policies (retain prod data, protect prod from accidental removal), `run()` defines all resources with TypeScript, return values create CLI-accessible outputs
      
      ```typescript
      // BAD: Missing stage-aware policies
      export default $config({
        app(input) {
          return {
            name: "my-app",
            home: "aws",
            // No removal or protect — production data can be accidentally deleted
          };
        },
        async run() {
          const bucket = new sst.aws.Bucket("Uploads");
          // No return — outputs not available to other tools
        },
      });
      ```
      
      **Why bad:** No `removal: "retain"` for production means `sst remove` deletes all S3 data, no `protect` means accidental removal is possible, no return value means no outputs for scripts or CI
      
      ---
      
      ## Resource Linking — Infrastructure Side
      
      ```typescript
      // sst.config.ts
      async run() {
        const table = new sst.aws.Dynamo("Notes", {
          fields: { userId: "string", noteId: "string" },
          primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
        });
      
        const bucket = new sst.aws.Bucket("Attachments");
      
        // Link grants IAM permissions AND injects type-safe references
        new sst.aws.Function("Api", {
          handler: "src/api.handler",
          link: [table, bucket],
        });
      
        // Link to frontend frameworks (server-side only)
        new sst.aws.Nextjs("Web", {
          link: [table, bucket],
        });
      }
      ```
      
      **Why good:** `link` automatically generates IAM policies (Function gets DynamoDB + S3 access), injects resource metadata into function bundle, and creates `sst-env.d.ts` type definitions
      
      ---
      
      ## Resource Linking — Runtime Side
      
      ```typescript
      // src/api.ts — handler code
      import { Resource } from "sst";
      import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
      
      const client = new DynamoDBClient({});
      
      export async function handler(event: unknown) {
        // Resource.Notes.name is the DynamoDB table name — type-safe
        await client.send(
          new PutItemCommand({
            TableName: Resource.Notes.name,
            Item: {
              userId: { S: "user-123" },
              noteId: { S: "note-456" },
            },
          }),
        );
      
        // Resource.Attachments.name is the S3 bucket name
        console.log("Bucket:", Resource.Attachments.name);
      
        return { statusCode: 200, body: "Created" };
      }
      ```
      
      **Why good:** `Resource.*` is fully typed (autocomplete works), no hardcoded table/bucket names, no manual environment variable wiring, permissions already granted via `link`
      
      ```typescript
      // BAD: Hardcoded resource names
      const TABLE_NAME = "my-app-production-Notes";
      await client.send(
        new PutItemCommand({
          TableName: TABLE_NAME, // Breaks across stages, no type safety
          Item: { userId: { S: "user-123" }, noteId: { S: "note-456" } },
        }),
      );
      ```
      
      **Why bad:** Hardcoded name breaks in dev/staging stages, no type safety, requires manual IAM policy management, no connection to SST infrastructure
      
      ---
      
      ## Custom Linkables
      
      Link arbitrary values (API keys, config) or wrap raw Pulumi resources.
      
      ```typescript
      // Link arbitrary values
      const stripe = new sst.Linkable("Stripe", {
        properties: { publishableKey: "pk_live_xxx" },
      });
      
      new sst.aws.Function("Billing", {
        handler: "src/billing.handler",
        link: [stripe],
      });
      ```
      
      ```typescript
      // Runtime access
      import { Resource } from "sst";
      const key = Resource.Stripe.publishableKey;
      ```
      
      ```typescript
      // Wrap a raw Pulumi resource to make it linkable
      sst.Linkable.wrap(aws.dynamodb.Table, (table) => ({
        properties: { tableName: table.name },
        include: [
          sst.aws.permission({
            actions: ["dynamodb:*"],
            resources: [table.arn],
          }),
        ],
      }));
      
      // Now raw Pulumi tables can be linked like SST components
      const rawTable = new aws.dynamodb.Table("Legacy", {
        /* ... */
      });
      new sst.aws.Function("Handler", {
        handler: "src/handler.handler",
        link: [rawTable],
      });
      ```
      
      **Why good:** `sst.Linkable` links arbitrary config, `Linkable.wrap` makes any Pulumi resource linkable with permissions — useful for AWS services without an SST component
      
      ---
      
      ## Secrets
      
      ```bash
      # Set secrets (per-stage, encrypted in S3)
      sst secret set DATABASE_URL "postgres://..."
      sst secret set STRIPE_SECRET_KEY "sk_live_xxx"
      
      # Set fallback value for all stages
      sst secret set API_KEY "key-xxx" --fallback
      ```
      
      ```typescript
      // sst.config.ts — link secrets like any resource
      const dbUrl = new sst.Secret("DatabaseUrl");
      const stripeKey = new sst.Secret("StripeSecretKey");
      
      new sst.aws.Function("Api", {
        handler: "src/api.handler",
        link: [dbUrl, stripeKey],
      });
      ```
      
      ```typescript
      // Runtime — access via Resource
      import { Resource } from "sst";
      const connectionString = Resource.DatabaseUrl.value;
      const stripe = new Stripe(Resource.StripeSecretKey.value);
      ```
      
      **Why good:** Secrets are encrypted at rest in S3, per-stage isolation, accessed through the same `Resource.*` pattern as other links, no `.env` files to manage
      
      ---
      
      ## Global Helpers
      
      ```typescript
      // $app — app context
      const isProd = $app.stage === "production";
      const appName = $app.name;
      
      // $dev — boolean, true during sst dev
      if ($dev) {
        // Skip expensive resources in dev mode
      }
      
      // $concat — join Output values (can't use template literals with Outputs)
      const bucketArn = $concat("arn:aws:s3:::", bucket.name);
      
      // $interpolate — template literal syntax for Outputs
      const policy = $interpolate`arn:aws:s3:::${bucket.name}/*`;
      
      // $resolve — await multiple Outputs
      $resolve([bucket.name, table.name]).apply(([b, t]) => {
        console.log(`Bucket: ${b}, Table: ${t}`);
      });
      
      // $transform — set global defaults for a component type
      $transform(sst.aws.Function, (args) => {
        args.runtime ??= "nodejs22.x";
        args.memory ??= "512 MB";
        args.architecture ??= "arm64";
      });
      ```
      
      **Why good:** Global helpers handle Pulumi Output resolution — you cannot use standard template literals or string concatenation with Output values. `$transform` sets defaults across all instances without repeating configuration.
      
      ```typescript
      // BAD: Using template literals with Outputs
      const arn = `arn:aws:s3:::${bucket.name}`; // ERROR: bucket.name is an Output, not a string
      ```
      
      **Why bad:** Pulumi Outputs are not strings — template literals produce `[object Object]`. Must use `$concat()` or `$interpolate` instead.
      
      ---
      
      ## Dev Workflow
      
      ```bash
      # 1. Start live development (personal stage)
      sst dev
      
      # 2. The multiplexer:
      #    - Deploys infrastructure to your personal stage
      #    - Proxies Lambda invocations to your local machine
      #    - Starts frontend dev servers (Next.js, Vite, etc.)
      #    - Creates VPC tunnel if needed
      
      # 3. Make code changes — Lambda reloads in <10ms
      
      # 4. When done, either:
      #    a) Run sst dev again next time (stubs still deployed)
      #    b) Deploy real code: sst deploy
      ```
      
      ### VS Code Debugging
      
      Enable "Debug: Toggle Auto Attach" -> "Always" in VS Code, then start `sst dev` in a new terminal. Set breakpoints in your Lambda handlers — they'll hit when invoked.
      
      ### Detect Dev Mode in Handlers
      
      ```typescript
      export async function handler(event: unknown) {
        if (process.env.SST_DEV) {
          // Local dev behavior (e.g., skip rate limiting, use local DB)
        }
      }
      ```
      
    • deployment.md 9.5 KB
      # SST (Ion) — Deployment & DevOps Patterns
      
      > Deployment, CI/CD, transforms, removal policies, VPC, containers, and frontend framework patterns. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Related examples:**
      
      - [Core Patterns](core.md) — sst.config.ts, resource linking, live dev, secrets
      - [API & Data](api-data.md) — ApiGatewayV2, Dynamo, Bucket, Queue, Topic, Cron
      
      ---
      
      ## Multi-Stage Configuration
      
      ```typescript
      /// <reference path="./.sst/platform/config.d.ts" />
      
      export default $config({
        app(input) {
          return {
            name: "my-app",
            home: "aws",
            removal: input.stage === "production" ? "retain" : "remove",
            protect: input.stage === "production",
            providers: {
              aws: {
                region: "us-east-1",
                // Different AWS profile per stage (optional)
                ...(input.stage === "production" && { profile: "prod" }),
              },
            },
          };
        },
        async run() {
          const isProd = $app.stage === "production";
          const isStaging = $app.stage === "staging";
      
          const table = new sst.aws.Dynamo("Data", {
            fields: { pk: "string", sk: "string" },
            primaryIndex: { hashKey: "pk", rangeKey: "sk" },
            deletionProtection: isProd,
          });
      
          const api = new sst.aws.ApiGatewayV2("Api", {
            domain: isProd
              ? "api.example.com"
              : isStaging
                ? "api-staging.example.com"
                : undefined,
          });
      
          api.route("GET /", {
            handler: "src/index.handler",
            link: [table],
          });
      
          return { url: api.url };
        },
      });
      ```
      
      **Why good:** Stage-aware removal (retain prod data), stage-aware domain names, deletion protection only in production, TypeScript conditionals for stage logic
      
      ---
      
      ## Removal Policies
      
      ```typescript
      app(input) {
        return {
          name: "my-app",
          home: "aws",
          // What happens when you run `sst remove`
          removal: input.stage === "production"
            ? "retain"      // Keep all resources (safe for production)
            : "remove",     // Delete everything (clean dev stages)
          // "retain-all" keeps everything including CloudWatch logs
        };
      }
      ```
      
      | Policy         | Behavior                                 | Use For                            |
      | -------------- | ---------------------------------------- | ---------------------------------- |
      | `"remove"`     | Deletes all resources                    | Dev stages, PR previews            |
      | `"retain"`     | Keeps data resources (S3, DynamoDB, RDS) | Production                         |
      | `"retain-all"` | Keeps everything including logs          | Production with audit requirements |
      
      ---
      
      ## Transforms — Per-Component
      
      ```typescript
      // Customize underlying Lambda resource
      new sst.aws.Function("Api", {
        handler: "src/api.handler",
        transform: {
          function: (args) => {
            // Enable X-Ray tracing (not exposed by SST)
            args.tracingConfig = { mode: "Active" };
          },
          role: (args) => {
            // Customize IAM role
            args.maxSessionDuration = 7200;
          },
        },
      });
      
      // Customize underlying API Gateway resource
      new sst.aws.ApiGatewayV2("Api", {
        transform: {
          route: {
            handler: (args) => {
              // Set default memory for all route handlers
              args.memory ??= "2048 MB";
            },
          },
        },
      });
      ```
      
      **Why good:** `transform` modifies the underlying Pulumi resource properties without abandoning SST's abstractions — you get both SST's linking/permissions AND low-level customization
      
      ---
      
      ## Transforms — Global Defaults
      
      ```typescript
      async run() {
        // Apply to ALL Functions in the app
        $transform(sst.aws.Function, (args) => {
          args.runtime ??= "nodejs22.x";
          args.architecture ??= "arm64";
          args.memory ??= "512 MB";
          args.timeout ??= "30 seconds";
        });
      
        // Apply to ALL Dynamo tables
        $transform(sst.aws.Dynamo, (args) => {
          args.deletionProtection ??= $app.stage === "production";
        });
      
        // Now every Function and Dynamo table inherits these defaults
        // unless explicitly overridden
        const api = new sst.aws.Function("Api", {
          handler: "src/api.handler",
          // memory: "512 MB" and architecture: "arm64" from $transform
        });
      }
      ```
      
      **Why good:** `$transform` sets defaults once instead of repeating on every component, `??=` only applies if not already set (overridable)
      
      ---
      
      ## VPC and Containers
      
      ```typescript
      async run() {
        const vpc = new sst.aws.Vpc("AppVpc", {
          bastion: true, // EC2 bastion for SSH tunneling to private subnets
          nat: "managed", // NAT Gateway for private subnet internet access
        });
      
        const db = new sst.aws.Postgres("Database", {
          vpc,
          scaling: {
            min: "0.5 ACU",
            max: "4 ACU",
          },
        });
      
        const cluster = new sst.aws.Cluster("AppCluster", { vpc });
      
        cluster.addService("Api", {
          link: [db],
          scaling: { min: 1, max: 10 },
          image: { context: "./api" }, // Dockerfile in ./api
          dev: {
            command: "node --watch src/index.ts",
          },
        });
      
        // Lambda functions can also use VPC
        new sst.aws.Function("Migration", {
          handler: "src/migrate.handler",
          vpc,
          link: [db],
          timeout: "5 minutes",
        });
      }
      ```
      
      **Why good:** VPC with bastion for database access, Postgres with autoscaling, ECS service with auto-scaling, Lambda in VPC for migrations, all connected via resource linking
      
      ---
      
      ## Frontend Framework Deployments
      
      ### Next.js
      
      ```typescript
      const web = new sst.aws.Nextjs("Web", {
        link: [api, table, bucket],
        domain: $app.stage === "production" ? "example.com" : undefined,
        environment: {
          // Public env vars use the framework's prefix (e.g., NEXT_PUBLIC_ for Next.js)
          PUBLIC_API_URL: api.url,
        },
      });
      ```
      
      ### Remix
      
      ```typescript
      const web = new sst.aws.Remix("Web", {
        link: [table],
        domain: "app.example.com",
      });
      ```
      
      ### Astro
      
      ```typescript
      const web = new sst.aws.Astro("Web", {
        link: [bucket],
        domain: "docs.example.com",
      });
      ```
      
      ### Static Site
      
      ```typescript
      const site = new sst.aws.StaticSite("Docs", {
        path: "./docs",
        domain: "docs.example.com",
        environment: {
          API_URL: api.url,
        },
      });
      ```
      
      **Key point:** Frontend `link` values are only accessible in server-side code (SSR, API routes, server loaders). Client-side code must use `environment` for public values.
      
      ---
      
      ## CI/CD with GitHub Actions
      
      ```yaml
      # .github/workflows/deploy.yml
      name: Deploy
      
      on:
        push:
          branches: [main]
      
      # Prevent concurrent deployments to the same stage
      concurrency:
        group: deploy-${{ github.ref }}
        cancel-in-progress: false
      
      jobs:
        deploy:
          runs-on: ubuntu-latest
          permissions:
            id-token: write
            contents: read
          steps:
            - uses: actions/checkout@v4
      
            - uses: actions/setup-node@v4
              with:
                node-version: "22"
                cache: npm
      
            - run: npm ci
      
            # OIDC auth (recommended over long-lived keys)
            - uses: aws-actions/configure-aws-credentials@v4
              with:
                role-to-assume: arn:aws:iam::123456789012:role/github-deploy
                aws-region: us-east-1
      
            - name: Deploy to production
              run: npx sst deploy --stage production
      ```
      
      ### PR Preview Environments
      
      ```yaml
      # .github/workflows/preview.yml
      name: Preview
      
      on:
        pull_request:
          types: [opened, synchronize]
      
      jobs:
        preview:
          runs-on: ubuntu-latest
          permissions:
            id-token: write
            contents: read
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                node-version: "22"
                cache: npm
            - run: npm ci
      
            - uses: aws-actions/configure-aws-credentials@v4
              with:
                role-to-assume: arn:aws:iam::123456789012:role/github-deploy
                aws-region: us-east-1
      
            - name: Deploy preview
              run: npx sst deploy --stage pr-${{ github.event.pull_request.number }}
      
        cleanup:
          runs-on: ubuntu-latest
          if: github.event.action == 'closed'
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                node-version: "22"
                cache: npm
            - run: npm ci
      
            - uses: aws-actions/configure-aws-credentials@v4
              with:
                role-to-assume: arn:aws:iam::123456789012:role/github-deploy
                aws-region: us-east-1
      
            - name: Remove preview
              run: npx sst remove --stage pr-${{ github.event.pull_request.number }}
      ```
      
      **Why good:** OIDC authentication (no long-lived AWS keys), PR-based preview environments with automatic cleanup, concurrency group prevents race conditions
      
      ---
      
      ## Monorepo Structure
      
      For larger projects, split infrastructure into an `infra/` directory:
      
      ```typescript
      // sst.config.ts
      /// <reference path="./.sst/platform/config.d.ts" />
      
      export default $config({
        app(input) {
          return {
            name: "my-app",
            home: "aws",
            removal: input.stage === "production" ? "retain" : "remove",
          };
        },
        async run() {
          // Split infra into focused modules
          const { table, bucket } = await import("./infra/storage");
          const { api } = await import("./infra/api");
          const { web } = await import("./infra/web");
      
          return {
            apiUrl: api.url,
            webUrl: web.url,
          };
        },
      });
      ```
      
      ```typescript
      // infra/storage.ts
      export const table = new sst.aws.Dynamo("Data", {
        fields: { pk: "string", sk: "string" },
        primaryIndex: { hashKey: "pk", rangeKey: "sk" },
      });
      
      export const bucket = new sst.aws.Bucket("Uploads");
      ```
      
      ```typescript
      // infra/api.ts
      import { table, bucket } from "./storage";
      
      export const api = new sst.aws.ApiGatewayV2("Api");
      api.route("GET /items", {
        handler: "src/items/list.handler",
        link: [table],
      });
      api.route("POST /upload", {
        handler: "src/upload.handler",
        link: [bucket],
      });
      ```
      
      **Why good:** `sst.config.ts` stays clean, infrastructure modules can import from each other, resources are co-located with their related config
      
  • reference.md 7.7 KB
    # SST (Ion) Quick Reference
    
    ## CLI Commands
    
    ### Development
    
    ```bash
    # Start live dev (deploys infra, proxies Lambda locally, starts frontends)
    sst dev
    
    # Dev in basic mode (no multiplexer, just links resources)
    sst dev --mode basic
    
    # Run a command with linked resources
    sst shell
    
    # Run a specific command with linked resources
    sst shell -- node scripts/seed.ts
    ```
    
    ### Deployment
    
    ```bash
    # Deploy to personal stage
    sst deploy
    
    # Deploy to a named stage
    sst deploy --stage production
    
    # Deploy a single component
    sst deploy --target MyFunction
    
    # Exclude a component from deploy
    sst deploy --exclude MyFrontend
    
    # Continue deploying despite errors
    sst deploy --continue
    ```
    
    ### Removal
    
    ```bash
    # Remove personal stage
    sst remove
    
    # Remove a named stage
    sst remove --stage staging
    
    # Remove a specific component
    sst remove --target MyFunction
    ```
    
    ### Secrets
    
    ```bash
    # Set a secret (prompts for value)
    sst secret set DATABASE_URL
    
    # Set with inline value
    sst secret set STRIPE_KEY sk_live_xxx
    
    # Set a fallback for all stages
    sst secret set API_KEY xxx --fallback
    
    # Load secrets from file (.env or bash format)
    sst secret load .env.production
    
    # List all secrets for current stage
    sst secret list
    
    # Remove a secret
    sst secret remove OLD_KEY
    ```
    
    ### Maintenance
    
    ```bash
    # Unlock stuck deployment state
    sst unlock
    
    # Sync local state with cloud
    sst refresh
    
    # Upgrade SST CLI
    sst upgrade
    
    # Upgrade to specific version
    sst upgrade 3.5
    
    # Show installed version
    sst version
    ```
    
    ---
    
    ## Component Cheat Sheet
    
    ### Compute
    
    | Component          | Service     | Key Props                                                                    |
    | ------------------ | ----------- | ---------------------------------------------------------------------------- |
    | `sst.aws.Function` | Lambda      | `handler`, `runtime`, `memory`, `timeout`, `link`, `url`, `streaming`, `vpc` |
    | `sst.aws.Cluster`  | ECS         | `vpc`                                                                        |
    | `sst.aws.Service`  | ECS Service | `cluster`, `image`, `link`, `scaling`                                        |
    | `sst.aws.Task`     | ECS Task    | `cluster`, `image`, `link`                                                   |
    
    ### API & Routing
    
    | Component              | Service        | Key Props                                                     |
    | ---------------------- | -------------- | ------------------------------------------------------------- |
    | `sst.aws.ApiGatewayV2` | API Gateway v2 | `domain`, `cors`, `accessLog`, `.route()`, `.addAuthorizer()` |
    | `sst.aws.Router`       | CloudFront     | `domain`, `.route()`                                          |
    
    ### Data & Storage
    
    | Component          | Service      | Key Props                                                  |
    | ------------------ | ------------ | ---------------------------------------------------------- |
    | `sst.aws.Dynamo`   | DynamoDB     | `fields`, `primaryIndex`, `globalIndexes`, `stream`, `ttl` |
    | `sst.aws.Bucket`   | S3           | `access`, `cors`, `versioning`                             |
    | `sst.aws.Postgres` | RDS Postgres | `vpc`, `scaling`                                           |
    
    ### Messaging & Scheduling
    
    | Component       | Service     | Key Props                                          |
    | --------------- | ----------- | -------------------------------------------------- |
    | `sst.aws.Queue` | SQS         | `fifo`, `visibilityTimeout`, `dlq`, `.subscribe()` |
    | `sst.aws.Topic` | SNS         | `.subscribe()`                                     |
    | `sst.aws.Cron`  | EventBridge | `schedule`, `function` / `task`                    |
    
    ### Frontend Frameworks
    
    | Component            | Framework      | Key Props                       |
    | -------------------- | -------------- | ------------------------------- |
    | `sst.aws.Nextjs`     | Next.js        | `link`, `domain`, `environment` |
    | `sst.aws.Remix`      | Remix          | `link`, `domain`                |
    | `sst.aws.Astro`      | Astro          | `link`, `domain`                |
    | `sst.aws.SvelteKit`  | SvelteKit      | `link`, `domain`                |
    | `sst.aws.SolidStart` | SolidStart     | `link`, `domain`                |
    | `sst.aws.StaticSite` | Static HTML/JS | `path`, `domain`, `environment` |
    
    ### Infrastructure
    
    | Component      | Service | Key Props               |
    | -------------- | ------- | ----------------------- |
    | `sst.aws.Vpc`  | VPC     | `bastion`, `nat`        |
    | `sst.Linkable` | Custom  | `properties`, `include` |
    
    ---
    
    ## Global Helpers (Available in `sst.config.ts` `run()`)
    
    | Helper                  | Purpose                   | Example                                           |
    | ----------------------- | ------------------------- | ------------------------------------------------- |
    | `$app.name`             | App name                  | `$app.name`                                       |
    | `$app.stage`            | Current stage             | `$app.stage === "production"`                     |
    | `$app.protect`          | Protect flag              | `$app.protect`                                    |
    | `$app.removal`          | Removal policy            | `$app.removal`                                    |
    | `$dev`                  | Is `sst dev` mode         | `if ($dev) { ... }`                               |
    | `$concat(...vals)`      | Join Output strings       | `$concat("prefix-", bucket.name)`                 |
    | `` $interpolate`...` `` | Template literal Outputs  | `` $interpolate`arn:aws:s3:::${bucket.name}` ``   |
    | `$resolve(vals)`        | Await multiple Outputs    | `$resolve([a, b]).apply(([a, b]) => ...)`         |
    | `$transform(Type, fn)`  | Global component defaults | `$transform(sst.aws.Function, (args) => { ... })` |
    | `$asset(path)`          | File/dir as Pulumi asset  | `$asset("./files/config.json")`                   |
    | `$jsonParse(str)`       | Parse JSON Output         | `$jsonParse(secret.value)`                        |
    | `$jsonStringify(obj)`   | Stringify Output          | `$jsonStringify({ key: output })`                 |
    
    ---
    
    ## Function Configuration Defaults
    
    ```typescript
    // sst.aws.Function defaults
    {
      runtime: "nodejs22.x",    // Also supports go, python, rust
      memory: "1024 MB",        // Range: 128 MB - 10240 MB
      timeout: "20 seconds",    // Range: 1 second - 900 seconds
      storage: "512 MB",        // Ephemeral storage: 512 MB - 10240 MB
      architecture: "x86_64",   // Also supports "arm64"
    }
    ```
    
    ---
    
    ## Named Constants
    
    ```typescript
    // SST / Lambda limits
    const LAMBDA_MAX_MEMORY_MB = 10_240;
    const LAMBDA_MAX_TIMEOUT_SECONDS = 900;
    const LAMBDA_MAX_STORAGE_MB = 10_240;
    const LAMBDA_MAX_ENV_SIZE_BYTES = 4_096;
    const DYNAMO_MAX_ITEM_SIZE_BYTES = 400 * 1024; // 400 KB
    const DYNAMO_MAX_BATCH_WRITE = 25;
    const DYNAMO_MAX_BATCH_GET = 100;
    const SQS_MAX_MESSAGE_SIZE_BYTES = 256 * 1024; // 256 KB
    const SQS_MAX_VISIBILITY_TIMEOUT_HOURS = 12;
    const SQS_DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 30;
    const S3_MAX_SINGLE_PUT_SIZE_BYTES = 5 * 1024 * 1024 * 1024; // 5 GB
    ```
    
    ---
    
    ## Environment Files
    
    SST automatically loads environment files in this order:
    
    1. `.env` — Always loaded (highest priority)
    2. `.env.<stage>` — Stage-specific overrides
    
    Both are available as `process.env` in `sst.config.ts` and in Lambda functions.
    
    **Note:** `.env` takes precedence over `.env.<stage>`. This is the opposite of some frameworks.
    
    ---
    
    ## Concurrency Environment Variables
    
    Control build parallelism during `sst deploy`:
    
    | Variable                          | Default | Purpose                      |
    | --------------------------------- | ------- | ---------------------------- |
    | `SST_BUILD_CONCURRENCY_SITE`      | 1       | Frontend builds in parallel  |
    | `SST_BUILD_CONCURRENCY_FUNCTION`  | 4       | Function bundles in parallel |
    | `SST_BUILD_CONCURRENCY_CONTAINER` | 1       | Container builds in parallel |
    
  • SKILL.md 17.5 KB
    ---
    name: infra-iac-sst
    description: SST (Ion) infrastructure-as-code — TypeScript-first serverless on AWS with Pulumi, resource linking, and live Lambda dev
    ---
    
    # SST (Ion) Patterns
    
    > **Quick Guide:** SST v3 (Ion) is TypeScript-first infrastructure-as-code for AWS, powered by Pulumi/Terraform (not CDK/CloudFormation). Define your entire app in `sst.config.ts` using high-level components (`sst.aws.Function`, `sst.aws.ApiGatewayV2`, `sst.aws.Bucket`, `sst.aws.Dynamo`, etc.). Use **resource linking** (`link: [bucket]` + `Resource.MyBucket.name`) for type-safe, permission-aware access between components. Use `sst dev` for live Lambda development with sub-10ms reloads. Use `$app.stage` for multi-environment isolation. Use `transform` to customize underlying Pulumi resources.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use resource linking (`link` + `Resource.*`) to connect components — NEVER hardcode ARNs, table names, or bucket names)**
    
    **(You MUST use `$app.stage` for environment isolation — NEVER share resources across stages without explicit intent)**
    
    **(You MUST use `sst dev` for local development — it provides live Lambda proxying with sub-10ms reloads against real AWS resources)**
    
    **(You MUST use `sst secret set` for secrets — NEVER put secrets in `sst.config.ts`, `.env` files committed to git, or environment variables)**
    
    **(You MUST use `transform` to customize underlying resources — NEVER reach for raw Pulumi resources when an SST component exists)**
    
    </critical_requirements>
    
    ---
    
    ## Examples
    
    - [Core Patterns](examples/core.md) — sst.config.ts structure, Function, resource linking, $app globals, secrets, multi-stage
    - [API & Data](examples/api-data.md) — ApiGatewayV2, Dynamo, Bucket, Queue, Topic, Cron, authorization
    - [Deployment & DevOps](examples/deployment.md) — sst deploy, CI/CD, removal policies, transforms, Vpc, Cluster, frontend frameworks
    - [Quick Reference](reference.md) — CLI commands, component cheat sheet, global helpers, named constants
    
    ---
    
    **Auto-detection:** SST, sst.config.ts, sst.aws.Function, sst.aws.ApiGatewayV2, sst.aws.Bucket, sst.aws.Dynamo, sst.aws.Queue, sst.aws.Topic, sst.aws.Cron, sst.aws.Nextjs, sst.aws.Remix, sst.aws.Astro, sst.aws.StaticSite, sst.aws.Vpc, sst.aws.Cluster, sst.aws.Postgres, sst.aws.Router, sst.Linkable, Resource from sst, sst dev, sst deploy, sst remove, sst secret, $app.stage, $transform, $concat, $interpolate, resource linking, live Lambda, Ion
    
    **When to use:**
    
    - Defining AWS infrastructure in TypeScript with high-level components
    - Deploying serverless applications (Lambda, API Gateway, DynamoDB, S3, SQS, SNS)
    - Deploying full-stack apps (Next.js, Remix, Astro, SvelteKit, SolidStart on AWS)
    - Setting up live Lambda development with real AWS resources
    - Managing multi-stage environments (dev, staging, production)
    - Connecting infrastructure components with type-safe resource linking
    
    **When NOT to use:**
    
    - Multi-cloud infrastructure spanning many providers (SST is AWS-focused with limited Cloudflare support)
    - Existing Terraform/Pulumi codebases where SST abstraction adds no value
    - Projects that need container-only deployments without serverless components
    
    **Key patterns covered:**
    
    - `sst.config.ts` structure (`app()` + `run()` functions)
    - Resource linking: `link` property + `Resource.*` SDK
    - Live Lambda development with `sst dev`
    - AWS components: Function, ApiGatewayV2, Dynamo, Bucket, Queue, Topic, Cron
    - Frontend deployments: Nextjs, Remix, Astro, StaticSite
    - Multi-stage isolation with `$app.stage`
    - Transforms for customizing underlying Pulumi resources
    - Secrets management with `sst secret`
    - Custom linkables with `sst.Linkable` and `Linkable.wrap`
    - Global helpers: `$app`, `$dev`, `$concat`, `$interpolate`, `$resolve`, `$transform`
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    SST v3 (Ion) replaces CDK/CloudFormation with Pulumi/Terraform for dramatically faster deployments and a simpler programming model. The core ideas:
    
    1. **One config file** — Your entire app is defined in `sst.config.ts`. Infrastructure, frontends, and functions all declared together in TypeScript with loops, conditionals, and functions.
    2. **Components over constructs** — High-level `sst.aws.*` components encapsulate best practices (IAM, logging, monitoring). Use `transform` to reach into underlying resources when defaults aren't enough.
    3. **Resource linking** — The killer feature. `link: [bucket]` automatically grants IAM permissions and injects type-safe references. Access via `Resource.MyBucket.name` at runtime. No manual ARN passing or environment variable wiring.
    4. **Stage-based isolation** — Every developer gets their own stage (`sst dev` creates a personal stack). `$app.stage` drives resource naming. Production uses `sst deploy --stage production`.
    5. **Live dev against real AWS** — `sst dev` replaces Lambda functions with stubs that proxy to your local machine. Changes reload in under 10ms. No local emulation — your code runs against real DynamoDB, S3, SQS.
    
    **When to use SST:**
    
    - Serverless-first applications on AWS (Lambda, API Gateway, DynamoDB, S3, SQS, SNS)
    - Full-stack apps deploying frontend frameworks (Next.js, Remix, Astro) to AWS
    - Teams that want TypeScript infrastructure with minimal AWS boilerplate
    - Projects needing fast local development loops against real cloud resources
    
    **When NOT to use SST:**
    
    - Multi-cloud infrastructure beyond AWS + Cloudflare (SST's multi-cloud support is limited)
    - Container-only workloads with no serverless components (use raw Pulumi or Terraform)
    - Existing large Terraform/Pulumi codebases where SST abstraction adds migration cost
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: sst.config.ts Structure
    
    Every SST app has a single `sst.config.ts` with two functions: `app()` for metadata and `run()` for resources.
    
    ```typescript
    /// <reference path="./.sst/platform/config.d.ts" />
    
    export default $config({
      app(input) {
        return {
          name: "my-app",
          home: "aws",
          removal: input.stage === "production" ? "retain" : "remove",
          protect: input.stage === "production",
          providers: {
            aws: { region: "us-east-1" },
          },
        };
      },
      async run() {
        const bucket = new sst.aws.Bucket("Uploads");
        const api = new sst.aws.Function("Api", {
          handler: "src/api.handler",
          link: [bucket],
        });
        return { apiUrl: api.url };
      },
    });
    ```
    
    **Why good:** `app()` handles metadata and stage-specific policies, `run()` defines all resources, `removal: "retain"` protects production data, returned values become outputs in `.sst/outputs.json`
    
    See [examples/core.md](examples/core.md) for full config with multi-stage, providers, and protect patterns.
    
    ---
    
    ### Pattern 2: Resource Linking
    
    The defining SST feature. Link resources to grant permissions and type-safe access automatically.
    
    ```typescript
    // In sst.config.ts
    const table = new sst.aws.Dynamo("Notes", {
      fields: { userId: "string", noteId: "string" },
      primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
    });
    
    new sst.aws.Function("Api", {
      handler: "src/api.handler",
      link: [table],
    });
    ```
    
    ```typescript
    // In src/api.ts — runtime code
    import { Resource } from "sst";
    import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
    
    const client = new DynamoDBClient({});
    // Resource.Notes.name is the table name — type-safe, auto-generated
    console.log(Resource.Notes.name);
    ```
    
    **Why good:** No manual ARN passing, IAM permissions granted automatically, type-safe access via generated `sst-env.d.ts`, works across Functions, frontends, and containers
    
    See [examples/core.md](examples/core.md) for linking patterns including custom linkables.
    
    ---
    
    ### Pattern 3: Live Lambda Development
    
    `sst dev` proxies Lambda invocations to your local machine for sub-10ms reload cycles.
    
    ```bash
    # Start live dev — deploys infra, proxies Lambda to local machine
    sst dev
    
    # Opens multiplexer: deploys resources, starts frontends, tunnels VPC
    ```
    
    ```typescript
    // Detect dev mode in handler code
    const isLocal = process.env.SST_DEV === "true";
    ```
    
    **Key behavior:** Functions are replaced with stubs that forward events via AppSync to your local machine. Your local code runs as Node.js Workers. Changes reload instantly — no redeploy needed.
    
    **Gotcha:** Killing `sst dev` leaves stubs deployed. Subsequent Lambda invocations will timeout until you run `sst dev` again or `sst deploy` to replace stubs with real code.
    
    See [examples/core.md](examples/core.md) for dev workflow and debugging setup.
    
    ---
    
    ### Pattern 4: AWS Components
    
    SST provides high-level components for common AWS services. Each handles IAM, logging, and configuration automatically.
    
    | Component              | AWS Service         | Use Case              |
    | ---------------------- | ------------------- | --------------------- |
    | `sst.aws.Function`     | Lambda              | Serverless functions  |
    | `sst.aws.ApiGatewayV2` | API Gateway v2      | HTTP APIs with routes |
    | `sst.aws.Dynamo`       | DynamoDB            | NoSQL database        |
    | `sst.aws.Bucket`       | S3                  | Object storage        |
    | `sst.aws.Queue`        | SQS                 | Message queues        |
    | `sst.aws.Topic`        | SNS                 | Pub/sub messaging     |
    | `sst.aws.Cron`         | EventBridge         | Scheduled tasks       |
    | `sst.aws.Vpc`          | VPC                 | Network isolation     |
    | `sst.aws.Cluster`      | ECS                 | Container workloads   |
    | `sst.aws.Postgres`     | RDS Postgres        | Relational database   |
    | `sst.aws.Nextjs`       | Lambda + CloudFront | Next.js deployment    |
    | `sst.aws.Remix`        | Lambda + CloudFront | Remix deployment      |
    | `sst.aws.Astro`        | Lambda + CloudFront | Astro deployment      |
    | `sst.aws.StaticSite`   | S3 + CloudFront     | Static site hosting   |
    | `sst.aws.Router`       | CloudFront          | URL routing           |
    
    See [examples/api-data.md](examples/api-data.md) for API, database, storage, queue, and cron patterns.
    
    ---
    
    ### Pattern 5: Secrets Management
    
    Secrets are encrypted and stored in S3, injected into function bundles at deploy time.
    
    ```bash
    # Set a secret (prompts for value)
    sst secret set DATABASE_URL
    
    # Set a secret with value inline
    sst secret set STRIPE_KEY sk_live_xxx
    
    # Load secrets from a file
    sst secret load .env.production
    
    # List all secrets
    sst secret list
    ```
    
    ```typescript
    // Access secrets via resource linking
    import { Resource } from "sst";
    const stripeKey = Resource.StripeKey.value;
    ```
    
    **Gotcha:** Secrets are per-stage. Set them for each stage separately. Use `--fallback` to set a default across all stages.
    
    See [examples/core.md](examples/core.md) for secrets with Linkable pattern.
    
    ---
    
    ### Pattern 6: Transforms
    
    Customize underlying Pulumi resources when SST defaults aren't enough.
    
    ```typescript
    // Per-component transform
    new sst.aws.Function("Api", {
      handler: "src/api.handler",
      transform: {
        function: (args) => {
          args.tracingConfig = { mode: "Active" };
        },
      },
    });
    
    // Global transform — applies to ALL components of a type
    $transform(sst.aws.Function, (args) => {
      args.environment ??= {};
      args.environment.variables ??= {};
      args.environment.variables.STAGE = $app.stage;
    });
    ```
    
    **Why good:** Transforms let you customize any underlying resource property without abandoning SST's abstractions. Global transforms set defaults across all components.
    
    See [examples/deployment.md](examples/deployment.md) for transform patterns.
    
    ---
    
    ### Pattern 7: Multi-Stage Environments
    
    Every stage is a fully isolated deployment. Use `$app.stage` for conditional configuration.
    
    ```typescript
    async run() {
      const isProd = $app.stage === "production";
    
      const table = new sst.aws.Dynamo("Notes", {
        fields: { userId: "string", noteId: "string" },
        primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
        deletionProtection: isProd,
      });
    }
    ```
    
    ```bash
    # Personal dev stage (default)
    sst dev
    
    # Deploy to staging
    sst deploy --stage staging
    
    # Deploy to production
    sst deploy --stage production
    ```
    
    See [examples/deployment.md](examples/deployment.md) for multi-stage patterns with removal policies.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Choosing an SST Component
    
    ```
    What are you building?
      |
      +-- HTTP API
      |     +-- Simple routes with Lambda handlers --> sst.aws.ApiGatewayV2
      |     +-- Need WebSocket support --> sst.aws.ApiGatewayWebSocket
      |     +-- URL routing / CDN --> sst.aws.Router
      |
      +-- Data storage
      |     +-- Key-value / document data --> sst.aws.Dynamo
      |     +-- Relational data with SQL --> sst.aws.Postgres
      |     +-- File/blob storage --> sst.aws.Bucket
      |
      +-- Async processing
      |     +-- Point-to-point messaging --> sst.aws.Queue (SQS)
      |     +-- Fan-out to multiple subscribers --> sst.aws.Topic (SNS)
      |     +-- Scheduled tasks --> sst.aws.Cron
      |
      +-- Compute
      |     +-- Serverless function --> sst.aws.Function
      |     +-- Container workload --> sst.aws.Cluster + sst.aws.Service
      |     +-- Long-running background job --> sst.aws.Function (up to 15min)
      |
      +-- Full-stack frontend
            +-- Next.js --> sst.aws.Nextjs
            +-- Remix --> sst.aws.Remix
            +-- Astro --> sst.aws.Astro
            +-- SvelteKit --> sst.aws.SvelteKit
            +-- SolidStart --> sst.aws.SolidStart
            +-- Static HTML/JS --> sst.aws.StaticSite
    ```
    
    ### When to Use Transforms vs Raw Pulumi
    
    ```
    Need to set a property on an SST component?
      |
      +-- Property exists on the SST component args --> Use the SST property directly
      |
      +-- Property exists only on the underlying AWS resource --> Use transform
      |
      +-- Need to set a default across ALL instances of a component --> Use $transform()
      |
      +-- No SST component exists for this AWS service --> Use raw Pulumi resource
            +-- Need to link it? --> Use sst.Linkable.wrap() or new sst.Linkable()
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Hardcoding ARNs, table names, or bucket names instead of using resource linking (`link` + `Resource.*`) — defeats SST's type-safe wiring and breaks across stages
    - Sharing resource names across stages without `$app.stage` prefix — causes resource conflicts and accidental cross-stage access
    - Putting secrets in `sst.config.ts` or committed `.env` files instead of using `sst secret set` — secrets leak to version control
    - Using `sst dev` for shared environments (staging, production) — stubs proxy to a single developer's machine, breaking for everyone else
    - Creating raw Pulumi resources when an equivalent `sst.aws.*` component exists — loses SST's linking, permissions, and defaults
    
    **Medium Priority Issues:**
    
    - Not setting `removal: "retain"` and `protect: true` for production stages — accidental `sst remove` deletes all data
    - Missing `/// <reference path="./.sst/platform/config.d.ts" />` at top of `sst.config.ts` — loses type checking for `$app`, `$transform`, etc.
    - Using `.env` files for secrets instead of `sst secret` — `.env` files aren't encrypted and must be managed manually per stage
    - Not running `sst deploy` after finishing `sst dev` session — stubs remain deployed and Lambda invocations timeout
    
    **Common Mistakes:**
    
    - Forgetting that `sst dev` stubs persist after you stop the process — always redeploy or re-run `sst dev`
    - Using `$app.stage` in runtime code — it's only available in `sst.config.ts`, use `Resource.*` or env vars for runtime stage awareness
    - Trying to use SST resource linking in client-side frontend code — links are server-side only (SSR functions, API routes)
    - Expecting `sst dev` to emulate AWS locally — it doesn't; it proxies to real AWS resources in your account
    
    **Gotchas & Edge Cases:**
    
    - Pulumi Outputs cannot be used directly in string templates — use `$concat()` or `$interpolate` instead of template literals
    - `sst dev` multiplexer starts frontends automatically — you don't need to run `next dev` or `vite dev` separately
    - FIFO queues require `.fifo` suffix in names — SST handles this automatically but be aware when referencing externally
    - `.env` and `.env.<stage>` files are loaded automatically — `.env` takes precedence over stage-specific files
    - Frontend framework links (Next.js, Remix) only work server-side — client components cannot access `Resource.*`
    - Layers specified in Function config are not applied during `sst dev` — local execution skips layers
    - The `removal` setting in `app()` controls what happens when you run `sst remove` — `"remove"` deletes resources, `"retain"` keeps them, `"retain-all"` keeps everything including logs
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use resource linking (`link` + `Resource.*`) to connect components — NEVER hardcode ARNs, table names, or bucket names)**
    
    **(You MUST use `$app.stage` for environment isolation — NEVER share resources across stages without explicit intent)**
    
    **(You MUST use `sst dev` for local development — it provides live Lambda proxying with sub-10ms reloads against real AWS resources)**
    
    **(You MUST use `sst secret set` for secrets — NEVER put secrets in `sst.config.ts`, `.env` files committed to git, or environment variables)**
    
    **(You MUST use `transform` to customize underlying resources — NEVER reach for raw Pulumi resources when an SST component exists)**
    
    **Failure to follow these rules will cause cross-stage resource conflicts, leaked secrets, broken type safety, and unnecessary infrastructure complexity.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related