Scaffolding Production Stacks with puls init
Starting a new Infrastructure-as-Code (IaC) repository can be intimidating. Often, developers are left staring at a blank terminal, wondering how to structure their directories, where to put environmental configurations, and how to link resources across different files.
To solve this, we have revamped the puls init command. Instead of dumping simple dummy files, puls init now bootstraps a production-ready, modular directory layout using a TypeScript-first configuration architecture.
The Philosophy: Separating Secrets from Constants
In traditional IaC setups, developers tend to dump every variable-image names, CPU cores, domain names, and regions-into a giant .env file. This makes configurations difficult to type-check, refactor, and reuse.
Puls shifts this paradigm:
* Secrets belong in .env: Credentials, access keys, and passwords should reside in a Git-ignored environment file.
* Architecture belongs in types.ts: Non-sensitive constants, configuration matrices, IP addresses, and VM specifications are declared as strongly-typed TypeScript objects.
How It Works: The Directory Blueprint
Running:
bootstraps the following structured workspace under your current working directory:
├── .env.example
├── .gitignore
├── package.json
├── tsconfig.json
└── infra/
└── my-app/
├── types.ts # Configuration constants & matrices
├── database-stack.ts # DB instances & S3 storage buckets
├── compute-stack.ts # EC2/VM instances & orchestration
└── network/
└── network-stack.ts # Route53/VLAN configurations
Deep Dive: The Generated Code
1. Typescript Configurations (types.ts)
The types.ts file acts as the configuration hub. Rather than parsing raw string values from process.env, you define constants directly:
// infra/my-app/types.ts
export const REGION = {
US_EAST_1: "us-east-1",
EU_WEST_1: "eu-west-1",
} as const;
export const INSTANCE_TYPE = {
SMALL: "t3.small",
MICRO: "t3.micro",
} as const;
export const BUCKET = {
ASSETS: "my-assets-replace-me",
SITE: "my-site-replace-me",
} as const;
export const DOMAIN_NAME = "example-replace-me.com";
2. Tier Isolation and Cross-Stack Linking
Stack files are cleanly separated by lifecycle tier, importing constants directly using ES relative module routes:
// infra/my-app/database-stack.ts
import { Stack, Deploy } from "@puls-dev/core";
import { AWS } from "@puls-dev/aws";
import { REGION, BUCKET } from "./types.js";
@Deploy({ dryRun: true })
export class DatabaseStack extends Stack {
assets = AWS.S3(BUCKET.ASSETS)
.region(REGION.US_EAST_1)
.versioning(true);
db = AWS.RDS("app-db")
.engine({ engine: "postgres", version: "16" })
.size("db.t3.micro")
.storage(20);
}
Compute resources can then link directly to database parameters without violating tier boundaries:
// infra/my-app/compute-stack.ts
import { Stack, Deploy } from "@puls-dev/core";
import { AWS } from "@puls-dev/aws";
import { DatabaseStack } from "./database-stack.js";
import { INSTANCE_TYPE } from "./types.js";
@Deploy({ dryRun: true })
export class ComputeStack extends Stack {
dbStack = Stack.from(DatabaseStack);
webServer = AWS.EC2("web-server")
.instanceType(INSTANCE_TYPE.SMALL)
.ami("ami-xxxxxx-replace-me")
.keyName("my-keypair-replace-me")
.provision("./playbooks/setup.yaml");
}
Automated Project Setup
To ensure you can run your new code right away, puls init also generates your project metadata configurations:
package.json: Pre-configured with the required provider dependencies (e.g.@puls-dev/aws),"type": "module"configuration, and quick scripts toplananddeploy.tsconfig.json: Pre-configured with standard NodeNext target and module resolution configurations to compile ES Modules flawlessly.
Get started today by running puls init <provider> in your terminal, run npm install, and you are ready to write type-safe infrastructure code!
Happy scaffolding!