Infrastructure
Provision cloud infrastructure with Alchemy and the infra module system
Stackpanel includes a modular infrastructure provisioning system built on Alchemy. It provisions AWS resources (IAM roles, KMS keys, OIDC providers), databases (Neon), caches (Upstash), deployment targets (Cloudflare Workers), and machine inventories -- all declared in Nix and deployed with a single command.
How It Works
The infra system has three parts:
- Nix modules declare what infrastructure you need (
.stack/config.nix) - Code generation produces TypeScript modules in
packages/infra/with typed inputs - Alchemy runtime provisions the actual cloud resources when you run
infra:deploy
Infrastructure outputs (ARNs, URLs, keys) are synced back to your storage backend (SOPS, SSM, or Chamber) so other parts of your stack can reference them.
Quick Start
Enable infrastructure
Add to .stack/config.nix:
stackpanel.infra = {
enable = true;
# Where to store provisioned outputs
storage-backend = {
type = "sops";
sops.group = "dev";
};
};Enable modules
Turn on the infrastructure modules you need:
# AWS IAM + KMS for secrets
stackpanel.infra.aws.secrets = {
enable = true;
oidc.provider = "github-actions";
oidc.github-actions = {
org = "my-org";
repo = "my-repo";
};
};
# Database (Neon Postgres)
stackpanel.infra.database = {
enable = true;
provider = "neon";
neon.region = "aws-us-east-1";
};Reload and deploy
# Reload to pick up config changes
exit && nix develop --impure
# Deploy all infrastructure
infra:deployAvailable Modules
| Module | What it provisions | Key options |
|---|---|---|
| aws-secrets | IAM role, OIDC provider, KMS key + alias | oidc.provider, iam.role-name, kms.alias |
| deployment | Cloudflare app hosting per app | Per-app deployment.host = "cloudflare" (or defaultHost) |
| database | Neon Postgres or devenv/Docker Postgres | provider (neon/devenv/docker) |
| cache | Upstash Redis or devenv/Docker Redis | provider (upstash/devenv/docker) |
| machines | Machine inventory (static or AWS EC2) | source (static/aws-ec2) |
Deploying Infrastructure
Basic Deploy
# Deploy all modules
infra:deployThis runs alchemy deploy in the generated packages/infra/ directory. Alchemy evaluates each module, creates or updates resources, and writes state to its .alchemy/ directory.
Adopting Existing Resources
If your AWS resources already exist (e.g., you created an IAM role manually), use the --adopt flag to bring them under Alchemy management without recreating them:
infra:deploy --adoptThis tells Alchemy to adopt existing resources instead of failing with "already exists" errors. The infra modules' custom resources (IAM roles, KMS keys) are built to handle adoption gracefully -- they detect existing resources and update them in place.
The --adopt flag is an Alchemy CLI option, not a per-resource setting. However, the Stackpanel infra modules are written to be adopt-safe by default -- IAM roles use get-or-create logic, and KMS keys look up existing aliases before creating new keys.
Stage Selection
Deploy to a specific stage (environment):
infra:deploy --stage production
infra:deploy --stage stagingWithout --stage, Alchemy defaults to your username as the stage name. This lets each developer have isolated infrastructure for testing.
Destroying Infrastructure
# Remove all provisioned resources
infra:destroy
# Remove a specific stage
infra:destroy --stage staginginfra:destroy removes cloud resources permanently. Adopted resources (IAM roles, KMS keys) are not deleted -- they're only unlinked from Alchemy state.
Storage Backends
Infrastructure outputs (ARNs, URLs, connection strings) need to be stored somewhere accessible to your apps and CI. Stackpanel supports three backends:
SOPS (Recommended for teams)
Outputs are written to a SOPS-encrypted YAML file in your repo:
stackpanel.infra.storage-backend = {
type = "sops";
sops.group = "dev"; # implementation-specific SOPS target group
};After infra:deploy, outputs like aws-secrets-roleArn appear in the configured SOPS file. They're decrypted at runtime using your configured SOPS recipients.
AWS SSM Parameter Store
Outputs are written to SSM parameters:
stackpanel.infra.storage-backend = {
type = "ssm";
ssm.prefix = "/myapp/infra";
};Chamber
Outputs are written via Chamber:
stackpanel.infra.storage-backend = {
type = "chamber";
chamber.service = "myapp";
};None
Skip output sync entirely (outputs are only in Alchemy state):
stackpanel.infra.storage-backend.type = "none";Studio UI: Infrastructure Panel
The Studio UI's Infrastructure panel (/studio/infra) provides a visual interface for managing your provisioned infrastructure. It has five tabs:
Status Tab
Shows an overview of your infrastructure configuration:
- SST project name and region
- OIDC provider type
- IAM role name and ARN
- KMS key status
- Config file path
Deploy Tab
Deploy or destroy infrastructure from the UI:
- Stage selector (dev, staging, production)
- Deploy button (runs
sst deploy --stage <stage>) - Remove button (runs
sst remove --stage <stage>) - Live command output
Outputs Tab
Displays all provisioned infrastructure outputs:
- ARNs, URLs, and connection strings
- Copy-to-clipboard for each value
- Output source module
Resources Tab
Lists all deployed AWS resources by type and URN.
Configure Tab
Full configuration form for SST/infrastructure settings:
- Enable toggle
- Project name, region, account ID
- KMS key settings
- OIDC provider selection (GitHub Actions, Fly.io, Roles Anywhere)
- IAM role name
AWS Secrets Module
The aws-secrets module is the most commonly used infra module. It provisions:
- IAM Role -- For CI/CD and local AWS access
- OIDC Provider -- For passwordless authentication from GitHub Actions or Fly.io
- KMS Key + Alias -- For encrypting secrets at rest
- KMS Access Policy -- Grants the IAM role encrypt/decrypt permissions
stackpanel.infra.aws.secrets = {
enable = true;
region = "us-west-2";
iam = {
role-name = "myapp-secrets-role";
additional-policies = [
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
];
};
kms = {
alias = "myapp-secrets";
deletion-window-days = 30;
};
oidc = {
provider = "github-actions";
github-actions = {
org = "my-org";
repo = "my-repo";
branch = "*";
};
};
};OIDC Providers
| Provider | Use case | Authentication flow |
|---|---|---|
github-actions | CI/CD from GitHub | GitHub OIDC token -> STS AssumeRoleWithWebIdentity |
flyio | Fly.io Machines | Fly OIDC token -> STS AssumeRoleWithWebIdentity |
roles-anywhere | Local dev with Step CA | mTLS certificate -> IAM Roles Anywhere |
Deployment Module
The infra deployment module provisions hosting resources for apps targeting Cloudflare.
stackpanel.apps.web = {
port = 0;
framework.tanstack-start.enable = true;
deployment = {
enable = true;
host = "cloudflare";
bindings = [ "DATABASE_URL" "BETTER_AUTH_SECRET" ];
secrets = [ "DATABASE_URL" "BETTER_AUTH_SECRET" ];
};
};Host resolution uses app config first, then the global default:
apps.<name>.deployment.host- fallback:
stackpanel.deployment.defaultHost
Supported framework/host combinations in the infra deployment module:
| Framework | Cloudflare resource |
|---|---|
tanstack-start | TanStackStart |
nextjs | Nextjs |
vite | cloudflare.Vite |
hono | cloudflare.Worker |
astro | Astro |
remix | Remix |
nuxt | Nuxt |
Fly deployment is handled separately by the Fly deployment module (flyctl + generated fly.toml).
Generated Files
The infra system generates a complete TypeScript package at packages/infra/:
packages/infra/
alchemy.run.ts # Orchestrator (imports all modules, syncs outputs)
package.json # Dependencies from all active modules
tsconfig.json # TypeScript config
src/
index.ts # Infra class (input resolution, output sync)
types.ts # Per-module input TypeScript interfaces
resources/
iam-role.ts # Custom adopt-safe IAM Role resource
kms-key.ts # Custom adopt-safe KMS Key resource
kms-alias.ts # Custom KMS Alias resource
modules/
aws-secrets/
index.ts # AWS secrets provisioning
policies.ts # IAM policy builders
deployment.ts # App deployment to Cloudflare
database.ts # Database provisioningThese files are regenerated on each devshell entry. The alchemy.run.ts orchestrator imports all active modules and runs Infra.syncAll() to write outputs to your storage backend.
Pulling Outputs
After deploying, pull outputs from your storage backend into the local Nix state:
infra:pull-outputsThis reads outputs from Chamber/SSM and writes them to .stack/data/infra-outputs.nix, making them available as config.stackpanel.infra.outputs.* in your Nix config.
CLI Reference
| Command | Description |
|---|---|
infra:deploy | Deploy all infrastructure modules |
infra:deploy --adopt | Deploy, adopting existing resources |
infra:deploy --stage <name> | Deploy to a specific stage |
infra:destroy | Remove all provisioned resources |
infra:dev | Start infrastructure dev mode |
infra:pull-outputs | Pull outputs from storage backend |
All commands accept additional Alchemy CLI options. Run infra:deploy --help for the full list.
Troubleshooting
"EntityAlreadyExists" on first deploy
Your AWS resources already exist from a previous provisioning tool. Use --adopt:
infra:deploy --adopt"Resource already exists" Alchemy error
This is different from the AWS error above. It means the Alchemy resource type is registered twice. This can happen if you have conflicting Alchemy versions. Check packages/infra/package.json and ensure a single alchemy version.
Outputs not showing up after deploy
- Check that
storage-backendis configured (not"none") - Run
infra:pull-outputsto sync outputs to local state - Reload the devshell to pick up new outputs
"storage-bakend" typo error
If you see The option 'stackpanel.infra.storage-bakend' does not exist, check your config for the typo. The correct option is storage-backend (with a c).
Reference
- Deployment Overview for all deployment options
- Machines for machine inventory
- SST for the older SST-based infrastructure
- Secrets for encrypted secrets management