Skip to content

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:

puls init aws
# or:
puls init proxmox

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 to plan and deploy.
  • 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!