| name | sst-deployment |
| description | Configure or update SST v3 infrastructure resources for AWS deployment. Use when adding new AWS resources, modifying Lambda configurations, or updating domain settings. |
| allowed-tools | Read, Edit, Bash, Grep, Glob |
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 codeinfra/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