SST Deployment Skill
This skill helps you configure and deploy infrastructure using SST v3 in infra/ .
When to Use This Skill
-
Deploying applications to AWS
-
Adding new Lambda functions or API routes
-
Configuring environment variables per stage
-
Setting up custom domains
-
Adding AWS resources (S3, DynamoDB, etc.)
-
Debugging deployment failures
-
Managing multiple environments (dev, staging, prod)
SST Architecture
infra/ ├── sst.config.ts # SST configuration ├── api.ts # API Lambda configuration ├── web.ts # Next.js web app configuration ├── dns.ts # Domain and DNS configuration └── database.ts # Database resources (optional)
SST Configuration
Main Config File
// infra/sst.config.ts import { SSTConfig } from "sst"; import { API } from "./api"; import { Web } from "./web"; import { DNS } from "./dns";
export default { config(_input) { return { name: "sgcarstrends", region: "ap-southeast-1", // Singapore profile: "default", }; }, stacks(app) { app.stack(DNS).stack(API).stack(Web); }, } satisfies SSTConfig;
Environment Management
Stage-Based Configuration
// infra/sst.config.ts export default { config(input) { return { name: "sgcarstrends", region: "ap-southeast-1", profile: input.stage === "production" ? "prod" : "default", }; }, stacks(app) { // Set default removal policy based on stage app.setDefaultRemovalPolicy( app.stage === "production" ? "retain" : "destroy" );
app.stack(DNS).stack(API).stack(Web);
}, } satisfies SSTConfig;
Environment-Specific Settings
// infra/api.ts import { StackContext, Function } from "sst/constructs";
export function API({ stack, app }: StackContext) { const stage = app.stage;
// Stage-specific configuration const config = { dev: { memory: 512, timeout: "30 seconds", runtime: "nodejs20.x", }, staging: { memory: 1024, timeout: "60 seconds", runtime: "nodejs20.x", }, production: { memory: 2048, timeout: "120 seconds", runtime: "nodejs20.x", }, }[stage] || { memory: 512, timeout: "30 seconds", runtime: "nodejs20.x", };
const api = new Function(stack, "api", { handler: "apps/api/src/index.handler", ...config, environment: { NODE_ENV: stage === "production" ? "production" : "development", DATABASE_URL: process.env.DATABASE_URL!, UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL!, UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN!, }, });
stack.addOutputs({ ApiUrl: api.url, });
return { api }; }
Lambda Configuration
API Lambda
// infra/api.ts import { StackContext, Function, use } from "sst/constructs"; import { DNS } from "./dns";
export function API({ stack, app }: StackContext) { const { hostedZone } = use(DNS);
const api = new Function(stack, "api", {
handler: "apps/api/src/index.handler",
runtime: "nodejs20.x",
architecture: "arm64", // Graviton2 - cheaper and faster
memory: 1024,
timeout: "60 seconds",
nodejs: {
esbuild: {
minify: app.stage === "production",
sourcemap: app.stage !== "production",
},
},
environment: {
DATABASE_URL: process.env.DATABASE_URL!,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL!,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN!,
GOOGLE_GEMINI_API_KEY: process.env.GOOGLE_GEMINI_API_KEY!,
QSTASH_TOKEN: process.env.QSTASH_TOKEN!,
DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL!,
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN!,
TELEGRAM_CHAT_ID: process.env.TELEGRAM_CHAT_ID!,
},
url: {
// Custom domain for API
domain: {
domainName: api.${app.stage === "production" ? "" : ${app.stage}.}sgcarstrends.com,
hostedZone: hostedZone.zoneName,
},
},
});
stack.addOutputs({ ApiUrl: api.url, ApiDomain: api.url, });
return { api }; }
Next.js Web App
// infra/web.ts import { StackContext, NextjsSite, use } from "sst/constructs"; import { DNS } from "./dns";
export function Web({ stack, app }: StackContext) { const { hostedZone } = use(DNS);
const web = new NextjsSite(stack, "web", {
path: "apps/web",
buildCommand: "pnpm build",
environment: {
NEXT_PUBLIC_API_URL: https://api.${app.stage === "production" ? "" : ${app.stage}.}sgcarstrends.com,
DATABASE_URL: process.env.DATABASE_URL!,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL!,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN!,
},
customDomain: {
domainName: app.stage === "production"
? "sgcarstrends.com"
: ${app.stage}.sgcarstrends.com,
hostedZone: hostedZone.zoneName,
},
});
stack.addOutputs({ WebUrl: web.url, WebDomain: web.customDomainUrl, });
return { web }; }
DNS and Domains
Hosted Zone Configuration
// infra/dns.ts import { StackContext } from "sst/constructs"; import * as route53 from "aws-cdk-lib/aws-route53";
export function DNS({ stack }: StackContext) { // Import existing hosted zone const hostedZone = route53.HostedZone.fromLookup(stack, "HostedZone", { domainName: "sgcarstrends.com", });
stack.addOutputs({ HostedZoneId: hostedZone.hostedZoneId, HostedZoneName: hostedZone.zoneName, });
return { hostedZone }; }
Domain Mapping
Environment-specific domains:
-
Production: sgcarstrends.com , api.sgcarstrends.com
-
Staging: staging.sgcarstrends.com , api.staging.sgcarstrends.com
-
Dev: dev.sgcarstrends.com , api.dev.sgcarstrends.com
Deployment Commands
Deploy to Environments
Development
pnpm deploy:dev
or
cd infra && npx sst deploy --stage dev
Staging
pnpm deploy:staging
or
cd infra && npx sst deploy --stage staging
Production
pnpm deploy:prod
or
cd infra && npx sst deploy --stage production
Deploy Specific Stack
Deploy only API
cd infra && npx sst deploy --stage dev API
Deploy only Web
cd infra && npx sst deploy --stage dev Web
Remove Stack
Remove dev stack
cd infra && npx sst remove --stage dev
Remove specific stack
cd infra && npx sst remove --stage dev API
Environment Variables
Local Development
.env.local (not committed)
DATABASE_URL=postgresql://... UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=... GOOGLE_GEMINI_API_KEY=... QSTASH_TOKEN=...
SST Secrets
Use SST secrets for sensitive values:
Set secret for specific stage
npx sst secrets set DATABASE_URL "postgresql://..." --stage production
List secrets
npx sst secrets list --stage production
Remove secret
npx sst secrets remove DATABASE_URL --stage production
Access secrets in code:
import { Config } from "sst/node/config";
export function handler() { const databaseUrl = Config.DATABASE_URL; // Use secret... }
Parameter Store
// infra/api.ts import { StringParameter } from "aws-cdk-lib/aws-ssm";
export function API({ stack }: StackContext) {
// Store parameter
const param = new StringParameter(stack, "DatabaseUrl", {
parameterName: /sgcarstrends/${stack.stage}/database-url,
stringValue: process.env.DATABASE_URL!,
});
const api = new Function(stack, "api", { handler: "apps/api/src/index.handler", environment: { DATABASE_URL: param.stringValue, }, }); }
Adding AWS Resources
S3 Bucket
// infra/storage.ts import { StackContext, Bucket } from "sst/constructs";
export function Storage({ stack, app }: StackContext) { const bucket = new Bucket(stack, "uploads", { cors: [ { allowedMethods: ["GET", "PUT", "POST", "DELETE", "HEAD"], allowedOrigins: [""], allowedHeaders: [""], }, ], });
stack.addOutputs({ BucketName: bucket.bucketName, });
return { bucket }; }
DynamoDB Table
// infra/database.ts import { StackContext, Table } from "sst/constructs";
export function Database({ stack }: StackContext) { const table = new Table(stack, "sessions", { fields: { userId: "string", sessionId: "string", }, primaryIndex: { partitionKey: "userId", sortKey: "sessionId" }, timeToLiveAttribute: "expiresAt", });
stack.addOutputs({ TableName: table.tableName, });
return { table }; }
EventBridge Cron
// infra/cron.ts import { StackContext, Cron, use } from "sst/constructs"; import { API } from "./api";
export function Cron({ stack }: StackContext) { const { api } = use(API);
new Cron(stack, "DataUpdateCron", { schedule: "rate(1 hour)", job: { function: { handler: "apps/api/src/cron/update-data.handler", environment: { API_URL: api.url, }, }, }, }); }
Debugging Deployments
Check Deployment Status
List all stacks
npx sst stacks list --stage dev
Get stack info
npx sst stacks info API --stage dev
View Logs
Tail logs for API function
npx sst logs --stage dev --function api
Tail logs with filter
npx sst logs --stage dev --function api --filter "ERROR"
Console Access
Open SST console
npx sst console --stage dev
Check Outputs
Get stack outputs
npx sst outputs --stage dev
Common Issues
Deployment Failures
Issue: Deployment fails with timeout Solution: Increase Lambda timeout or check network issues
const api = new Function(stack, "api", { timeout: "120 seconds", // Increase from 30s });
Issue: Out of memory errors Solution: Increase memory allocation
const api = new Function(stack, "api", { memory: 2048, // Increase from 1024 });
Issue: Domain not working Solution: Verify DNS propagation and Route53 configuration
Check DNS records
dig sgcarstrends.com dig api.sgcarstrends.com
Check certificate status in AWS Console
Environment Variable Issues
Issue: Environment variables not available Solution: Check they're set in SST config
environment: { DATABASE_URL: process.env.DATABASE_URL!, // Ensure all required vars are listed }
Issue: Secrets not found Solution: Set secrets for the correct stage
npx sst secrets set DATABASE_URL "value" --stage production
Monitoring
CloudWatch Metrics
SST automatically creates CloudWatch metrics for:
-
Lambda invocations
-
Errors
-
Duration
-
Throttles
-
Concurrent executions
Access in AWS Console: CloudWatch → Metrics → Lambda
Alarms
// infra/monitoring.ts import { StackContext, use } from "sst/constructs"; import { Alarm } from "aws-cdk-lib/aws-cloudwatch"; import { API } from "./api";
export function Monitoring({ stack }: StackContext) { const { api } = use(API);
new Alarm(stack, "ApiErrorAlarm", { metric: api.metricErrors(), threshold: 10, evaluationPeriods: 1, alarmDescription: "Alert when API has more than 10 errors", }); }
Cost Optimization
Use ARM Architecture
const api = new Function(stack, "api", { architecture: "arm64", // 20% cheaper than x86 });
Appropriate Memory
// Don't over-provision const api = new Function(stack, "api", { memory: 1024, // Start here, adjust based on metrics });
Enable Minification
const api = new Function(stack, "api", { nodejs: { esbuild: { minify: app.stage === "production", }, }, });
CI/CD Integration
GitHub Actions
.github/workflows/deploy-prod.yml
name: Deploy Production
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-southeast-1
- name: Deploy to production
run: pnpm deploy:prod
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
UPSTASH_REDIS_REST_URL: ${{ secrets.UPSTASH_REDIS_REST_URL }}
UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN }}
Rollback Strategy
Rollback Deployment
List recent deployments
aws cloudformation describe-stacks --stack-name sgcarstrends-api-production
Rollback to previous version
npx sst deploy --stage production --rollback
Manual Rollback
-
Identify last good deployment
-
Checkout that commit
-
Redeploy
git log --oneline git checkout <commit-hash> npx sst deploy --stage production
Best Practices
Resource Naming
// ✅ Good - clear, scoped names new Function(stack, "ApiHandler", { ... }); new Bucket(stack, "UserUploads", { ... });
// ❌ Bad - generic names new Function(stack, "Function1", { ... }); new Bucket(stack, "Bucket", { ... });
Environment Management
// ✅ Good - environment-specific config const config = getConfigForStage(app.stage);
// ❌ Bad - hardcoded values const config = { memory: 1024 };
Outputs
// ✅ Good - add useful outputs stack.addOutputs({ ApiUrl: api.url, BucketName: bucket.bucketName, });
References
-
SST Documentation: https://docs.sst.dev
-
AWS CDK: https://docs.aws.amazon.com/cdk
-
Related files:
-
infra/
-
All infrastructure code
-
infra/CLAUDE.md
-
Infrastructure documentation
-
Root CLAUDE.md - Project documentation
Best Practices
-
Use Stages: Separate dev/staging/prod environments
-
Secrets Management: Use SST secrets for sensitive data
-
Monitoring: Set up CloudWatch alarms
-
Cost Optimization: Use ARM, appropriate memory
-
Naming: Use clear, descriptive resource names
-
Outputs: Export useful values for reference
-
Removal Policy: Retain production data
-
Testing: Test deployments in staging first