Shipping a full-stack app on AWS works better when the AWS Console is not the source of truth. Frontend, secrets, domains, and stage rules belong in TypeScript next to the application, reviewed in pull requests, and applied the same way in every environment.
SST defines the app in code — usually from a single sst.config.ts — and automates the underlying AWS resources. This site is a Bun + Turborepo monorepo with Next.js on OpenNext, and stages for local, shared preview, and production.
Three infra stories stay separate:
- Deployed today —
sst.config.tsimports the Next.js / OpenNext stack only. - Defined in-repo, not yet imported — container API infra (
Vpc→Cluster→Service+Router) lives underpackages/infraand stays commented out ofrun()until that stack should deploy. - Related pattern elsewhere — Lambda + API Gateway for a Hono API is in Backend APIs with Hono, Drizzle, Zod OpenAPI, and SST. That note is not what this site’s
sst.config.tsruns.
1. Why SST for This Stack
SST lets developers define app features as Components without assembling every low-level resource by hand.
- Higher-level components used here:
sst.aws.Nextjs,sst.Secret, and (when the API module is wired)sst.aws.Service,sst.aws.Router,sst.aws.Vpc, andsst.aws.Cluster. - Resource linking so runtime code can read infrastructure through a typed
ResourceSDK instead of hardcoded names and ARNs. sst devas a unified local environment: infra watcher, frontend (and later container)devprocesses from the same CLI, plus Live when Lambda handlers are in the stage.
This site: Next.js via OpenNext, stage-based domains, edge Basic Auth on non-prod, application packages separate from packages/infra. SST’s comparison to Pulumi or CDK for Terraform is in the SST FAQ.
2. How SST Compares to Other IaC
All of these tools share infrastructure as code. SST is framed for developers; CDKTF and Pulumi are primarily for DevOps engineers. SST still uses open-source Pulumi and bridged Terraform providers; it does not require a Pulumi account, and app state stays with the team’s cloud account (SST docs).
| Tool | Mental model | Strength | Why SST fits app stacks here |
|---|---|---|---|
| Terraform / OpenTofu | Declarative HCL, provider-centric | Universal, mature state and workflow; strong for org-wide platform teams | App and infra stay split; no first-class Live, linking, or Nextjs DX for this monorepo |
| AWS CDK | TypeScript constructs → CloudFormation | Deep AWS coverage and Constructs ecosystem | Strong AWS-native modeling; weaker unified app loop (linking, sst dev, frontend + backend multiplexer) for this workflow |
| Pulumi / CDKTF | General-purpose IaC in real languages | Flexible providers; good when a platform team owns the program | SST sits on Pulumi but adds app components, linking, and sst dev without a full Pulumi program for every feature |
| Serverless Framework | Framework focused on serverless apps | Familiar Lambda packaging | Narrower than full-stack Next/OpenNext, containers, and stage workflows used here |
| Vercel / PaaS | Git push → hosted platform | Fast frontend DX with little AWS surface | Strong when only a managed frontend is needed; SST when TypeScript ownership of AWS resources, domains, secrets, and optional ECS/API matters |
- SST components encode common app patterns and still allow
transformplus 150+ providers when lower-level control is required. - Linking closes the runtime gap that Terraform or CDK usually fill with hand-wired env vars and IAM.
sst devis a product feature rather than an assemble-yourself local stack. - SST still has state — local plus a backup bucket — so managed resources should not be edited by hand in the Console (Basics). Backup of that state, and when a snapshot is not a failover, is Disaster Recovery in System Design.
SST is a poor fit when a platform team already owns shared networking in Terraform or OpenTofu, when Pulumi or Terraform is the team language for multi-cloud control planes, or when CloudFormation-native CDK construct libraries are the hiring and review surface.
3. Project Shape: sst.config.ts and packages/infra
SST supports drop-in mode — a single sst.config.ts next to a Next.js app — and a monorepo where the config stays at the root and infrastructure is split into modules. This project uses the monorepo shape.
sst.config.ts app name, region, protect / removal, imports
packages/infra/ Next.js, secrets, domain, edge, optional API/VPC
apps/web Next.js site
apps/api Hono / Mastra API
packages/* shared auth, db, content, design systemThe config file defines app identity and safety rules. The run function loads infra modules. Today only Next.js is imported; the API module stays commented until that stack should deploy:
export default $config({
app(input) {
return {
name: "my-app",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
providers: {
aws: {
region: "us-east-1",
},
},
}
},
async run() {
// await import("./infra/api")
await import("./infra/nextjs")
},
})nextjs— OpenNext deployment and domains (imported today)secrets— stage secrets such as edge Basic Auth credentials, plus database and auth secrets for the API pathdomain/edge— apex domain and non-prod Basic Auth at the edgeapi/router/vpc/cluster— container API path, defined but not imported yet
Business logic stays out of these files. Infra modules create and link resources; application packages consume them.
4. Components in This Stack
The site deploys with sst.aws.Nextjs. Production uses the apex domain; other stages get {stage}.example.com.
const isProd = $app.stage === "production"
const domain = "example.com"
export const nextjs = new sst.aws.Nextjs("Web", {
path: "apps/web",
domain: {
name: isProd ? domain : `${$app.stage}.${domain}`,
redirects: isProd ? [`www.${domain}`] : [],
},
warm: isProd ? 1 : 0,
environment: {
NEXT_PUBLIC_SITE_URL: `https://${domain}`,
SST_STAGE: $app.stage,
},
openNextVersion: "4.0.3",
edge,
dev: {
command: "bun run dev",
directory: "apps/web",
url: "http://localhost:3000",
},
})- Production keeps one warm server instance; non-prod uses
warm: 0. Theedgeprop attaches non-prod Basic Auth. Thedevblock tellssst devhow to start the local Next process. - Secrets are first-class resources, not loose
.envfiles:sst.Secret("Username"),Password,DatabaseUrl,BetterAuthSecret,AiGatewayApiKey. - When the API module is imported, the path is
Vpc→Cluster→Service, withRouterforapi.example.comor{stage}.api.example.com. Untilawait import("./infra/api")is enabled, that stack does not deploy. - A different shape — Lambda behind API Gateway with a Lambda authorizer — is the pattern in Backend APIs with Hono, Drizzle, Zod OpenAPI, and SST. That uses
sst.aws.Functionandsst.aws.ApiGatewayV2. It is not the infrastructure this site’s config currently loads.
5. Linking Instead of Hardcoded Env
Resource linking is the bridge between infrastructure and runtime. A resource is created, passed in link, and read with the SDK.
- Linking injects values, generates types (
sst-env.d.ts), and grants permissions where the component supports them. - On this site today, the Next.js stack pulls edge Basic Auth credentials through the
edgeconfiguration. The web app itself does not readResource.*for the database. - When the API module is imported, secrets are linked into the container service.
import { Resource } from "sst"
const client = postgres(Resource.DatabaseUrl.value, {
ssl: "require",
})- Linked values are available on the server side only. Client components only see what is passed in explicitly.
- For CLIs that need stage secrets — Drizzle migrations, Better Auth schema generation —
sst shellloads the linked stage environment without inventing a second secrets path.
6. Stages as the DevOps Unit
In SST, a stage is an environment: a namespaced copy of the app.
| Stage | Purpose | Domain pattern |
|---|---|---|
local / personal | Daily sst dev | local Next URL; cloud resources for that stage |
dev | Shared preview | dev.example.com |
production | Live site | example.com |
- Production is protected. Accidental
sst removeshould not wipe critical data, so the config usesprotectandremoval: "retain"on production (removal policies). - Non-production stages get CloudFront Basic Auth via an edge
viewerRequestinjection, so preview URLs are not public by default. - Credentials stay in the local AWS credential chain (profile or SSO session). SST deploys into that account and region. Next.js and secret resources are not created by clicking through the Console.
aws sso login # refresh AWS credentials
sst dev --stage local # personal / local stage
sst deploy --stage dev # shared preview
sst deploy --stage production7. Local Workflow: sst dev, Live, and Mode Discipline
sst dev is the local control plane.
- A watcher that deploys infrastructure changes.
- The Next.js
devblock — local Next, linked to the stage’s deployed resources. - Later, when the API module is imported, the container
devcommand and an optional tunnel to VPC resources. - Live — stub Lambdas in AWS that proxy to the local machine — matters most for Lambda-heavy stages, including the Function / API Gateway pattern in the backend APIs note. It is less central while this site only imports OpenNext.
sst dev belongs on a personal or local stage. Shared dev and production get sst deploy. A shared URL means deploying a real stage. Debugging a Lambda handler with breakpoints means Live on a personal stage — with VS Code Auto Attach when debugging Node handlers.
Failure: flipping the same stage between Live stubs and real deploys. If the CLI is killed, stubs can remain and remote invokes time out waiting for the local machine (Live quirks).
8. Production Safety and the CI Boundary
IaC only helps if state and process stay honest.
- Do not Console-edit resources SST manages. SST applies diffs from config against state; manual edits drift that state (Basics).
- Protect production with
protectandremoval: "retain". The Postgres used by the API path is an external URL viasst.Secret, not ansst.aws.Postgresthis config created. - Verify, then deploy in CI. GitHub Actions runs install, lint, format check, typecheck, tests, and build first. Only after those checks succeed does the workflow run
sst deployfordevorproduction. - Local
deploy:dev/deployscripts remain useful for intentional one-off deploys. The default path is CI: green checks, then deploy. - The SST Console is another way to get git-push autodeploy. This site uses GitHub Actions for the verify-then-
sst deploysequence instead.
Tenant isolation and SaaS boundary design are a different problem — see Building a Multi-Tenant Backend with Hono, Better Auth, Drizzle, and Postgres RLS.
9. Mental Model
sst.config.ts
→ packages/infra/nextjs (imported today)
→ Nextjs / OpenNext + edge Basic Auth secrets
→ packages/infra/api (defined, not imported)
→ Vpc / Cluster / Service / Router + linked secrets
→ link (when a consumer is linked)
→ Resource.* in app runtime
→ sst shell for CLIs
stages: local | dev | production
local: sst dev
ci: checks → tests → build → sst deploy --stage …SST keeps AWS infrastructure as reviewable TypeScript beside the app. Terraform, CDK, or Pulumi remain the better fit when the problem is org-wide platform ownership. For current component and CLI details, start at the SST docs. For the Hono API on Lambda or Fargate, continue with Backend APIs with Hono, Drizzle, Zod OpenAPI, and SST.