State

A state directory contains everything that isn't your code: database files, redis data, port assignments, and configuration. All state directories are live within +state+/ inside your project root.

This document explains the concept of a state directory — what it is, why code and state are decoupled, and how you bind, clone, and swap between states. For the mechanics of how a state directory gets created and populated (initdb, config files, seed data), see Template.

How state works

Code and state are explicitly decoupled. A worktree contains your source code. A state directory contains everything else. You bind a worktree to a state directory, and slice up starts your processes using the code from the worktree and the data from the state.

This means you can:

  • Run the same code against different states (a clean database vs. one with seed data)
  • Run different code against the same state (swap your worktree, keep the database)
  • Clone a state to get an instant copy of a known-good environment

State directory contents

A typical state directory looks like:

+state+/dev-signed-in/
  thinslice.toml              # source of truth: env vars, processes, ports
  env                     # KEY=VALUE derived from [env]; for shell interop
  pgdata/                 # postgres data directory
  redis/                  # redis data directory
  backend.config.json     # config file for the backend server process of your stack
  ...                     # anything else your thinslice.new-state creates

Both the thinslice.toml file and the initial contents of the directory (pgdata/, redis/, config files, etc.) are produced by running your thinslice.new-state executable. The template is the single source of truth for what goes into a state directory — it allocates ports, emits the TOML config, and runs any setup commands (initdb, mkdir, schema migrations, config-file generation) needed to populate the directory. See Template for how to write one.

thinslice.toml has two required sections. The [env] section is a flat key=value mapping of every environment variable needed to configure the slice — ports, connection strings, data directory paths. The [process.*] sections declare managed processes — what to run and which ports each one claims. This makes thinslice.toml the single source of truth for both runtime configuration and process management.

The env file is a derived artifact — a plain KEY=VALUE projection of [env], one entry per line, regenerated whenever thinslice.toml is written. thinslice.toml is the source of truth; env is a convenience for shell interop. Tools that only need environment variables can source env directly without knowing anything about TOML or the thinslice directory structure:

source /Users/you/src/foo/+state+/dev/env
psql -p $PG_PORT
cargo run --bin api

After the processes start, they write their own data into the directory alongside the initial contents — that's the runtime state you're trying to preserve and clone.

Commands

slice state new

Create a new state directory.

slice state new <name> [--from <existing>]

Without --from, this creates an empty state directory and runs your template to populate it — generating thinslice.toml, initializing data directories, writing config files, etc.

With --from, this clones an existing state directory (copying all data files) and re-runs the template to regenerate port assignments so the new state doesn't collide with the original.

$ slice state new dev-signed-in                         # fresh state
$ slice state new dev-signed-in --from template-clean   # clone from template

slice state ls

List all state directories with their bindings and status.

$ slice state ls

  STATE             BOUND TO              STATUS
  template-clean    —                     stopped
  dev-signed-in     local/feature-auth    running
  scratch           tmp/try-sqlx          running
  agent-1           tmp/task-a            running

slice state rm

Remove a state directory.

$ slice state rm scratch

This refuses to remove a state that is currently bound to a worktree or has running processes. Stop and detach first:

$ slice down
$ slice detach
$ slice state rm scratch

Binding worktrees to state

A worktree must be bound to a state directory before slice up will work. The binding is stored by thinslice and persists across restarts.

$ cd local/feature-auth
$ slice use dev-signed-in       # bind this worktree to dev-signed-in state
$ slice up                      # now works

slice use shuts down any processes that were previously running against the specified state, unbinds the old worktree, and binds the new one. Processes marked depends_on_worktree = false are the exception — they keep running across the swap, so it only restarts the code you're actually changing. This is how you swap code: stop the old worktree, bind the state to a different worktree, start again.

# feature-auth is running against dev-signed-in
$ cd ../bugfix-503
$ slice use dev-signed-in       # shuts down feature-auth, binds here
$ slice up                      # same state, different code

To unbind without rebinding elsewhere:

$ slice detach                  # stops processes and unbinds

How bindings are stored

+state+/bindings.toml is an index that maps worktrees to state directories:

[bind]
"/Users/you/src/foo/main" = "/Users/you/src/foo/+state+/dev-signed-in"
"/Users/you/src/foo/local/feature-auth" = "/Users/you/src/foo/+state+/slot-a"
"/Users/you/src/foo/pr/4521" = "/Users/you/src/foo/+state+/slot-b"

Keys are absolute paths to worktree directories. Values are absolute paths to state directories. Each state may appear at most once — the 1:1 constraint is enforced here.

I chose absolute paths on both sides to make interop easy. Any tool — a shell script, direnv, an editor plugin — can read this file and use the paths directly without knowing anything about thinslice's directory structure or git internals.

slice use and slice detach are the only commands that write to this file.

The tradeoff is that paths go stale if the project directory moves. slice doctor fixes this: the state paths all contain +state+ as a path component, so the doctor can split on that to recover the old project root, diff it against the actual location of +state+/ on disk, and rewrite both sides of every entry.

Template state

The template-clean state created during slice setup is intended as a seed. You clone it when starting new work:

$ slice state new my-work --from template-clean

You can maintain multiple templates for different scenarios:

+state+/
  template-clean/           # empty database, default config
  template-signed-in/       # seeded with a logged-in user
  template-full/            # seeded with realistic sample data

Create these manually by cloning template-clean, starting it up, running your seed scripts, then stopping:

$ slice state new template-signed-in --from template-clean
$ cd main
$ slice use template-signed-in
$ slice up
$ psql -p 5440 -c "INSERT INTO users ..."
$ ./scripts/seed-signed-in.sh
$ slice down
$ slice detach
# template-signed-in now has a signed-in user in its database