Examples

Simple stack

A single top-level key becomes the stack name. Add managed blocks by their stackie.* identifier — no further config needed.

# stackie-stack.yml
my-project:
  blocks:
    stackie.postgres:    # managed Postgres — installs and runs natively, no Docker
    stackie.redis:       # managed Redis — same, zero config required
Full-featured stack

Multiple blocks, a first-party service, substack composition, inter-block dependencies, variable overrides, and a custom storage root.

# stackie-stack.yml
my-project:
  storage_root: "myapp"        # isolates volumes under ~/.stackie/volumes/myapp/
  mode: shared                 # one shared postgres instance across all projects

  blocks:
    # Managed third-party blocks (stackie.* namespace)
    stackie.postgres:
      vars:
        USER: mydb             # override the default postgres user
      serves:
        PORT: { port: 5433 }   # shift off the default 5432 to avoid conflicts

    stackie.redis:
      depends_on:
        - stackie.postgres     # Stackie waits for postgres health before starting redis

    # First-party block — your own service, run from source
    my-api:
      path: "./backend"        # relative to the directory where you run stackie up
      command: ["npm", "run", "dev"]
      tools:
        - node: "20"           # Stackie installs the right Node version automatically
      consumes:
        db: stackie.postgres   # injects ${consumes.db.PORT} → the postgres port

  # Compose another stack in as a substack (e.g., full Supabase)
  stacks:
    stackie.supabase: {}       # {} = use the substack as-is, no overrides

Stack YAML Format

Reference for Stackie stack YAML files generated from the Rust Serde contract.

Top-Level Shape

A stack file contains exactly one top-level key. That key becomes CompositeStack.name.

YAML key <stack-name>
Value type CompositeStack

CompositeStack Fields

A named collection of block instances that run together as a development environment. Stacks are declared in a YAML file with a single top-level key that becomes the stack name.

web-app: storage_root: "myapp" blocks: stackie.postgres: stackie.redis: stackie.nodejs: depends_on: - stackie.postgres - stackie.redis 
# blocks
map<string, StackBlockNode> optional

Map of block node instances indexed by block name.

Keys are block identifiers (e.g., "stackie.postgres"). Values hold dependency declarations and per-instance overrides.

Defaults to an empty map when absent, so substack-only stacks (that only declare stacks:) parse cleanly. The schema layer still enforces that at least one of blocks, ingredients, or stacks is present.

# stacks
map<string, SubstackReference> optional

Substacks to compose into this stack, keyed by stack name.

Each entry references another stack by its name (e.g., stackie.supabase) and optionally provides overrides applied when resolving that substack's blocks.

Defaults to an empty map when the stacks: key is absent, ensuring backwards compatibility with existing stack YAML that has no substack references.

# mode
StackMode optional

Controls whether automatic port allocation is active for this stack.

  • unmanaged — port numbers must be declared explicitly (default) - managed — the port allocator assigns stable port numbers automatically
my-stack: mode: managed blocks: stackie.postgres: {} 
# storage_root
string|null optional

Optional storage root prefix for all block volume paths in this stack.

When set, every block's named path is stored under ~/.stackie/storage/volumes/{project}/{storage_root}/{path_name}/ instead of the default ~/.stackie/storage/volumes/{project}/{path_name}/.

This lets multiple stacks share the same project namespace without their volumes colliding. Blocks that provide an explicit path override are not affected.

my-stack: storage_root: "v2" blocks: stackie.postgres:  # data at ~/.stackie/storage/volumes/my-stack/v2/data/… 
# schema_version
string|null optional

Declares the YAML format version for this stack file. Omit to use the default (currently v1). When present, must match a supported version.

my-stack: schema_version: v1 blocks: stackie.postgres: {} 

Validation Rules

stack.first-party-path · StackBlockNode.path

First-party path validation

Validate all first-party blocks in a composite stack.

For every StackBlockNode with a path field set, this function verifies that:

1. The path is not absolute (only relative paths are permitted). 2. The path resolves to a directory within base_dir after std::fs::canonicalize is applied (prevents symlink-based traversal). 3. The resolved directory exists on the filesystem. 4. The block also has a command set (required for first-party blocks).

Third-party blocks (no path) are skipped entirely.

stack — The parsed composite stack to validate. base_dir — The working directory against which relative paths are resolved. Typically the current working directory (std::env::current_dir()).

