infra-iac-sst
SST (Ion) infrastructure-as-code — TypeScript-first serverless on AWS with Pulumi, resource linking, and live Lambda dev
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-iac-sst/skills/infra-iac-sst
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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.tsusing 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. Usesst devfor live Lambda development with sub-10ms reloads. Use$app.stagefor multi-environment isolation. Usetransformto 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.tsstructure (app()+run()functions)- Resource linking:
linkproperty +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.LinkableandLinkable.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.stageprefix — causes resource conflicts and accidental cross-stage access - Putting secrets in
sst.config.tsor committed.envfiles instead of usingsst secret set— secrets leak to version control - Using
sst devfor 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"andprotect: truefor production stages — accidentalsst removedeletes all data - Missing
/// <reference path="./.sst/platform/config.d.ts" />at top ofsst.config.ts— loses type checking for$app,$transform, etc. - Using
.envfiles for secrets instead ofsst secret—.envfiles aren't encrypted and must be managed manually per stage - Not running
sst deployafter finishingsst devsession — stubs remain deployed and Lambda invocations timeout
Common Mistakes:
- Forgetting that
sst devstubs persist after you stop the process — always redeploy or re-runsst dev - Using
$app.stagein runtime code — it's only available insst.config.ts, useResource.*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 devto 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$interpolateinstead of template literals sst devmultiplexer starts frontends automatically — you don't need to runnext devorvite devseparately- FIFO queues require
.fifosuffix in names — SST handles this automatically but be aware when referencing externally .envand.env.<stage>files are loaded automatically —.envtakes 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
removalsetting inapp()controls what happens when you runsst 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.
Reviews (0)
No reviews yet.
No comments yet.