SST v3 Apps skill for Codex
TypeScript infrastructure with links, not hardcoded ARNs. Codex writes SST v3 apps the Ion way: $config with protected, retained production stages, components such as Nextjs, Function, Postgres and Cron, and resources reached through link and the typed Resource helper instead of copied names. Secrets go through sst secret, every production deploy is preceded by sst diff and your approval, and preview stages get torn down.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- sst-apps Installs to ~/.agents/skills/sst-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install sst-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project sst-apps Installs to .agents/skills/sst-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/sst-apps.zipunzip -o sst-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) sst-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $sst-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and operates SST v3 (Ion) apps - sst.config.ts with $config, stages, removal and protect policies, components such as sst.aws.Nextjs, Function, Bucket, Postgres, Vpc, Cluster and Cron, resource linking with the Resource helper, sst secret, sst dev live mode, multi-account deploys and Cloudflare components. Use when starting an SST project, adding infrastructure to sst.config.ts, a link or secret is undefined at runtime, sst deploy fails or is locked, a stage must be torn down, or migrating from SST v2 or CDK-based setups.
Use it when
- Starting or extending an SST v3 project on AWS or Cloudflare
- A linked resource or secret is undefined at runtime
- sst deploy fails, is locked, or wants to replace a stateful resource
- Setting up per-developer or per-PR stages
Use something else when
- The project is plain AWS CDK: use aws-cdk
- You want general Pulumi programs without SST components: use pulumi-iac
How to check the result
sst diff for the target stage shows only intended changes with no stateful replacements, and deployed outputs respond with linked resources readable in logs.
The mistake it prevents
Renaming a component's logical name, which replaces the underlying bucket or database and loses its data.
SST v3 defines infrastructure in TypeScript in sst.config.ts, runs on Pulumi and
Terraform providers (not CloudFormation or CDK), and keeps state in your own account. A
well-run SST app has production protected from removal, one stage per developer, every
resource reached through link instead of hardcoded names, and secrets set with
sst secret rather than in .env files.
Inspect first
- Read
AGENTS.md,sst.config.ts,package.jsonand thesstversion. v2 projects usesst.config.tswithstacksand CDK constructs; v3 uses$configwithapp()andrun(). Do not mix their docs. - Check
.sst/is gitignored andsst-env.d.tsexists (generated types for links). - Find which AWS profile or credentials each stage uses (
providers.aws.profile). sst deploy,sst devandsst secretneed network and cloud credentials. In an offline sandbox, edit config and code, runtsc, and hand deploy commands to the user.
Config skeleton
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "acme",
home: "aws",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
providers: { aws: { region: "us-east-1" } },
};
},
async run() {
const vpc = new sst.aws.Vpc("Vpc", { bastion: false });
const db = new sst.aws.Postgres("Db", { vpc });
const uploads = new sst.aws.Bucket("Uploads");
const stripeKey = new sst.Secret("StripeSecretKey");
const api = new sst.aws.Function("Api", {
handler: "packages/functions/src/api.handler",
url: true,
link: [db, uploads, stripeKey],
vpc,
});
new sst.aws.Cron("Nightly", {
schedule: "cron(0 4 * * ? *)",
function: { handler: "packages/functions/src/nightly.handler", link: [db] },
});
const web = new sst.aws.Nextjs("Web", {
path: "packages/web",
link: [api, uploads],
domain: $app.stage === "production" ? "app.example.com" : undefined,
});
return { api: api.url, web: web.url };
},
});
Rules for app():
removal: "retain"on production keeps buckets and databases if the stage is removed.protect: trueblockssst removefor that stage entirely. Set it for production.- Do not import packages at the top of
sst.config.ts; use dynamicawait import()insiderun()if you need one.
Linking instead of env vars
link grants IAM permissions and exposes typed values at runtime:
import { Resource } from "sst";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
export async function handler() {
await s3.send(new PutObjectCommand({ Bucket: Resource.Uploads.name, Key: "x.txt", Body: "hi" }));
const key = Resource.StripeSecretKey.value;
const { host, port, username, password, database } = Resource.Db;
// ...
}
Resource.X is undefined when the resource is not in that function’s link array, or
when the code runs outside SST (plain node). Run scripts with sst shell -- <cmd> to get
links locally.
Secrets
npx sst secret set StripeSecretKey sk_test_xxx # current stage
npx sst secret set StripeSecretKey "$PROD_KEY" --stage production # confirm with user
npx sst secret list --stage production
npx sst secret set StripeSecretKey sk_test_xxx --fallback # default for all dev stages
Secrets are encrypted in the state bucket and injected through links. A secret declared in config but never set fails the deploy with a clear error; set it before deploying.
Daily commands
npx sst dev # personal stage; live functions, runs frontends in a multiplexer
npx sst deploy --stage staging
npx sst diff --stage production # preview changes before a production deploy
npx sst deploy --stage production # only after the user approves the diff
npx sst remove --stage pr-123 # tear down a preview stage
npx sst unlock --stage staging # after an interrupted deploy left a lock
npx sst refresh --stage staging # reconcile state with what exists in the cloud
Stage naming: default stage is your local username, so each developer gets an isolated
copy. Use pr-<number> for previews and remove them when the PR closes.
Component decision table
| Need | Component |
|---|---|
| Next.js, Astro, Remix, SvelteKit, Nuxt, SolidStart | sst.aws.Nextjs, sst.aws.Astro, … (OpenNext and adapters under the hood) |
| Static SPA | sst.aws.StaticSite |
| HTTP API in Lambda | sst.aws.Function with url: true, or sst.aws.ApiGatewayV2 for routes and auth |
| Long-running container service | sst.aws.Cluster plus sst.aws.Service (ECS Fargate) in a Vpc |
| Queue and consumer | sst.aws.Queue with .subscribe() |
| Postgres | sst.aws.Postgres (RDS) or sst.aws.Aurora; both need a Vpc |
| Path routing across several apps under one domain | sst.aws.Router |
| Cloudflare Worker, KV, D1 | sst.cloudflare.Worker and related components, with home: "cloudflare" or a cloudflare provider |
| Anything without a component | Use the Pulumi provider directly (aws.s3.BucketPolicy, etc.) inside run() |
Customize the underlying resources with transform rather than forking a component:
new sst.aws.Bucket("Uploads", {
transform: { bucket: (args) => { args.forceDestroy = $app.stage !== "production"; } },
});
Verification
-
npx tsc --noEmitpasses with the generatedsst-env.d.ts. -
npx sst diff --stage <stage>shows only intended creates and updates; no replacements of databases or buckets unless planned. - After deploy, outputs (URLs) respond; a linked resource read works in logs.
- Production stage has
protectandremoval: "retain". - Preview stages are removed after merge (CI job or manual
sst remove).
Gotchas
- Renaming a component’s logical name (the first argument) replaces the resource; for a database or bucket that means data loss. Keep names stable.
sst devdeploys real infrastructure to your personal stage; it costs money while running and leaves resources until yousst removeit.- Functions in a VPC need a NAT gateway or VPC endpoints to reach the internet and AWS
APIs;
sst.aws.Vpchas anatoption, and NAT gateways are a fixed monthly cost. - Interrupted deploys leave a lock in the state;
sst unlockis safe only when no other deploy is actually running. - Upgrading SST changes the bundled provider versions; run
sst diffafter every upgrade.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "SST v3 Apps"
short_description: "TypeScript infrastructure with links, not hardcoded ARNs"
default_prompt: "$sst-apps Add an S3 bucket and a Postgres database to our SST app and link them to the API" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.