Escape hatches
Three levels of control for when a primitive's defaults aren't enough — from a single property to the whole template.
wisp's primitives cover the common case well, but real infrastructure occasionally needs something outside what an option bag can express. There are three levels, in order of how much of the synthesized template they touch.
L1 — .override()
Every standalone primitive (Api, Store, Queue, Schedule, Bucket, Workflow) has an .override() method
that shallow-merges raw CloudFormation properties into its synthesized resource:
export const orders = new Store("orders", {
partitionKey: { name: "id", type: "S" },
});
orders.override({
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
});Multiple .override() calls on the same resource merge in call order — later keys win. It's a top-level statement,
like the primitive's own declaration, not something you call from inside a handler.
Per-route or per-consumer overrides (chaining .override() onto api.post(...)'s return, for instance) aren't
supported — the compiler flags the attempt with a clear diagnostic (WISP-CONFIG-003) rather than silently doing
nothing.
The always-on denylist still runs on the result — .override() can't be used
to smuggle a provisioned-throughput table or a VPC config past the $0-idle guarantee.
L2 — addRawResource
For infrastructure wisp has no primitive for at all — an SNS topic, a CloudFront distribution, anything — add a plain CloudFormation resource directly:
import { addRawResource } from "@vorynza/wisp";
addRawResource("MyCustomTopic", {
Type: "AWS::SNS::Topic",
Properties: { TopicName: "my-custom-topic" },
});The first argument is the resource's literal logical ID — used as-is, bypassing wisp's normal hash-derived ID
scheme. If it collides with an internally-generated one, synthesis fails loudly (WISP-CONFIG-004) rather than
silently overwriting a real resource.
Raw resources go through the always-on denylist exactly like everything else — a raw AWS::RDS::DBInstance is still
caught.
L3 — transform
For changes that need to see (or reshape) the whole template, wisp.config.ts accepts a transform function:
export default {
app: "my-app",
target: "aws",
region: "eu-north-1",
transform: (template) => {
template.Description = "my-app, deployed with extra love";
return template;
},
};transform runs after L1 and L2 have already been baked into the template by the compiler, and the always-on
denylist is re-scanned on its output — the same rule as L1 and L2: none of the three levels can bypass the
$0-idle guarantee.
transform currently only sees the root template. If your app is large enough to have split into nested
stacks, the resources living in those shards aren't passed through it — a
deliberate scope limit for now, not an oversight.
Which one should I reach for?
Start at L1. If there's no primitive to attach .override() to, use L2. Reach for L3 only when you need to reshape
something structural across the whole template — it's the broadest hatch and the easiest one to make a template hard
to reason about.

