---
title: "thinslice: bare+worktrees layout with named state"
description: "thinslice keeps your code separate from the data your services run on, like databases and config. That lets you work on several versions of your app at once without them colliding."
---
# thinslice: bare+worktrees layout with named state

## Problem

A normal git checkout conflates two things: the code you are working on and the
runtime state those processes need (databases, config, ports, secrets). When you
switch branches you have to manually re-point all your tools at the right state,
or you share one pile of state across every branch and hope for the best.

The goal here is to make code and state independently addressable, so you can
have multiple workstreams open at once — a feature branch, a PR under review,
a scratch experiment — each with its own isolated state, without touching each
other.

## Central invariant

Each state slot can be bound to at most one worktree at a time.

You will typically have a small number of slots — two or three, maybe five.
They are named, reusable resources. When you want to run the processes in a
worktree you attach a slot to it. You cannot accidentally run two servers
against the same postgres data directory because the mapping is explicit and
exclusive.

## Layout

`slice setup` converts a normal git checkout into this structure:

```
~/src/foo/
    +clone+/        bare git repo — the canonical source of objects and refs
    +state+/        a small number of named state slots
    main/           git worktree, checked out at branch 'main'
    local/          personal worktrees (unmanaged by thinslice)
    pr/             worktrees for reviewing pull requests
    tmp/            throwaway worktrees
```

The `+` delimiters are chosen to sort to the top of a directory listing and
make it visually obvious these are infrastructure directories rather than
worktrees.

### +clone+

A bare git repository. All objects and refs live here. Worktrees registered
against it can be added and removed without affecting each other or the history.
`git rev-parse --git-common-dir` from inside any worktree returns this path,
which is how `slice source` locates the rest of the structure without any
additional config.

### +state+

A directory of named state slots. Each slot is a self-contained bag of runtime
state: databases, config files, ports, secrets — everything the processes in a
worktree need that is not in the source code. Slots are named arbitrarily
(e.g. `template`, `slot-a`, `slot-b`) and there are usually only a handful.

One slot serves as a template: a known-good starting point with migrations run
and seed data loaded. When you start a new workstream you copy the template to
a fresh slot, attach it to your worktree, and go. The template itself is never
attached to a worktree; it just sits there as a clean base to copy from.

### Worktree categories

`local/`, `pr/`, and `tmp/` are conventional namespaces for organizing
worktrees by purpose. They are plain directories; thinslice has no opinion about
what goes inside them beyond that convention.

## bindings.toml

`+state+/bindings.toml` maps worktree paths to state directory names:

```toml
[bind]
"/Users/you/src/foo/main" = "slot-a"
"/Users/you/src/foo/pr/123" = "slot-b"
```

Keys are absolute paths to worktree directories. Values are slot names inside
`+state+`. Each slot may appear at most once — the 1:1 binding is enforced by
convention in this file. A worktree with no entry has no state attached;
`slice source` will tell you clearly when that is the case.

## State directories

Each slot inside `+state+` is a self-contained home for all the runtime state
a set of processes needs. Nothing that would normally be gitignored lives in the
worktree itself — there is no `config.toml.example` → `config.toml` copy step,
no local database at a fixed system path. The config file lives in the slot.
The postgres data directory lives in the slot. The worktree stays clean.

The required convention is a `thinslice.toml` manifest at the slot root:

```
+state+/slot-a/
    thinslice.toml      manifest: env vars, processes, ports, log paths
    env             KEY=VALUE derived from thinslice.toml [env]; for shell interop
    pids/           one .pid file per managed process
    logs/           one .log file per managed process
    postgres/       postgres data directory (PGDATA points here)
    config.toml     application config (an env var points here)
    myapp.db        sqlite database
    ...             anything else processes need to persist
```

`thinslice.toml` has two sections. The `[env]` section is a flat KEY=VALUE mapping
read by `slice source` at direnv time. The `[process.*]` sections declare
managed processes — what to run, which ports each one claims, and where its log
goes. This makes `thinslice.toml` the single source of truth for both runtime
configuration and process management.

```toml
# slot-a/thinslice.toml

[env]
DATABASE_PORT = "5433"
DATABASE_URL  = "postgres://localhost:5433/myapp"
PGDATA        = "/Users/you/src/foo/+state+/slot-a/postgres"
PORT          = "3001"

[process.postgres]
start = "postgres -D $SLICE_STATE_DIR/postgres"
ports = [5433]
log   = "$SLICE_STATE_DIR/logs/postgres.log"

[process.server]
start = "npm run dev"
ports = [3001]
log   = "$SLICE_STATE_DIR/logs/server.log"
```

Ports and paths differ between slots so that multiple worktrees can run
simultaneously without colliding.

Processes read their configuration from environment variables and write their
state to paths under `SLICE_STATE_DIR`. They need no knowledge of the thinslice
structure beyond those two variables. PID files are stored at
`$SLICE_STATE_DIR/pids/<name>.pid` by convention; `slice` derives this path
from the process name without requiring it to be declared explicitly.

### depends_on_worktree

