How wisp works
The path from your TypeScript source to a deployed CloudFormation stack, one stage at a time.
wisp is a compiler, not a runtime framework. Nothing in wisp inspects your app while it's running: no dispatcher, no plugin registry, no dependency container. Everything is decided before deploy, by reading your source code.
That single decision explains most of wisp's behaviour — why routes must be top-level, why options have to be literals, and why the IAM policy it writes is as narrow as it is.
The pipeline
src/**/*.ts
│
│ 1. Discover find candidate source files
▼
TypeScript program
│
│ 2. Analyse read the AST, collect resources
▼
Resources + handlers
│
│ 3. Cross-reference link handlers to what they touch
▼
ResourceGraph
│
│ 4. Validate duplicate names, scope rules
▼
Validated graph
│
│ 5. Emit CloudFormation + derived IAM
▼
Templates
│
│ 6. Gate always-on cost scan
▼
.wisp/cloudformation.json → your AWS accountSteps 1 through 4 live in @vorynza/wisp-compiler. Step 5 is @vorynza/wisp-emit-aws. Step 6 is the
always-on denylist, and it's the last thing that runs before anything reaches
AWS.
1. Discover
wisp walks src/ and collects the TypeScript files that could declare resources. There's no manifest to keep in
sync and no decorator to register — the file layout is the registration.
2. Analyse
wisp builds a real TypeScript program and reads the syntax tree. When it finds new Store("orders", {...}) or
api.post("/orders", {}, handler), it records a resource: its name, its options, and the source location it came
from.
This step never executes your code. That's why the compiler insists on values it can read statically:
// wisp can read this.
export const orders = new Store("orders", {
partitionKey: { name: "id", type: "S" },
});
// wisp cannot read this — the options only exist once the code runs.
const options = loadOptionsFromSomewhere();
export const orders = new Store("orders", options);The second form is rejected with WISP-CONFIG-001 rather than
silently producing a half-configured table.
3. Cross-reference
Now wisp connects handlers to the resources they use. It resolves the symbol behind each call — not the text — so renaming an import doesn't confuse it:
import { orders as orderTable } from "../stores/orders.ts";
export const createOrder = api.post("/orders", {}, async ({ body }) => {
await orderTable.put(body); // still recognised as the "orders" Store
});Each connection becomes an edge in the graph, carrying the action performed: read, write, delete, send,
receive, or invoke. Those edges are what the IAM derivation reads later, which is why the permissions wisp
writes match what your code actually does. See IAM derivation.
4. Validate
Before emitting anything, wisp checks the graph as a whole: two resources sharing a name, a primitive constructed
somewhere it can't be statically found, a queue with more than one consumer. These are the
WISP-DUP, WISP-SCOPE, and WISP-CONFIG diagnostics.
5. Emit
The validated graph becomes CloudFormation. Each function gets its own role, built from its own edges and nothing else. If the app is large enough, the template is split into nested stacks.
Your escape hatches are applied here too — first property overrides, then raw resources, then the whole-template transform.
6. Gate
Finally wisp scans the finished template for resources that cost money while idle. This runs on the output of the
escape hatches, deliberately: a transform() cannot be used to slip an always-on resource past the check.
If the scan is clean, the template is written to .wisp/cloudformation.json and deployed. If it isn't, the deploy
stops and prints what it found.
Seeing it for yourself
Two commands expose the middle of the pipeline:
wisp graph # the ResourceGraph from step 3
wisp synth # the CloudFormation from step 5, written to .wisp/Neither touches AWS. wisp synth in particular is worth running before your first deploy — the output is plain,
readable JSON, and it's exactly what would have been sent.
Why a compiler
Reading code instead of running it costs you some flexibility: options must be literals, and resources must be declared where they can be found. In exchange:
- Permissions are exact. wisp knows every resource each handler touches, so it never has to guess and grant something broad.
- Cost is knowable before deploy. The template is complete before anything is created, so it can be scanned and priced.
- There's no runtime to pay for. Nothing of wisp's own dispatches your requests, so nothing of wisp's own shows up in your cold start.

