Disposable software stacks: named, leased, isolated, proven, accounted for, destroyed.
stackless is a CLI that owns the complete lifecycle of disposable stacks. One declarative file describes your product once — every service, datastore, secret, and health contract. One verb spawns a full, isolated, working copy with a name and a URL; one verb proves it works; one verb (or an expired lease) destroys it verifiably. On a laptop or on a cloud provider, for a human, a CI job, or — first and foremost — an AI agent.
$ stackless up --name demo --on local
demo: up on local (all health contracts passed)
api: http://api.demo.localhost:4444
web: http://demo.localhost:4444
$ stackless verify demo
demo: verify passed (lease renewed)
$ stackless down demo
demo: destroyed, verified gone; tombstone and logs keptUnopinionated about the application. Opinionated about the lifecycle.
See VISION.md for why this exists and the invariants it
refuses to break; ARCHITECTURE.md for how it is
built; docs/SCHEMA.md for the complete
stackless.toml reference.
A stranger — or an agent handed nothing but a repo containing a
stackless.toml — runs one command and gets a working, isolated, named
copy of the entire product with a URL they can open. One more command
proves it healthy. They walk away; within the lease window it is gone,
verifiably. No wiki page, no teammate, no manual cleanup.
- A stack definition (
stackless.toml) declares services, datastores, hosted integrations, secrets, wiring, and health contracts once. Wiring is interpolation —DATABASE_URL = "${datastores.db.url}",CLERK_SECRET_KEY = "${integrations.clerk.secret_key}"— and the startup order is derived from it; there is nodepends_onto drift. - Hosted integrations (
[integrations.<name>], with a requiredprovidernaming the catalog adapter) are provisioned as stack resources too. For Clerk (provider = "clerk"), Stackless creates the app through Stripe Projects, can enable slugged Organizations, and exposes the selected publishable/secret keys for services and verify. - Secrets —
[secrets].requiredkeys resolve from.stackless.envbeside the definition file (vault pull layers in when[stack.projects.stripe].projectis recorded). A required key missing from every source fails validation before anything provisions. - An instance is a named, short-lived incarnation of the stack.
Pass
--nameat creation (DNS-safe); omit it and stackless assigns{stack.name}-{uuid}. Everything the instance owns derives from the name. Any number of instances coexist without colliding on ports, names, data, or credentials. - Substrates (stack hosts) decide where instances live. Pass
--on local,--on render,--on vercel,--on fly, or--on netlifyat creation (required); resume uses the recorded substrate and never asks again:- local — services run as host processes from your declared
commands; datastores run as labeled Docker containers with
per-instance volumes; everything meets at a built-in reverse proxy,
so origins are derivable from the name alone:
http://{service}.{instance}.localhost:4444. - render — the same definition deploys to
Render through the same Stripe Project used
for hosted integrations (one long-lived project per stack, one
named environment per instance), with hard spend caps and
per-invocation paid consent (
--confirm-paid). Stripe Projects provisions catalog resources; the Render REST API handles env vars, deploys, health waits, and teardown verification (RENDER_API_KEYor.render-api-key). After cloudup/down, a spend summary is printed (bounded by the project hard cap). - vercel — git-backed projects on
Vercel via Stripe
vercel/project(and optionalvercel/prowhen[stack.vercel].plan = "pro"). Stripe creates/links the project; the Vercel REST API pushes interpolated env, triggers git deployments, polls until READY, and verifies teardown (VERCEL_TOKENor.vercel-token). No managed postgres on Vercel in v0;source.repomust be a public GitHub HTTPS remote. - fly — container apps on Fly.io via Stripe
flyio/app(paid →--confirm-paid). Stripe creates the app and hands back a scoped deploy token; the Fly Machines REST API uses it to allocate the app's public IPs, run the service's prebuiltimageas a machine, and poll it tostarted, health-gating onhttps://{stack}-{instance}-{service}.fly.dev. Teardown removes the Stripe resource and confirms via its registration (no operator API token needed). v0 is image-only (no build-from-source) and has no managed datastore. - netlify — static sites on Netlify via Stripe
netlify/project(free). Stripe creates the site and returns a scoped token; the substrate clones the pinned ref and runs the Netlify file-digest deploy (SHA1 per file, upload only what's missing), polls toready, and health-gates onhttps://{stack}-{instance}-{service}.netlify.app. Teardown removes the Stripe resource and confirms via its registration. v0 is static-upload (no build step) and has no managed datastore.
- local — services run as host processes from your declared
commands; datastores run as labeled Docker containers with
per-instance volumes; everything meets at a built-in reverse proxy,
so origins are derivable from the name alone:
- Sources are git references (
repo+ref), materialized per instance from a shared object cache. For the edit loop,--source service=/path/to/checkoutpins a service to your dirty worktree — explicit, recorded, local-only. setup/preparehooks — optional per service.setupruns once after source materialization (toolchain, deps);prepareruns on everyupafter the service's dependencies are ready and before it starts (migrations, seed).- Health gates
up(invariant: provisioned ≠ configured ≠ verified). An instance is not "up" because processes started; it is up when every service's health contract passes through its public origin.stackless verifyruns the stack's own proof command (the smoke tier) with the instance's origins and env exported. - Every instance carries a lease (local default 24h; render,
vercel, fly, and netlify default 8h).
Mutating verbs and successful
verifyrenew it; when it expires, a reaper sends the instance through the same verified teardown asdown. Teardown refuses to report success while anything that bills or holds state survives — and leaves a tombstone, sostatusandlogsstill answer why an instance disappeared.
| Verb | Does |
|---|---|
up [--name <name>] |
Create or resume an instance (no separate resume verb). --name optional at creation ({stack}-{uuid}); --on <substrate> required at creation; --file <path>, --source svc=path, --lease 8h, --confirm-paid |
down <name> |
Verified teardown; exits non-zero listing survivors if anything remains |
verify <name> |
Run the stack's proof contract; renews the lease |
status <name> |
Staged truth per service: provisioned → prepared → started → healthy, downgraded by observation |
list |
All instances with substrate, active/tombstoned, per-service stage, remaining lease |
logs <name> [service] |
Captured service output (local files / Render log API); Vercel uses the dashboard for now; survives teardown; --tail (default 100) |
check <file> |
Parse + validate a definition, print the derived graph; --on <substrate> adds substrate checks |
Every command is non-interactive, supports --json, and exits with
codes an agent can branch on.
- stdout — final success or failure envelopes (
ok: true/false). - stderr — human prose in non-JSON mode; in
--jsonmode, NDJSON progress events duringup(so stdout stays machine-parseable).
Every error carries three parts: what failed, why (observed, not guessed), and how to proceed:
{
"ok": false,
"error": {
"schema_version": 1,
"code": "state.lock.held",
"message": "instance \"demo\" is locked by operation \"up\" (pid 4242, ...)",
"step": "start:api",
"instance": "demo",
"remediation": "wait for the running operation on \"demo\" to finish and retry; ...",
"context": {
"service": "api",
"hook": "setup",
"command": "mise install",
"log_path": "/path/to/log",
"log_tail": "last lines of captured output on hook/health failures"
}
}
}step, instance, and context fields are omitted when not
applicable; context subfields are populated only when observables
exist.
During up --json, stderr emits one NDJSON object per plan step:
step_started, step_skipped, step_completed, or step_failed,
with schema_version, instance, step, kind, node, index,
total, and optional code.
Success shapes: up --json includes schema_version, executed,
skipped, and origins; status/list --json may include
persistence_warning when daemon boot persistence failed (leases then
depend on the daemon staying up).
Codes are stable, versioned API surface — branch on error.code,
never on prose.
$ cargo build --release # one binary: target/release/stackless
$ cd your-repo # containing a stackless.toml
$ stackless check stackless.toml # validate + see the derived graph
$ stackless up --name demo --on local # clone, build, wire, health-gate
$ stackless down demo # verified teardownLocal substrate: Docker is required for datastore containers; app services run as host processes.
Writing a definition: start from docs/SCHEMA.md — it is written to be sufficient on its own, for humans and agents.
The repository pins its toolchain and auxiliary tools via mise:
# one-time: install mise (https://mise.jdx.dev/getting-started.html), then:
mise installThis provides the exact Rust 1.96.0 (via rust-toolchain.toml + mise) plus cargo-nextest, cargo-audit, cargo-deny, cargo-vet, cargo-dist, and taplo.
Common commands (also wired as mise run <task>):
- Tests:
cargo nextest run --workspace(ormise run test) - Hygiene ("cargo crap"):
mise run ci(fmt + clippy + taplo + nextest + audit + deny + vet) - Individual:
cargo audit,cargo deny check,cargo vet,taplo fmt --check - Live smoke tests against real providers:
mise run smoke-vercel/mise run smoke-render/mise run smoke-fly/mise run smoke-netlify/mise run smoke(creds from.stackless.env) — see docs/SELFTEST.md
Releases use cargo-dist (see generated .github/workflows/release.yml).
The original cargo build / cargo test paths remain valid.
| Crate | Owns |
|---|---|
stackless-core |
Definition model + validation + interpolation + derived graph, the SQL state store (local rusqlite file; opt-in fleet plane via libsql remote), instances, leases, locks, checkpoint journal, the lifecycle engine, the Substrate trait |
stackless-stripe-projects |
Neutral Stripe Projects CLI driver: project anchor ([stack.projects.stripe]), per-instance environments, catalog add/remove, env materialization |
stackless-integrations |
Hosted integration routing and provider adapters (Clerk today); substrates call here for provision / observe / destroy |
stackless-local |
Local substrate: process spawn/teardown, container datastores, source materialization (via stackless-git), wiring, hosted integrations |
stackless-git |
Pure-Rust git (backed by grit-lib): one bare cache repo per source URL with thin per-instance checkouts sharing objects via alternates (local materialization); shallow clone + checkout for cloud prepare |
stackless-render |
Render substrate (REST calls go through the generated render-client crate) |
stackless-vercel |
Vercel substrate (REST calls go through the generated vercel-client crate) |
render-client / vercel-client |
Provider REST API clients generated by cargo-progenitor from the vendored OpenAPI specs (regenerate via specs/regen-clients.sh) |
stackless-daemon |
The one resident component: reverse proxy, supervision, lease reaper, launchd/systemd persistence |
stackless |
The clap CLI binary (also hosts the daemon via daemon run) |
Substrates are plugins behind one trait: adding a provider crate requires no changes to the engine or state machinery — only a registry entry in the binary.
Stripe Projects is the internal catalog driver — never declared in
stackless.toml. Checked items work today; unchecked items are not
implemented yet.
- local
- render
- vercel
- fly.io
- railway
- netlify
- cloudflare workers
- gitlab
- laravel cloud
- wordpress.com
- clerk
- auth0
- workos
- privy
- supabase
- postgres (local — Docker)
- postgres (render —
render/postgres) - postgres (vercel)
- neon
- supabase
- planetscale
- turso
- upstash redis
-
stackless logs(local) -
stackless logs(render) -
stackless logs(vercel) - fleet state plane (Turso Cloud)
v0 lifecycle layer, under active development. Local substrate, daemon, and lifecycle engine are implemented and tested. Render and Vercel substrates are implemented (Stripe Projects provisions catalog resources; each cloud host's REST API handles post-provision lifecycle steps). Live end-to-end verification on real cloud accounts is ongoing. Opt-in fleet mode shares state across machines; Turso Cloud live verification is pending. The secret-blind egress boundary described in VISION.md is deliberately sequenced after v0 — see ARCHITECTURE.md §0 for the v0 secrets posture (operator-visible, test-scoped).