By default every process restarts on a worktree swap: when `slice use <state>` is
run from another worktree, the old worktree's processes are stopped during the
unbind and started again by the next `slice up`. Set `depends_on_worktree = false`
on a process that doesn't depend on the worktree's code, and a swap leaves it
running instead of tearing it down and recreating it:

```toml
[process.postgres]
start = "postgres -D $SLICE_STATE_DIR/postgres"
ports = [5433]
log   = "$SLICE_STATE_DIR/logs/postgres.log"
depends_on_worktree = false
```

This is an optimization. A swap should cost no more than restarting the code
you're actually changing; anything shared or slow to recreate — a local database,
a cache, `kubectl port-forward`s to services you aren't editing — can stay up
across it. It matters most when you can't run the whole system locally and lean
on shared backends to fill in the rest.

The key defaults to `true`. When it's `false`, the process keeps running while the
state is unbound; the new worktree's `slice up` finds it already alive by its
`$SLICE_STATE_DIR/pids/<name>.pid` file and leaves it in place rather than
starting a second copy. `slice down` and `slice detach` still stop it — the flag
only skips the restart a swap would cause, not a teardown you asked for.

## slice source

`slice source` is the runtime bridge between a worktree and its state. It is
designed to be called by direnv from a `.envrc` in any worktree directory:

```bash
# .envrc
eval "$(slice source)"
```

When invoked, it:

1. Locates `+clone+` via `git rev-parse --git-common-dir`
2. Derives `+state+` as a sibling of `+clone+`
3. Reads `+state+/bindings.toml` to find which state directory is mapped to the
   current worktree path
4. Reads the `[env]` section of `+state+/<name>/thinslice.toml` and emits each
   entry as a bash `export` statement
5. Appends two thinslice-managed variables:

```bash
export SLICE_CODE_DIR="/abs/path/to/worktree"
export SLICE_STATE_DIR="/abs/path/to/+state+/name"
```

Errors go to stderr so that stdout stays clean for `eval`. Each failure mode
produces a diagnostic explaining what was expected, what was found, and the
exact steps to fix it.

`SLICE_CODE_DIR` and `SLICE_STATE_DIR` are the only variables processes need in
order to locate their own state without any knowledge of the surrounding
structure. A process that wants to find its data directory computes
`$SLICE_STATE_DIR/myprogram/` and stops there.

## slice state new

`slice state new <slot-name> [generator-script]` creates a new state slot by running
a generator script and writing its output as `+state+/<slot-name>/thinslice.toml`.

The generator is an executable file — any language — that prints a valid
`thinslice.toml` to stdout. It calls back into `slice give-me-a-port` to obtain
ports that are guaranteed not to conflict with any existing slot.

If no generator path is given, `slice state new` searches the repository for one in
this order:

```
config/new-state
src/new-state
```

A minimal bash generator looks like this:

```bash
#!/usr/bin/env bash

DB_PORT=$(slice give-me-a-port)
APP_PORT=$(slice give-me-a-port)

cat <<EOF
[env]
DATABASE_PORT = "$DB_PORT"
DATABASE_URL  = "postgres://localhost:$DB_PORT/myapp"
PGDATA        = "\$SLICE_STATE_DIR/postgres"
PORT          = "$APP_PORT"

[process.postgres]
start = "postgres -D \$SLICE_STATE_DIR/postgres"
ports = [$DB_PORT]
log   = "\$SLICE_STATE_DIR/logs/postgres.log"

[process.server]
start = "npm run dev"
ports = [$APP_PORT]
log   = "\$SLICE_STATE_DIR/logs/server.log"
EOF
```

Because the generator is just a program, it can contain conditional logic,
read existing files, or call other tools — without any DSL to learn.

After writing `thinslice.toml`, `slice state new`:

- Derives a flat `env` file from the `[env]` section (KEY=VALUE, one per line)
  for interop with shells and tools that source env files directly
- Creates the `pids/` and `logs/` subdirectories

`thinslice.toml` is the source of truth; `env` is a derived artifact regenerated
whenever `thinslice.toml` changes. Tools that only need environment variables can
use `env` without knowing anything about the thinslice structure. `slice source`
reads `thinslice.toml` directly.

`slice state new` does not run any processes or perform database initialization; that
is left to the user's own setup scripts, which can use `$SLICE_STATE_DIR` to
locate the right place.

## slice give-me-a-port

`slice give-me-a-port` finds an unused port and prints it to stdout. It is
designed to be called from inside a generator script during `slice state new`.

To select a port it:

1. Collects all ports declared in `[process.*]` sections across every existing
   slot's `thinslice.toml`
2. Collects all ports already issued to the current generator invocation (ports
   are tracked in a session tempfile that `slice state new` creates and cleans up)
3. Attempts a `bind()` on candidate ports to confirm nothing else on the machine
   is using them
4. Prints the chosen port and appends it to the session list so the next call
   within the same script picks a different one

Errors go to stderr; the port number is the only thing on stdout, so
`PORT=$(slice give-me-a-port)` works without further parsing.
