Zero-dependency cloud infrastructure as TypeScript. A driver-based system — deploy production-ready infrastructure to AWS or Hetzner with the same config, no cloud SDK or CLI required.
ts-cloud is a modern infrastructure-as-code framework that lets you define cloud infrastructure using TypeScript configuration files. Your config is provider-agnostic; a pluggable cloud driver translates it into provider-native API calls. Unlike AWS CDK or Terraform, ts-cloud:
- Driver-Based Architecture - One
CloudDriverinterface, swappable backends. AWS and Hetzner drivers ship in the box. - Zero Cloud Dependencies - No AWS SDK, no Hetzner SDK, no CLIs. Direct, signed HTTPS API calls only.
- Type-Safe Configuration - Full TypeScript types for all resources
- Production-Ready Presets - 13 battle-tested infrastructure templates
- Bun-Powered - Lightning-fast builds and deployments
- CloudFormation Native - The AWS driver generates clean, reviewable CloudFormation templates
Every provision, deploy, and teardown flows through the same CloudDriver interface — the CLI and your config never talk to a provider directly:
cloud.config.ts
│
▼
resolveCloudProvider(config) createCloudDriver({ config })
cloud.provider, or auto-detected ───────────────►┌──────────────────────────┐
from hetzner credentials │ CloudDriver │
│ │
│ provisionCompute / │
│ getComputeOutputs / │
│ uploadRelease / │
│ runRemoteDeploy / │
│ destroyCompute │
└────────────┬─────────────┘
│
┌──────────────────────┬───────────────────┴──────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌─────────────┐
│ AwsDriver │ │ HetznerDriver│ │LocalBoxDriver│
└────────────┘ └──────────────┘ └─────────────┘
| Driver | Compute | Deploys via | Release staging | Infrastructure model |
|---|---|---|---|---|
aws |
EC2 (CloudFormation stacks or a lightweight single-box boot) | SSM AWS-RunShellScript |
S3 deploy bucket | Clean, reviewable CloudFormation templates |
hetzner |
Hetzner Cloud servers, cloud firewalls, private networks, native load balancers | SSH + cloud-init bootstrap | /var/ts-cloud/staging on the box |
Direct Hetzner Cloud API calls |
local-box |
The machine ts-cloud runs on | Local shell | — | On-box management dashboard (box mode) |
New providers implement the same interface (packages/core/src/drivers/types.ts) and register with the factory — DNS stays provider-agnostic via the separate DnsProvider abstraction (Cloudflare, Porkbun, GoDaddy, Route53).
// cloud.config.ts
export default {
project: { name: 'My App', slug: 'my-app', region: 'us-east-1' },
cloud: {
provider: 'hetzner', // 'aws' (default) | 'hetzner'
},
// ...
}Resolution order: an explicit cloud.provider wins; otherwise a hetzner.apiToken in the config file selects the Hetzner driver; with neither, the AWS driver is used (backward compatible). The token itself is supplied at deploy time — HCLOUD_TOKEN / HETZNER_API_TOKEN in the environment, or hetzner.apiToken in the config.
Set the provider, supply a token, deploy:
export HCLOUD_TOKEN="your-hetzner-cloud-api-token"
cloud deploy// cloud.config.ts
export default {
// ...
cloud: { provider: 'hetzner' },
}The Hetzner driver boots an Ubuntu 24.04 server behind a cloud firewall, registers your deploy SSH key, and provisions the stack over cloud-init (nginx, PHP-FPM, bun/node/deno runtimes, databases, caches — installed via pantry). Fleets (a private network, dedicated services box, N app servers, and a load balancer) are first-class too. Server state is pinned in storage/cloud/state/<stack>.json — commit it, and CI reuses the same box instead of provisioning a new one.
Optional config (all env-overridable):
| Config | Env | Default |
|---|---|---|
hetzner.apiToken |
HCLOUD_TOKEN / HETZNER_API_TOKEN |
— (required) |
hetzner.location |
HCLOUD_LOCATION |
fsn1 |
hetzner.image |
HCLOUD_IMAGE |
ubuntu-24.04 |
hetzner.sshPrivateKeyPath |
HCLOUD_SSH_KEY |
~/.ssh/id_ed25519 |
hetzner.sshUser |
HCLOUD_SSH_USER |
root |
AWS is the default driver — just standard credentials:
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-east-1"
cloud deployFull infrastructure (VPC, ALB, RDS, ElastiCache, CloudFront, …) is generated as CloudFormation; the lightweight Forge-style path boots a single tagged EC2 box and deploys over SSM with releases staged in S3.
import { createCloudDriver } from 'ts-cloud'
const driver = createCloudDriver({ config }) // AwsDriver | HetznerDriver (LocalBoxDriver in box mode)
const outputs = await driver.getComputeOutputs({ config, environment: 'production' })
// outputs.appPublicIp, outputs.appInstanceId, outputs.deployStoragePath, ...One config, two production drivers today:
- AWS - CloudFormation-generated stacks or a lightweight single-EC2 boot, SSM-based deploys, S3 release staging
- Hetzner - Cloud servers + firewalls + private networks + load balancers over the Hetzner Cloud API, SSH deploys, cloud-init bootstrap
See Cloud Drivers for the architecture and per-provider setup.
Skip the boilerplate with production-ready presets for common architectures:
- Static Sites - S3 + CloudFront for SPAs and static websites
- Node.js Servers - EC2 + ALB + RDS + Redis for traditional apps
- Serverless Apps - ECS Fargate + ALB + DynamoDB for scalable services
- Full-Stack Apps - Complete frontend + backend + database stack
- API Backends - API Gateway + Lambda + DynamoDB for serverless APIs
- WordPress - Optimized WordPress hosting with RDS + EFS + CloudFront
- JAMstack - Modern static sites with Lambda@Edge for SSR
- Microservices - Multi-service architecture with service discovery
- Real-time Apps - WebSocket API + Lambda + DynamoDB Streams
- Data Pipelines - Kinesis + Lambda + S3 + Athena + Glue for ETL
- ML APIs - SageMaker + API Gateway for ML inference
- Traditional Web Apps - Session-based apps with EFS + Redis + ALB
Complete CloudFormation template builders for:
- Network - VPC, subnets, NAT gateways, routing, security groups
- Storage - S3 buckets with versioning, encryption, lifecycle rules, EFS
- Compute - EC2 Auto Scaling, ECS Fargate, Lambda functions
- Database - RDS (PostgreSQL/MySQL), DynamoDB with streams and GSIs
- Cache - ElastiCache Redis/Memcached with replication
- CDN - CloudFront distributions with custom domains and Lambda@Edge
- API Gateway - HTTP, REST, and WebSocket APIs
- Queue - SQS queues with dead letter queues
- Messaging - SNS topics and subscriptions
- Monitoring - CloudWatch dashboards, alarms, and log groups
- Security - ACM certificates, WAF rules, security groups
Persistent security findings and policy gates prevent unsafe releases:
- 35+ Secret Patterns - AWS keys, API tokens, private keys, database credentials
- Container Supply Chain - Local Trivy scanning, CycloneDX SBOMs, vulnerability summaries, and SLSA provenance
- Environment Policies - Block, warn, or record by severity and scanner health
- Auditable Remediation - Assignment, comments, expiring waivers, recurrence, and decision history
- Posture Center - Responsive dashboard for findings, scanner health, policy editing, review, and export
# Scan for secrets before deploying
cloud deploy:security-scan --source ./dist
# Deploy with automatic security scanning
cloud deploy # Scans automatically before deploymentSee the security posture center documentation for policy, scanner, waiver, and release-artifact behavior.
No SDK, no CLI - pure signed HTTPS API calls against the providers themselves:
- AWS - Signature V4 calls: CloudFormation (CreateStack, UpdateStack, DeleteStack, DescribeStacks), S3 (PutObject, multipart upload, sync directory), CloudFront (cache invalidations with wait support). Credentials resolve from env vars, ~/.aws/credentials, or IAM roles
- Hetzner - Bearer-token Cloud API client with pagination, action polling, firewalls, private networks, load balancers, and SSH key management. Token resolves from
HCLOUD_TOKEN/HETZNER_API_TOKENorhetzner.apiToken
bun add ts-cloudCreate a cloud.config.ts:
import { createStaticSitePreset } from 'ts-cloud/presets'
export default createStaticSitePreset({
name: 'My Website',
slug: 'my-website',
domain: 'example.com',
})Deploy:
bun run cloud deployThat's it! You now have:
- ✅ S3 bucket with static website hosting
- ✅ CloudFront CDN with HTTPS
- ✅ Route53 DNS configuration
- ✅ ACM SSL certificate
The same config deploys to Hetzner by setting cloud.provider: 'hetzner' and exporting HCLOUD_TOKEN — see Cloud Drivers.
import { createFullStackAppPreset } from 'ts-cloud/presets'
export default createFullStackAppPreset({
name: 'My App',
slug: 'my-app',
domain: 'app.example.com',
apiSubdomain: 'api.example.com',
})Includes:
- Frontend: S3 + CloudFront
- Backend: ECS Fargate with auto-scaling
- Database: PostgreSQL RDS with Multi-AZ
- Cache: Redis ElastiCache
- Queue: SQS for background jobs
import { createApiBackendPreset } from 'ts-cloud/presets'
export default createApiBackendPreset({
name: 'My API',
slug: 'my-api',
domain: 'api.example.com',
})Includes:
- API Gateway HTTP API
- Lambda functions with auto-scaling
- DynamoDB tables with on-demand billing
- CloudWatch monitoring and alarms
You can extend any preset with custom configuration:
import { createNodeJsServerPreset, extendPreset } from 'ts-cloud/presets'
export default extendPreset(
createNodeJsServerPreset({
name: 'My App',
slug: 'my-app',
}),
{
infrastructure: {
compute: {
server: {
instanceType: 't3.large', // Upgrade instance type
autoScaling: {
max: 20, // Increase max instances
},
},
},
},
}
)Combine multiple presets:
import { composePresets, createStaticSitePreset, createApiBackendPreset } from 'ts-cloud/presets'
export default composePresets(
createStaticSitePreset({ name: 'Frontend', slug: 'frontend', domain: 'example.com' }),
createApiBackendPreset({ name: 'Backend', slug: 'backend' }),
{
// Custom overrides
infrastructure: {
monitoring: {
alarms: [{ metric: 'Errors', threshold: 10 }],
},
},
}
)Generate templates programmatically:
import { CloudFormationBuilder } from 'ts-cloud/cloudformation'
const builder = new CloudFormationBuilder(config)
const template = builder.build()
console.log(JSON.stringify(template, null, 2))Use the AWS clients directly:
import { CloudFormationClient, S3Client, CloudFrontClient } from 'ts-cloud/aws'
// CloudFormation
const cfn = new CloudFormationClient('us-east-1')
await cfn.createStack({
stackName: 'my-stack',
templateBody: JSON.stringify(template),
})
// S3
const s3 = new S3Client('us-east-1')
await s3.putObject({
bucket: 'my-bucket',
key: 'file.txt',
body: 'Hello World',
})
// CloudFront
const cloudfront = new CloudFrontClient()
await cloudfront.createInvalidation({
distributionId: 'E1234567890',
paths: ['/*'],
})The Hetzner driver's Cloud API client is exported too:
import { HetznerClient } from 'ts-cloud'
const hcloud = new HetznerClient({ apiToken: process.env.HCLOUD_TOKEN! })
const servers = await hcloud.listServers() // fully paginated
const { firewall } = await hcloud.createFirewall({
name: 'web-fw',
rules: [{ direction: 'in', protocol: 'tcp', port: '443', source_ips: ['0.0.0.0/0', '::/0'] }],
})ts-cloud speaks the S3 API for AWS S3, Backblaze B2 and Hetzner Object Storage, so you can move a service's data off AWS (or between any two S3-compatible buckets) with a single command. Bytes are copied (not strings), so binary payloads — images, archives, mail attachments — survive intact, and Content-Type is preserved when the source reports it. The copy is idempotent (objects already present at the destination with the same size are skipped) and can verify itself afterwards.
cloud migrate:storage \
--from aws:stacks-production-email \
--to hetzner:stacks-mail \
--include mailboxes/,inbox/,incoming/,sent/,trash/,drafts/,junk/,archive/,flags/,uids/,sms/ \
--exclude mail-server,deploy/,_deploy/,imap-server/ \
--verifyThis copies only the mail data prefixes, deliberately leaving the server
binaries and deploy artifacts behind (they show up in the report's EXCLUDED
list so you can confirm nothing was missed), then re-lists the destination and
asserts the object count and sizes match.
--from <provider:bucket>/--to <provider:bucket>— provider isaws,hetznerorbackblaze(e.g.aws:my-bucket).--from-region/--to-region,--from-endpoint/--to-endpoint— optional; default to the provider's standard endpoint.--from-prefix/--to-prefix— key prefix on each side. The source prefix is stripped and the dest prefix prepended, so you can remap (email/inbox/a.eml→mail/inbox/a.eml).--include <csv>/--exclude <csv>— only copy / skip keys under these comma-separated prefixes (excludealways wins).--dry-run— print the plan (what would copy / what is excluded) without writing.--force— re-copy even if the destination already has an object of the same size.--delete-extraneous— delete destination keys not present in the source (default OFF).--concurrency <n>— max concurrent copies (default 8).--verify— after copying, re-list the destination and assert object count + sizes match the copied set.
Set the credentials for both sides — the migrator resolves each side independently using the object-storage env conventions:
| Provider | Access key env | Secret key env | Region env |
|---|---|---|---|
aws |
AWS_ACCESS_KEY_ID (or S3_ACCESS_KEY_ID) |
AWS_SECRET_ACCESS_KEY (or S3_SECRET_ACCESS_KEY) |
AWS_REGION |
hetzner |
HETZNER_S3_ACCESS_KEY (falls back to S3_ACCESS_KEY_ID/AWS_ACCESS_KEY_ID) |
HETZNER_S3_SECRET_KEY (falls back to S3_SECRET_ACCESS_KEY/AWS_SECRET_ACCESS_KEY) |
HETZNER_S3_REGION |
backblaze |
B2_APPLICATION_KEY_ID (falls back to S3_ACCESS_KEY_ID/AWS_ACCESS_KEY_ID) |
B2_APPLICATION_KEY (falls back to S3_SECRET_ACCESS_KEY/AWS_SECRET_ACCESS_KEY) |
B2_REGION |
When both sides use the same credentials (e.g. one Hetzner project), the generic
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY pair covers them.
The same migration is available as a library function (usable from scripts or stacks buddy):
import { migrateObjectStorage } from 'ts-cloud'
const result = await migrateObjectStorage({
from: { provider: 'aws', bucket: 'stacks-production-email' },
to: { provider: 'hetzner', bucket: 'stacks-mail' },
include: ['mailboxes/', 'inbox/', 'sent/'],
exclude: ['mail-server', 'deploy/'],
verify: true,
})
// result: { copied, skipped, excluded, bytesCopied, errors, excludedKeys, deleted, verification }ts-cloud supports multiple DNS providers for domain management and SSL certificate validation:
- Log in to your Cloudflare Dashboard
- Go to My Profile→API Tokens (or visit https://dash.cloudflare.com/profile/api-tokens)
- Click Create Token
- Use the Edit zone DNS template, or create a custom token with:
- Permissions: Zone → DNS → Edit
- Zone Resources: Include → All zones (or specific zones)
- Copy the generated token
export CLOUDFLARE_API_TOKEN="your-api-token-here"- Log in to your Porkbun Dashboard
- Enable API access for your domain(s)
- Generate an API key pair
export PORKBUN_API_KEY="your-api-key"
export PORKBUN_SECRET_KEY="your-secret-key"- Log in to GoDaddy Developer Portal
- Create a new API key
- Note both the key and secret
export GODADDY_API_KEY="your-api-key"
export GODADDY_API_SECRET="your-api-secret"
export GODADDY_ENVIRONMENT="production" # or "ote" for testingUses AWS credentials from environment or ~/.aws/credentials:
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-east-1"
export AWS_HOSTED_ZONE_ID="Z1234567890" # Optional# List domains
cloud domain:list --provider cloudflare
# List DNS records
cloud dns:records example.com --provider cloudflare
# Add a DNS record
cloud dns:add example.com A 192.168.1.1 --name api --provider cloudflare
# Generate SSL certificate with DNS validation
cloud domain:ssl example.com --provider cloudflare# Install dependencies
bun install
# Run tests
bun test
# Build
bun run build
# Type check
bun run typecheck- Configuration - Define infrastructure in TypeScript (provider-agnostic)
- Driver Resolution -
resolveCloudProviderpicksawsorhetznerfrom your config;createCloudDriverbuilds the matching driver (cached per project) - Template / API Translation - The AWS driver generates CloudFormation templates; the Hetzner driver composes Cloud API calls + cloud-init bootstraps
- Deployment - Create/update stacks with change sets (AWS), or provision servers/firewalls/networks directly (Hetzner)
- Release Deploys - Releases upload to provider staging (S3 / on-box) and roll out over SSM or SSH with zero-downtime cutovers
- Monitoring - Track deployment progress with real-time events
ts-cloud uses zero external dependencies for cloud operations:
- AWS Signature V4 - Manual request signing for authentication
- Direct HTTPS - Native
fetch()for AWS and Hetzner Cloud API calls - Credentials - Parse ~/.aws/credentials without SDK; bearer tokens for Hetzner
- CloudFormation - XML/JSON parsing for responses
This means:
- ⚡ Faster startup and execution
- 📦 Smaller bundle size
- 🔒 Better security (no supply chain attacks)
- 🎯 Full control over provider interactions
Please see CONTRIBUTING for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
"Software that is free, but hopes for a postcard." We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙
