Core Concepts
Eager discovery
Every resource starts an API lookup the moment it is declared - before deploy() is called.
class MyStack extends Stack {
// discoverVm("ix-sto1-app01") fires here, in the constructor
app = Proxmox.VM("ix-sto1-app01").cores(4).memory(8192);
}
By the time the decorator triggers deploy(), the discovery promise is already in flight (or done). Puls awaits it, checks the result, and either creates, skips, or updates the resource.
Idempotency
Every provider implements the same contract:
- Resource exists and matches intent → log and return, no API write
- Resource exists but differs → update only what changed
- Resource does not exist → create it
Running the same stack twice never produces duplicates or errors.
Stack
A Stack is a plain class that holds resource declarations as properties. The @Deploy decorator instantiates it and calls deploy(), which iterates over every BaseBuilder property in order.
@Deploy({ proxmox: CONFIG.STAGING })
class MyStack extends Stack {
vm1 = Proxmox.VM("host-01")...; // deployed first
vm2 = Proxmox.VM("host-02")...; // deployed second
}
destroy() tears down in reverse order.
Sidecars
Some methods automatically create and wire up additional resources. Sidecars are deployed after their parent and destroyed before it.
DO.Droplet("web").allowPublicWeb() // creates + attaches a Firewall sidecar
DO.Domain("x.com").withSSL() // creates a Certificate sidecar
AWS.Route53().withWildcardSSL() // creates an ACM certificate sidecar
Dry run
Setting dryRun: true makes every resource print what it would do without touching any API. Async waits (boot, DNS propagation, cloud-init) are skipped entirely in dry-run mode.
The @DryRun decorator is a shorthand for the above.
CONFIG pattern
Credentials are defined once as typed constants and referenced everywhere:
// src/types/proxmox.ts
export const CONFIG = {
STAGING: {
url: process.env.PROXMOX_URL!,
user: process.env.PROXMOX_USER!,
tokenName: process.env.PROXMOX_TOKEN_NAME!,
tokenSecret: process.env.PROXMOX_TOKEN_SECRET!,
nodes: process.env.PROXMOX_NODES?.split(','),
dnsDomain: process.env.PROXMOX_DNS_DOMAIN,
dnsServers: process.env.PROXMOX_DNS_SERVERS?.split(','),
verifySsl: false,
},
};
// any stack file
@Deploy({ proxmox: CONFIG.STAGING })
@Destroy({ proxmox: CONFIG.STAGING })
Extensibility & Custom Types
Puls is designed to be completely extendable. While it ships with built-in constants (like AWS_TYPES or PROXMOX_TYPES), you are not required to use them.
If the built-in types don't match your environment (e.g., a custom VM image, a private AWS region, or specific instance sizes), you can simply create your own constants in your project and pass them to the builder methods.
Example: Custom VM Images
// your-project/types/my-images.ts
export const MY_OS = {
GOLDEN_IMAGE: "1234", // Your custom template ID
LEGACY_APP: "9999",
} as const;
// your-project/stack.ts
import { MY_OS } from "./types/my-images.js";
@Deploy({ proxmox: CONFIG.PROD })
class MyStack extends Stack {
app = Proxmox.VM("app-01").image(MY_OS.GOLDEN_IMAGE)...;
}
Since the DSL methods typically accept strings or numbers, any constant you define will work as long as the underlying provider API understands it.