Skip to content

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.

@Deploy({ dryRun: true, proxmox: CONFIG.STAGING })
class MyStack extends Stack { ... }

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.