A TypeScript-first Infrastructure as Code (IaC) framework built as a pnpm monorepo. Define your infrastructure in JSON, run it from the CLI. One engine, pluggable providers, sequential stacks with context passing between steps.
- How It Works
- Installation
- Repository Structure
- Packages
- Getting Started (Monorepo Development)
- Local Testing
- Configs
- Stacks
- Context Passing Between Steps
- Adding a New Resource to an Existing Provider
- Adding a New Provider
- Publishing
- Environment Variables
OpenIaC has three layers:
JSON Config / Stack ← the only thing end users touch
↓
Engine (@openiac/core) ← reads the JSON, routes to the right provider + resource + action
↓
Provider Package ← calls the actual API (Stripe, AWS, etc.)
A single config file describes what to do:
{
"$docs": "https://docs.stripe.com/api/products/create",
"provider": "stripe",
"resource": "product",
"action": "create",
"params": {
"name": "Standard Widget",
"description": "A one-time purchase widget"
}
}Run it:
iac configs/stripe/product.create.jsonThat's it. The engine loads @openiac/provider-stripe, finds the product resource, calls the create action with your params, and prints the result.
Install @openiac/all globally to get the iac CLI and all providers at once:
npm install -g @openiac/allThen run configs from anywhere:
iac path/to/config.jsonIf you only need specific providers, install @openiac/core alongside the ones you want:
npm install -g @openiac/core @openiac/provider-stripeAdd @openiac/all as a dev dependency to get the CLI and all providers:
npm install -D @openiac/allOr install only the core and the providers you need:
npm install -D @openiac/core @openiac/provider-stripeThen run it via npx:
npx iac path/to/config.jsonOpenIaC/
├── .changeset/ # Versioning config (Changesets)
├── packages/
│ ├── core/ # @openiac/core — engine, CLI, shared types
│ │ ├── src/
│ │ │ ├── engine.ts # CLI entrypoint
│ │ │ ├── types.ts # Shared interfaces: Resource, Provider, Stack, Context
│ │ │ └── index.ts # Public type exports
│ │ └── dist/ # Compiled JS (generated by `pnpm build`)
│ ├── openiac/ # @openiac/all — meta-package that bundles core + all providers
│ └── providers/
│ └── stripe/ # @openiac/provider-stripe
│ ├── src/
│ │ ├── client.ts # Shared Stripe SDK singleton
│ │ ├── resources/
│ │ │ ├── product.ts # create, list, retrieve, delete
│ │ │ └── price.ts # create, list, retrieve, update
│ │ └── index.ts # Exports active resources
│ ├── dist/ # Compiled JS (generated by `pnpm build`)
│ ├── templates/ # Placeholder configs users copy into their project
│ │ ├── product.create.json
│ │ ├── price.one-time.create.json
│ │ ├── price.recurring.create.json
│ │ ├── product.one-time.stack.json
│ │ └── product.subscription.stack.json
│ └── iac_tests/ # Filled-out example configs for local testing
│ ├── product.one-time.stack.json
│ └── product.subscription.stack.json
├── .npmrc # pnpm settings
├── package.json # Monorepo root (private, not published)
├── pnpm-workspace.yaml # Declares packages/* and packages/providers/* as workspaces
├── tsconfig.base.json # Shared TypeScript config
└── turbo.json # Turborepo build pipeline
| Package | Version | Description |
|---|---|---|
@openiac/all |
0.2.0 |
Meta-package — installs the CLI and all providers |
@openiac/core |
0.2.0 |
Engine, CLI entrypoint (iac), and shared TypeScript types |
@openiac/provider-stripe |
0.2.0 |
Stripe API resources: product, price |
- Node.js >= 18
- pnpm >= 9
npm install -g pnpmgit clone https://github.com/samuelcuster/OpenIaC
cd OpenIaC
pnpm install
pnpm buildFor local development, you can skip the build and run TypeScript directly via the iac script (which uses tsx).
# Add a dependency to a specific package
pnpm add stripe --filter @openiac/provider-stripe
# Add a dev dependency to the monorepo root
pnpm add -Dw typescriptRun the engine locally from the monorepo root using the iac script:
# Set required env vars for the provider you're testing
export STRIPE_SECRET_KEY=sk_test_...
# Run a single config
pnpm iac packages/providers/stripe/iac_tests/product.one-time.stack.json
# Run any JSON config file
pnpm iac path/to/your/config.jsonA config file targets a single resource action:
{
"$docs": "https://docs.stripe.com/api/products/create",
"provider": "stripe",
"resource": "product",
"action": "create",
"params": {
"name": "Standard Widget",
"description": "A one-time purchase widget",
"metadata": {
"internal_product_id": "widget-001"
}
}
}Fields:
| Field | Required | Description |
|---|---|---|
$docs |
No | URL to the API documentation. Ignored by the engine, useful for reference. |
provider |
Yes | Which provider package to use (e.g. stripe → @openiac/provider-stripe) |
resource |
Yes | Which resource on the provider (e.g. product, price) |
action |
Yes | Which method to call (e.g. create, list, retrieve, delete) |
params |
Yes | The parameters passed directly to the action |
A stack is an ordered list of steps run sequentially. Each step is a config with an additional id field. Here's an example that creates a one-time purchase product with a price:
{
"name": "one-time-product",
"steps": [
{
"$docs": "https://docs.stripe.com/api/products/create",
"id": "create_product",
"provider": "stripe",
"resource": "product",
"action": "create",
"params": {
"name": "Standard Widget",
"description": "A one-time purchase widget",
"metadata": {
"internal_product_id": "widget-001"
}
}
},
{
"$docs": "https://docs.stripe.com/api/prices/create",
"id": "create_price",
"provider": "stripe",
"resource": "price",
"action": "create",
"params": {
"product": "$context.create_product.id",
"unit_amount": 1999,
"currency": "usd"
}
}
]
}The engine detects a stack automatically by the presence of a steps array — no special flag needed.
Each step's output is stored in a shared context object under its id. Subsequent steps can reference any field from a previous step's output using $context.<step_id>.<field>:
"product": "$context.create_product.id"This resolves to the id field returned by the create_product step at runtime.
Important: a $context reference only works if:
- The step
idmatches exactly (e.g.create_product) - The referenced field (e.g.
id) is actually returned by the executor
If the reference can't be resolved, it is left as the literal string $context.create_product.id and the API will return a validation error — so double-check both the step ID and the executor's return shape if you see unexpected failures.
- Create
packages/providers/stripe/src/resources/webhook.ts:
import type { Resource } from "@openiac/core";
import { stripe } from "../client.js";
export const webhook: Resource = {
async create(params: Record<string, unknown>) {
const result = await stripe.webhookEndpoints.create(
params as Parameters<typeof stripe.webhookEndpoints.create>[0]
);
return { id: result.id, url: result.url };
},
async list() {
const result = await stripe.webhookEndpoints.list();
return result.data.map((w) => ({ id: w.id, url: w.url }));
},
async delete(params: { id: string }) {
await stripe.webhookEndpoints.del(params.id);
return { id: params.id };
},
};- Export it from
packages/providers/stripe/src/index.ts:
export { webhook } from "./resources/webhook.js";- Add a template to
packages/providers/stripe/templates/webhook.create.json.
- Scaffold the package:
cp -r packages/providers/stripe packages/providers/github- Update
packages/providers/github/package.json:
{
"name": "@openiac/provider-github",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"@openiac/core": "workspace:*",
"@octokit/rest": "^20.0.0"
}
}-
Replace
src/client.tswith your SDK client. -
Add resources under
src/resources/— each must export an object conforming to theResourcetype from@openiac/core, with async methods likecreate,list,retrieve,delete. -
Export all resources from
src/index.ts. -
Add templates to
templates/. -
Add the new provider as a devDependency in the root
package.jsonso the engine can resolve it locally:
"@openiac/provider-github": "workspace:*"- Install from monorepo root:
pnpm installThis repo uses Changesets to version and publish packages independently. Each package has its own version — you can ship a new version of @openiac/provider-stripe without touching @openiac/core.
# 1. After making changes, create a changeset describing what changed
pnpm changeset
# Follow the prompts: select which packages changed, choose patch/minor/major, write a summary
# 2. Version the affected packages (updates package.json versions and CHANGELOG.md)
pnpm changeset version
# 3. Build and publish to npm
pnpm publish-allSet NPM_TOKEN as a secret in your GitHub repository settings, then uncomment this line in .npmrc:
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Environment variables are never loaded by OpenIaC itself. The engine reads directly from process.env, so variables must be present before the CLI runs. How you load them is up to you.
| Variable | Required By | Description |
|---|---|---|
STRIPE_SECRET_KEY |
@openiac/provider-stripe |
Your Stripe secret key (sk_live_... or sk_test_...) |
Loading options:
# Option 1: export directly in your terminal
export STRIPE_SECRET_KEY=sk_live_...
pnpm iac path/to/config.json
# Option 2: inline for a single run
STRIPE_SECRET_KEY=sk_live_... pnpm iac path/to/config.json
# Option 3: load your .env file first using dotenv-cli
npx dotenv -e .env -- pnpm iac path/to/config.json