Ok(()) if all first-party blocks are valid.

  • StackError::FirstPartyAbsolutePath — path is absolute.
  • StackError::FirstPartyPathTraversal — resolved path escapes base_dir.
  • StackError::FirstPartyPathNotFound — the directory does not exist.
  • StackError::FirstPartyMissingCommand — block has path but no command.

Uses canonicalize + starts_with as the security boundary. String-based .. checks are intentionally omitted — paths like ./sub/../sibling that stay within base_dir are accepted by design.

# use bridge::stack::composite::CompositeStack;
# use bridge::stack::parser::validate_first_party_blocks;
# use std::path::Path;
let yaml = r#"
my-app:
  blocks:
    api:
      path: "./backend"
      command: ["npm", "start"]
"#;
let stack = CompositeStack::from_yaml(yaml).unwrap();
validate_first_party_blocks(&stack, Path::new("/home/user/project")).unwrap();
stack.gateway-block-kind · StackBlockNode

Gateway block validation

Validate per-kind field rules for every block in the stack.

Runs after YAML parsing and schema validation to enforce the discrimination rules described by BlockKind:

- Gateway blocks (gateway.*) must declare a provider: chosen from KNOWN_GATEWAY_PROVIDERS, and must not declare path: or command:. The spaceport router lifecycle is owned by stackied. - Non-gateway blocks must not declare provider: — it is reserved for the gateway kind.

Returns the first error encountered (iteration order is implementation-defined because CompositeStack::blocks is a HashMap).

  • StackError::GatewayMissingProvider
  • StackError::GatewayHasPath
  • StackError::GatewayHasCommand
  • StackError::GatewayUnknownProvider
  • StackError::NonGatewayHasProvider
# use bridge::stack::composite::CompositeStack;
# use bridge::stack::parser::validate_gateway_blocks;
let yaml = r#"
web-app:
  blocks:
    gateway.aws:
      provider: aws
      serves:
        HTTP: { port: 4566, type: api }
"#;
let stack = CompositeStack::from_yaml(yaml).unwrap();
validate_gateway_blocks(&stack).unwrap();
stack.references · CompositeStack.blocks

Block reference validation

Validate stack structure and block references.

Performs validation without checking circular dependencies (handled separately). Checks: - All third-party block references exist in the provided registry. First-party blocks (those with StackBlockNode::path set) are exempt from the registry check — they are resolved from the local filesystem. - All dependencies reference blocks that exist in this stack.

  • ingredient_exists — Function to check if a block name exists in the registry.

Returns error if any third-party block or dependency is undefined.

validate_first_party_blocks — for path containment and command validation of first-party blocks.

stack.schema · CompositeStack

Schema validation

Validates a parsed YAML serde_yaml::Value against the composite stack schema.

The YAML value is converted to a JSON serde_json::Value first (YAML null/bool/number/string/seq/map all round-trip cleanly for the subset of YAML that stack files use; YAML-only features like explicit tags or non-string mapping keys are rejected with an appropriate error).

Returns StackError::InvalidFormat with a multi-line message listing every schema violation found. The message is formatted for direct display to the user:

Stack YAML does not match schema:
  - /my-stack: missing required property 'blocks'
  - /my-stack/blocks/bad.block/ports/HTTP: 70000 is greater than the maximum of 65535
use bridge::stack::validation::validate_composite_stack_yaml;

