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.

  • Cloud Platforms
  • $sst-apps
  • 164 lines
  • 2 files
  • ~1,650 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- sst-apps

Installs to ~/.agents/skills/sst-apps/ for every project on this machine.

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.

~/.agents/skills/sst-apps/SKILL.md

--- name: sst-apps description: Builds and operates SST v3 (Ion) apps - sst.config.ts with $config, stages, removal and protect policies, components such as sst.aws.Nextjs,… license: MIT ---

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

  1. Read AGENTS.md, sst.config.ts, package.json and the sst version. v2 projects use sst.config.ts with stacks and CDK constructs; v3 uses $config with app() and run(). Do not mix their docs.
  2. Check .sst/ is gitignored and sst-env.d.ts exists (generated types for links).
  3. Find which AWS profile or credentials each stage uses (providers.aws.profile).
  4. sst deploy, sst dev and sst secret need network and cloud credentials. In an offline sandbox, edit config and code, run tsc, 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: true blocks sst remove for that stage entirely. Set it for production.
  • Do not import packages at the top of sst.config.ts; use dynamic await import() inside run() 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

NeedComponent
Next.js, Astro, Remix, SvelteKit, Nuxt, SolidStartsst.aws.Nextjs, sst.aws.Astro, … (OpenNext and adapters under the hood)
Static SPAsst.aws.StaticSite
HTTP API in Lambdasst.aws.Function with url: true, or sst.aws.ApiGatewayV2 for routes and auth
Long-running container servicesst.aws.Cluster plus sst.aws.Service (ECS Fargate) in a Vpc
Queue and consumersst.aws.Queue with .subscribe()
Postgressst.aws.Postgres (RDS) or sst.aws.Aurora; both need a Vpc
Path routing across several apps under one domainsst.aws.Router
Cloudflare Worker, KV, D1sst.cloudflare.Worker and related components, with home: "cloudflare" or a cloudflare provider
Anything without a componentUse 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 --noEmit passes with the generated sst-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 protect and removal: "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 dev deploys real infrastructure to your personal stage; it costs money while running and leaves resources until you sst remove it.
  • Functions in a VPC need a NAT gateway or VPC endpoints to reach the internet and AWS APIs; sst.aws.Vpc has a nat option, and NAT gateways are a fixed monthly cost.
  • Interrupted deploys leave a lock in the state; sst unlock is safe only when no other deploy is actually running.
  • Upgrading SST changes the bundled provider versions; run sst diff after every upgrade.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem