gs-sst-infra
SKILL.md
SST v3 Infrastructure
Project Structure
project/
├── sst.config.ts # Main SST config
├── stacks/
│ ├── dynamodb.ts # Database stack
│ ├── nextjs.ts # Next.js deployment
│ └── api.ts # Lambda API stack
├── open-next.config.ts # Lambda streaming config
└── src/
└── backend/ # Clean Architecture backend
Main Config
// sst.config.ts
export default $config({
app(input) {
return {
name: 'my-app',
removal: input?.stage === 'prod' ? 'retain' : 'remove',
protect: ['prod'].includes(input?.stage ?? ''),
home: 'aws',
providers: { aws: { region: 'us-east-1' } },
}
},
async run() {
const { table } = await import('./stacks/dynamodb')
const { site } = await import('./stacks/nextjs')
return { url: site.url, tableName: table.name }
},
})
DynamoDB Stack
// stacks/dynamodb.ts
export const table = new sst.aws.Dynamo('Table', {
fields: {
pk: 'string', sk: 'string',
gsi1pk: 'string', gsi1sk: 'string',
},
primaryIndex: { hashKey: 'pk', rangeKey: 'sk' },
globalIndexes: {
gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },
},
transform: {
table: (args) => { args.billingMode = 'PAY_PER_REQUEST' },
},
})
Next.js Stack
// stacks/nextjs.ts
import { table } from './dynamodb'
export const site = new sst.aws.Nextjs('Site', {
path: 'apps/web',
link: [table],
environment: { TABLE_NAME: table.name },
domain: {
name: `${$app.stage === 'prod' ? '' : `${$app.stage}.`}myapp.com`,
dns: sst.aws.dns({ zone: 'myapp.com' }),
},
})
Lambda Handler with Clean Architecture
Lambda handlers follow the thin adapter pattern - resolve Use Case from DI and execute.
// src/functions/create-category.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda'
import { DIContainer, TOKENS, initializeDI } from '@/backend/di'
import { CreateCategoryUseCase } from '@/backend/application/category/use-cases'
import { DomainException } from '@/backend/domain/shared/exceptions'
// Initialize DI once per cold start
let initialized = false
export async function handler(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> {
// Initialize DI Container (cold start only)
if (!initialized) {
await initializeDI()
initialized = true
}
try {
const input = JSON.parse(event.body ?? '{}')
// Thin adapter: resolve → execute → return
const useCase = DIContainer.resolve<CreateCategoryUseCase>(
TOKENS.CreateCategoryUseCase
)
const result = await useCase.execute(input)
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
}
} catch (error) {
return mapErrorToResponse(error)
}
}
function mapErrorToResponse(error: unknown): APIGatewayProxyResultV2 {
if (error instanceof DomainException) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: error.message, code: error.code }),
}
}
console.error('Unhandled error:', error)
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Internal server error' }),
}
}
Lambda API Stack
// stacks/api.ts
import { table } from './dynamodb'
const api = new sst.aws.ApiGatewayV2('Api')
api.route('POST /categories', {
handler: 'src/functions/create-category.handler',
link: [table],
environment: { TABLE_NAME: table.name },
})
api.route('GET /categories/{id}', {
handler: 'src/functions/get-category.handler',
link: [table],
environment: { TABLE_NAME: table.name },
})
export { api }
OpenNext Config
// open-next.config.ts
import type { OpenNextConfig } from 'open-next/types/open-next'
const config: OpenNextConfig = {
default: {
override: { wrapper: 'aws-lambda-streaming' },
},
}
export default config
Commands
# Development
npx sst dev --stage dev
# Deploy
npx sst deploy --stage dev
npx sst deploy --stage prod
# Secrets
npx sst secret set AUTH_SECRET "value" --stage dev
npx sst secret list --stage dev
# Outputs
npx sst outputs --stage dev
Environment Stages
| Stage | Domain | Protection | Removal |
|---|---|---|---|
| dev | dev.app.com | No | Remove |
| test | test.app.com | No | Remove |
| prod | app.com | Yes | Retain |
CI/CD (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
inputs:
stage:
type: choice
options: [dev, test, prod]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- run: pnpm install --frozen-lockfile
- run: npx sst deploy --stage ${{ inputs.stage }}
Local Development
# DynamoDB Local
docker run -p 8000:8000 amazon/dynamodb-local
# Environment
TABLE_NAME=dev-Table
DYNAMODB_LOCAL=true
References
- Feature Architecture:
skills/feature-architecture/SKILL.md - Bun Lambda:
skills/bun-aws-lambda/SKILL.md
Weekly Installs
1
Repository
gilbertopsantos…cknextjsGitHub Stars
1
First Seen
13 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1