let yaml_value: serde_yaml::Value = serde_yaml::from_str(r#"
my-stack:
  blocks:
    stackie.postgres:
"#).unwrap();
assert!(validate_composite_stack_yaml(&yaml_value).is_ok());

Nested Types

# ConsumesEntry

Describes how a block consumes a port served by another block in the stack. Appears in the consumes: map of a block node under a logical name (e.g., DB). The interpolation engine resolves ${consumes.DB} to the actual port number.

String shorthand — the value is used as the block name:

consumes: db: neon                     # ${consumes.db.PORT} storage: stackie.minio       # ${consumes.storage.API} 

Typed struct — provides additional filtering options:

consumes: db: { port_type: db }              # match any block with a db-type port store: { block_override: stackie.minio }  # explicit block reference 
# port_type
string|null optional

Filters the consumed port by type (e.g., "db", "web"). When set, only ports matching this type are considered. When omitted, any port from the matched block is accepted.

# block_override
string|null optional

Explicitly names the block that provides this port, bypassing automatic resolution. Also set automatically when the string shorthand form is used (e.g., db: neon sets block_override to "neon").

If both block_override and port_type are set, block_override selects the block and port_type filters which port within it is used.

# HealthCheck

Health check configuration

Defines how to verify that a sandbox is running and healthy. Used by the daemon to monitor sandbox status.

# interval
integer optional

How often to perform health checks (seconds) Default: 30 seconds

# timeout
integer optional

How long to wait for response (seconds) Default: 5 seconds

# retries
integer optional

Number of consecutive failures before marking unhealthy Default: 3 retries

# start_period
integer optional

Grace period before starting health checks (seconds) Allows time for sandbox to initialize Default: 0 seconds

Accepted shapes

Execute a command and check exit code Example: { "type": "command", "command": ["pg_isready", "-h", "localhost"] }

required: <code class="icode">command</code>, <code class="icode">type</code>

Check HTTP endpoint Example: { "type": "http", "url": "http://localhost:8080/health" }

required: <code class="icode">type</code>, <code class="icode">url</code>

Check TCP port connectivity Example: { "type": "tcp", "port": 5432 } or { "type": "tcp", "port": "${ports.PORT}" }

required: <code class="icode">port</code>, <code class="icode">type</code>

Check if a process is running by name Example: { "type": "process", "process_name": "postgres" }

required: <code class="icode">process_name</code>, <code class="icode">type</code>

# PortConfig

Port configuration with number and type

# port
integer required

Port number

# type
PortType|null optional

Port type classification (optional, defaults to None for backward compatibility)

# PortType

Port type classification

Value Description
"web"

Web service port (HTTP, HTTPS, WebSocket, etc.)

"db"

Database port

"admin"

Administration/management port

"api"

API endpoint port

"metrics"

Metrics/monitoring port (Prometheus, StatsD, etc.)

"grpc"

gRPC service port

"messaging"

Message queue/broker port (AMQP, MQTT, STOMP, etc.)

"misc"

Miscellaneous port

# StackBlockNode

A single block instance within a stack, with optional dependency ordering and per-instance configuration overrides.

Three block kinds are supported based on the block name and fields present:

  • Third-party (stackie.) — resolved from the ingredient registry; no path: field - First-party (any other name) — a locally-owned service; requires path: and command: - Gateway (gateway.) — a cloud provider gateway; requires provider:
my-api: path: "./backend" command: ["npm", "run", "dev"] tools: - node: "20"

stackie.postgres: vars: USER: "mydb"

gateway.aws: provider: aws serves: HTTP: { port: 4566, type: api } consumes: - postgres 
# path
string|null optional

Local source directory for first-party (user-owned) blocks. When set, this block is treated as a locally-owned service whose source code lives at this path, relative to the directory where stackie up is run.

Must be a relative path pointing to a subdirectory of the working directory. Absolute paths and .. traversal are not allowed.

When omitted, the block is resolved as a third-party ingredient from the registry.

# depends_on
array<string> optional

Dependencies that must start before this block.

Defaults to empty list if not specified.

# consumes
map<string, ConsumesEntry> optional

Logical port consumption declarations for managed-mode port resolution.

Accepts two YAML forms:

List form (preferred) — each entry is a block instance name. The name is used directly as the interpolation key: F0

Map form — each key is a logical alias for the consumed block: F1

Only meaningful when the parent CompositeStack::mode is StackMode::Managed.

# name
string|null optional

Custom display name for this block instance If not specified, defaults to the block type name (e.g., "postgres" from "stackie.postgres") This is similar to docker's --name flag for containers.

# provider
string|null optional

Required on gateway.* blocks. Identifies which cloud provider the gateway should proxy to.

Accepted values: aws, gcp, azure, cloudflare.

gateway.cloudflare: provider: cloudflare serves: HTTP: { port: 4566, type: api } 
# version
string|null optional

Override version

# environment
object|null optional

Override environment variables

# serves
object|null optional

Named port configurations served by this block (accessed as ${serves.NAME}).

Declares the ports that this block exposes to the stack. In managed mode, the port allocator reads these entries to assign stable port numbers from the static-DHCP pool. Port numbers declared here are whitelisted in the sandbox policy.

Accepts ports as a serde alias for backward compatibility: existing YAML files that use ports: continue to parse without modification.

stackie.postgres: serves:           # or legacy: ports: PORT: { port: 5432, type: db } 
# vars
object|null optional

Override string variables (accessed as ${vars.NAME}) These override the default vars defined in the block

# paths
object|null optional

Override filesystem paths (accessed as ${paths.NAME}).

Maps a logical path name to the sandbox-side mount path. The physical host directory is derived as: ~/.stackie/storage/volumes/{project}/{path_name}/{sandbox_path_stripped}/

The directory is created automatically before the process starts and the sandbox policy grants read-write access to it.

# hooks_json
any optional

Defines commands to run at specific points in the block's lifecycle (e.g., after start, before stop). Hooks are keyed by lifecycle event name.

# init
array|null optional

Declarative setup operations (e.g., ensure_dir, copy, write_file) that run after the block's built-in init sequence. Lets stacks provide custom configuration not included in the block definition.

stackie.tomcat: init: - write_file: path: "${paths.base}/conf/server.xml" content: | <?xml version="1.0" encoding="UTF-8"?> <Server port="-1" shutdown="SHUTDOWN"> ... </Server> - ensure_dir: "${paths.base}/logs" 
# command
any optional

Required for first-party blocks (those with path: set). Specifies the process to run.

Accepts a string (shell-interpreted) or an array of strings (direct exec):

command: "npm run dev"          # shell-parsed command: ["npm", "run", "dev"]  # direct exec (preferred) 
# tools
array|null optional

Declares the tools (and versions) that must be available before this block's command runs. The tool manager provisions each tool automatically if not already installed.

Each entry is a single-key map of tool name to version:

tools: - node: "20" - python: "3.11" 
# health_check
HealthCheck|null optional

Health check configuration for this block.

Provides (for first-party blocks) or overrides (for third-party blocks) the health check used by the daemon to monitor block availability.

Uses bridge's own HealthCheck type, which supports HTTP, TCP, command, and process-based checks.

# emoji
string|null optional

Display emoji for topology view.

Overrides the block's default emoji in the dashboard topology node. For first-party blocks, this is the only way to set a topology icon.

emoji: "🚀" — shown in the topology node for this block instance.

# sandbox
boolean|null optional

Controls sandbox isolation for first-party blocks. Defaults to true (sandbox enabled) when omitted. Only applies to first-party blocks; third-party blocks are always sandboxed.

Set to false for development workflows that need unrestricted filesystem access. A warning is shown whenever sandbox isolation is disabled.

# bindings
object|null optional

Maps serverless function names to locally-running blocks on gateway.* blocks. When present, the gateway routes invocations to the specified block instead of a container, enabling full IDE debugger attachment without Docker image builds.

gateway.aws: provider: aws bindings: my-lambda-function: my-lambda-block another-function: another-block 

# StackMode

Controls whether automatic port allocation is active. YAML values: managed or unmanaged.

  • unmanaged — port numbers must be declared explicitly (default) - managed — the port allocator assigns stable port numbers automatically
Value Description
"managed"

Full managed mode: automatic port allocation.

The port allocator assigns stable ports from the static-DHCP pool to all serves: declarations.

"unmanaged"

No managed-mode automation (the default).

Matches all behaviour prior to managed-mode introduction. Port numbers must be provided explicitly.

# StringOrU16

A value that can be either a string or a u16 (for port numbers that support variable interpolation)

This allows YAML health check ports to be specified as either: - Numeric: port: 6379 - Variable reference: port: "${ports.PORT}"

Accepted shapes

A numeric port value

required: <code class="icode">Number</code>

A string that may contain variable references like "${ports.PORT}"

required: <code class="icode">String</code>

# SubstackReference

Optional overrides applied when composing another stack via the stacks: field. All fields are optional — use {} to include a substack with no overrides.

  • vars — applied to all blocks in the substack - ports — port overrides keyed by block name, then port name - storage_root — overrides the substack's volume path prefix
stackie.dev: stacks: stackie.supabase: {} stackie.redis: vars: MAX_MEMORY: "512mb" ports: stackie.redis: PORT: { port: 6380 } storage_root: "redis-v2" 
# vars
object|null optional

Variable overrides applied to ALL blocks in the substack (global scope).

These values are merged with each block's own vars, with the override values taking precedence.

# ports
object|null optional

Port overrides for blocks within the substack. The outer key is the block name (as declared in the substack), the inner key is the port name. The referenced block must exist in the substack.

# storage_root
string|null optional

Overrides the storage root for all blocks resolved from this substack. See storage_root on the top-level stack for full semantics.