AWS Provider
Setup
Uses standard AWS SDK environment variables - no explicit AWS.init() needed:
Pass the region via decorator:
Constants
import { AWS_TYPES } from "puls-dev";
const { REGION, RUNTIME, DB, DB_SIZE, DISTRO, BUCKET } = AWS_TYPES;
Lambda
Deploy a function from a local directory or pre-built zip.
AWS.Lambda("my-function")
.code("./dist") // directory → auto-zipped, or pass a .zip path
.runtime(RUNTIME.NODEJS_20)
.handler("index.handler") // default
.memory(256) // MB, default 128
.timeout(30) // seconds, default 30
.env({ LOG_LEVEL: "info" })
An IAM execution role (puls-lambda-{name}-role) is created automatically if you don't supply one via .role(arn).
Constants
RUNTIME.NODEJS_20 // nodejs20.x
RUNTIME.NODEJS_18 // nodejs18.x
RUNTIME.PYTHON_3_12 // python3.12
RUNTIME.JAVA_21 // java21
RUNTIME.DOTNET_8 // dotnet8
API Gateway
HTTP API (v2) routing to Lambda functions.
AWS.APIGateway("my-api")
.route("GET /users", this.listUsers)
.route("POST /users", this.createUser)
// Single-function proxy - forwards all traffic to one Lambda
AWS.APIGateway("my-api").proxy(this.handler)
Outputs the live endpoint URL on deploy. Uses $default stage with auto-deploy - no manual deployment step needed. Lambda invoke permissions are granted automatically and idempotently.
ECS / Fargate
Containerized services without instance management.
AWS.Fargate("my-service")
.image("nginx:latest") // required
.cpu(256) // vCPU units, default 256
.memory(512) // MB, default 512
.port(80)
.replicas(2) // default 1
.env({ NODE_ENV: "production" })
.cluster("my-cluster") // default: "puls"
What it manages automatically:
- ECS cluster (
pulsby default) - created if absent - IAM task execution role with
AmazonECSTaskExecutionRolePolicy - CloudWatch log group
/puls/{name} - Default VPC + subnets (or override with
.subnets(ids[])) - Security group with port open (or override with
.securityGroups(ids[]))
RDS
Managed database instances.
AWS.RDS("my-db")
.engine(DB.POSTGRES_16)
.size(DB_SIZE.SMALL)
.storage(20) // GB, default 20
.database("appdb") // initial DB name
.credentials("admin", process.env.DB_PASSWORD!)
What it manages automatically:
- DB subnet group across all default VPC subnets
- Security group with DB port open to VPC CIDR only (use
.publicAccess()to open to the internet)
Waits for available status after creation (up to 20 minutes, polls every 30 seconds). Outputs the endpoint and port.
Constants
DB.POSTGRES_16 // { engine: 'postgres', version: '16' }
DB.POSTGRES_15
DB.MYSQL_8
DB.MARIADB_11
DB_SIZE.MICRO // db.t3.micro - free tier eligible
DB_SIZE.SMALL // db.t3.small
DB_SIZE.MEDIUM // db.t3.medium
DB_SIZE.LARGE // db.r6g.large
SQS
Standard and FIFO queues with optional dead-letter queue.
// Standard queue
AWS.SQS("job-queue")
.retention(7) // days, default 4
.timeout(60) // visibility timeout in seconds, default 30
.delay(0) // delivery delay in seconds, default 0
.dlq("job-queue-dlq", 3) // DLQ name + max receives before redirect
// FIFO queue - .fifo suffix appended automatically
AWS.SQS("order-events")
.fifo()
.deduplication()
.dlq() creates the dead-letter queue first and wires the redrive policy. resolvedDlqUrl and resolvedDlqArn are available on the builder after deploy.
S3
AWS.S3("my-bucket")
.allowFrom(this.cdn, this.game) // adds CloudFront OAC policy for each distro
.region(REGION.EU_WEST_1) // bucket in non-default region
.upload("./dist/checksums.json") // upload a single file on deploy
.allowFrom() merges CloudFront ARNs into the bucket policy without overwriting other statements.
CloudFront
Clone an existing distribution and attach it to a domain.
AWS.CloudFront("my-distro-name")
.copyFrom(DISTRO.CDN) // clone config from existing distribution ID
.forDomain(this.domain, ["ec", "nc"]) // CNAMEs: ec.zoneName, nc.zoneName
.invalidate(["/index.html", "/api/*"]) // invalidate paths after deploy
.forDomain() wires the ACM cert from the Route53 sidecar automatically. Retries cert propagation for up to 5 minutes.
Route53
Hosted zone discovery, DNS record management, and domain registration.
AWS.Route53("example.com") // existing hosted zone
AWS.Route53("example.com").withWildcardSSL() // attach wildcard ACM cert (sidecar)
AWS.Route53().randomDomain().register(DOMAIN_REGISTER) // register a new random .com
Adding records
AWS.Route53("example.com")
.record("@", "A", "203.0.113.10")
.record("www", "CNAME", "example.com")
.record("@", "MX", "10 mail.example.com") // MX: "priority hostname"
.record("@", "TXT", "v=spf1 include:_spf.google.com ~all") // quotes added automatically
.record("mail", "AAAA", "2001:db8::1")
.record("_dmarc", "TXT", "v=DMARC1; p=reject", 600) // optional custom TTL (default 300)
Supported types: A, AAAA, CNAME, MX, TXT, NS, PTR, SRV, CAA, NAPTR, SPF
TXT and SPF values are automatically wrapped in double quotes if not already quoted.
Alias pointer
Use .pointer() to point a name at another Puls-managed resource (e.g. a Fargate service or load balancer):
Domain registration
import { AWS_TYPES } from "puls-dev";
const { REGION } = AWS_TYPES;
const DOMAIN_REGISTER = {
FIRSTNAME: "Jane",
LASTNAME: "Doe",
EMAIL: "jane@example.com",
MOBILE: "+1.5555550100",
CONTACT_TYPE: "PERSON",
ADDRESSLINE: "123 Main St",
CITY: "Seattle",
ZIPCODE: "98101",
COUNTRY: "US",
};
AWS.Route53("example.com").register(DOMAIN_REGISTER)
AWS.Route53().randomDomain().register(DOMAIN_REGISTER) // registers a random available .com
Registration polls every 15 seconds and waits up to 15 minutes for the domain to become active. Privacy protection is enabled by default for admin, registrant, and tech contacts.
Outputs
| Field | Type |
|---|---|
.out.zone |
Output<{ name: string; id: string }> |
ACM
Managed automatically as a Route53 sidecar - not used directly.
.withWildcardSSL() on a Route53 builder requests a wildcard cert, writes the DNS validation CNAME, and waits for ISSUED (up to 10 minutes).
SecretsManager
Read and manage AWS Secrets Manager secrets.
// Read an existing secret (value available at deploy time via .resolvedValue)
AWS.SecretsManager("my-app/db-password")
// Create or update a secret
AWS.SecretsManager("my-app/db-password")
.plainText("s3cr3t")
.description("Production DB password")
// Store a JSON object (key-value secret)
AWS.SecretsManager("my-app/config")
.keyValue({ host: "db.internal", port: 5432 })
Using secrets as env vars
Pass a SecretsBuilder directly into .env() on Lambda or Fargate - Puls resolves the secret value at deploy time and injects it as a plain string:
const dbPass = AWS.SecretsManager("my-app/db-password");
AWS.Lambda("my-fn")
.code("./dist")
.runtime(RUNTIME.NODEJS_20)
.env({ DB_PASSWORD: dbPass })
Destroy behaviour
By default, @Destroy schedules a 30-day recovery window. Use .forceDelete() to delete immediately:
Full example
import "dotenv/config";
import "reflect-metadata";
import { AWS, AWS_TYPES, Stack, Deploy } from "puls-dev";
const { REGION, RUNTIME, DB, DB_SIZE } = AWS_TYPES;
// import { SECRETS } from "./types/secrets.ts"; // your local constants
@Deploy({ region: REGION.EU_CENTRAL_1, dryRun: false })
class AppStack extends Stack {
// secret = AWS.SecretsManager(SECRETS.DB_KEY);
db = AWS.RDS("app-db")
.engine(DB.POSTGRES_16)
.size(DB_SIZE.SMALL)
.credentials(this.secret);
api = AWS.Fargate("app-api")
.image("my-org/app:latest")
.cpu(512).memory(1024)
.port(3000)
.replicas(2);
resizer = AWS.Lambda("image-resizer")
.code("./functions/resizer")
.runtime(RUNTIME.NODEJS_20)
.memory(512);
gateway = AWS.APIGateway("app-gateway")
.route("GET /resize", this.resizer);
jobs = AWS.SQS("resize-jobs")
.retention(7)
.dlq("resize-jobs-dlq", 3);
}