SST v3 Infrastructure Stack
Wire up a Bun monorepo prototype for AWS deployment using SST v3 (Ion). Lambda
functions run nodejs22.x (not Bun — Bun is the local dev/build runtime only).
Hono apps bridge to Lambda via hono/aws-lambda. Infrastructure modules live in
infra/ and are imported by sst.config.ts at the repo root.
Defaults and Deviations
These are team defaults. In execution mode, follow them unless the project CLAUDE.md explicitly overrides them.
In Plan mode, apply the deviation protocol from prototyping-skills:team-conventions:
state the default, name the alternative, explain the trade-off, flag the blast radius,
let the human decide.
Key defaults:
- SST version: v3 (Ion) — not SST v2
- Lambda runtime:
nodejs22.x— neverbunornodejs18.x - Secrets:
sst.Secretfor all sensitive vars — never plaintext in infra files - Removal policy:
retainfor production,removefor all other stages - Env bridging: Bridge
Resource.<X>.valuetoprocess.env.Xat handler top — app packages readprocess.envorBun.env, not SST at runtime
Pre-flight Checklist
Run ALL of these checks before generating any file:
- Detect packages:
ls packages/— record which ofapi,ui,core,mcpexist - Detect UI framework:
grep '"next"' packages/ui/package.json 2>/dev/null— only generateinfra/frontend.tsif Next.js is confirmed - Detect env vars:
cat .env.example 2>/dev/nullthen grep forBun.env.andprocess.env.across all packages — build the full list of secrets to declare - Detect native modules:
grep -r '@libsql/client\|better-sqlite3' packages/*/package.json 2>/dev/null— addnodejs.installif found - Pipeline gate — use
AskUserQuestion: "Does this project have a background processing pipeline? (queue-based jobs, scheduled Lambda functions, cron workers)" - App name gate — use
AskUserQuestion: "What is the SST app name? (kebab-case, e.g. 'my-prototype') This becomes the CloudFormation stack prefix."
App Export Split (Critical)
Every project built with init-prototype has packages/api/src/index.ts exporting:
export default { port, fetch: app.fetch }. Lambda cannot use this — it needs a Hono
app exported from a separate module. Detect before generating infra:
grep -l "export default.*fetch" packages/api/src/index.ts
If found:
- Create
packages/api/src/app.ts— move the Hono app construction and all route mounts here, addexport default app - Update
packages/api/src/index.ts— importappfrom./app.js(.jsrequired for ESM), keep only the Bun server entry (Bun.serve({ port, fetch: app.fetch }))
See references/sst-patterns.md → App Export Split for full file templates.
Step-by-Step Execution
Execute all steps in order. Skip steps whose conditions aren't met (e.g. skip Step 7 if no Next.js).
Step 1 — Root package.json scripts
Add "sst": "^3.0.0" to root package.json devDependencies. Add scripts:
"sst:dev": "sst dev",
"sst:deploy": "sst deploy",
"sst:remove": "sst remove"
Step 2 — infra/secrets.ts
Convert all detected env vars to sst.Secret() declarations. Conversion rule:
SNAKE_CASE → PascalCase per segment (OPENAI_API_KEY → OpenaiApiKey).
Add all to the allSecrets array and export it.
Exclusions: NEXT_PUBLIC_*, PORT, NODE_ENV.
See Secret Name Conversion Table below and references/sst-patterns.md → infra/secrets.ts.
Step 3 — sst.config.ts
Always generate. Import only the infra modules that were actually generated.
Set removal: input?.stage === "production" ? "retain" : "remove". Use the app
name provided by the user in Step 0.
See references/sst-patterns.md → sst.config.ts.
Step 4 — App export split
Run the detection from App Export Split above. Create app.ts and fix index.ts
if needed. This must be done before generating infra handler files.
Step 5 — infra/api.ts (if packages/api exists)
Generate sst.aws.Function("Api", ...) with runtime nodejs22.x, timeout 30 seconds,
memory 512 MB, link: allSecrets, url: true. Add nodejs.install for native deps
if detected in pre-flight. Export apiUrl.
See references/sst-patterns.md → infra/api.ts.
Step 6 — infra/functions/api.ts (if packages/api exists)
Generate the Lambda handler: import handle from hono/aws-lambda and the Hono app
from ../../packages/api/src/app.js (.js extension required for ESM). Bridge all
detected secrets to process.env using ??=. Export handler.
See references/sst-patterns.md → infra/functions/api.ts.
Step 7 — infra/frontend.ts (if Next.js confirmed)
Generate sst.aws.Nextjs("Frontend", { path: "packages/ui", environment: {...} }).
NEXT_PUBLIC_API_URL gets apiUrl (stack output — not a secret). Auth vars
(NEXTAUTH_SECRET, NEXTAUTH_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
get .value from their secrets. Export frontendUrl.
See references/sst-patterns.md → infra/frontend.ts.
Step 8 — infra/pipeline.ts (only if user confirmed pipeline)
Ask a follow-up with AskUserQuestion: "What triggers the pipeline — a queue (SQS),
a schedule (cron), or both?" Then generate the appropriate pattern.
See references/sst-patterns.md → infra/pipeline.ts.
Step 9 — infra/tsconfig.json
Always generate. Extends ../tsconfig.json, includes ["**/*.ts", "../sst.config.ts"].
Step 10 — infra/CLAUDE.md
Always generate. Document secret-bridging convention, bundling rules, stage behavior, how to add new Lambdas, and SST commands.
See references/sst-patterns.md → infra/CLAUDE.md for exact content.
Secret Name Conversion Table
| Env Var | SST Secret Name |
|---|---|
OPENAI_API_KEY |
OpenaiApiKey |
GOOGLE_API_KEY |
GoogleApiKey |
GOOGLE_CLIENT_ID |
GoogleClientId |
GOOGLE_CLIENT_SECRET |
GoogleClientSecret |
NEXTAUTH_SECRET |
NextauthSecret |
NEXTAUTH_URL |
NextauthUrl |
DATABASE_URL |
DatabaseUrl |
DATABASE_AUTH_TOKEN |
DatabaseAuthToken |
JINA_API_KEY |
JinaApiKey |
GOOGLE_CX |
GoogleCx |
Rule: split on _, capitalize first letter of each word, join. GOOGLE_CX → GoogleCx.
Anti-Patterns
- Never hardcode secrets in infra files — always use
sst.Secret - Never use
Bun.envin Lambda handlers — useprocess.env(bridged via??=) - Never bundle native modules (
@libsql/client,better-sqlite3) with esbuild — usenodejs.install - Never use
sst.aws.Functionfor Next.js — usesst.aws.Nextjs - Never omit the removal policy — always set
removalinsst.config.ts - Never import from application packages without
.jsextension in Lambda handlers — ESM requires explicit extensions
Verification Checklist
After generating all files, run the TypeScript check:
bunx tsc --noEmit -p infra/tsconfig.json
Fix any type errors before proceeding. Then create this task:
TaskCreate({
subject: "Verify SST infrastructure before first deploy",
description: "1. Set all secrets: npx sst secret set <Name> <value> — see infra/secrets.ts for full list\n2. bun run sst:dev — confirm API URL is printed\n3. curl <API_URL>/health — confirm 200 response\n4. If frontend: confirm CloudFront URL loads\n5. bun run sst:remove to tear down dev stack when done"
})