Docs

Settings

What Toyon reads from a project's settings file, and what it hands every command it runs.

The file

The settings live in .toyon/settings.json, or in toyon.json at the root for a project that wants the file in plain sight. Beside either one, .toyon/settings.local.json or toyon.local.json overrides it for one person and stays out of git. Comments and trailing commas are fine in any of them.

The first open guesses a file from package.json and asks you to confirm it. Other stacks start from an empty guess the agent can fill in.

{
  "setup": ["npm install"],
  "run": {
    "web": "vite --port $PORT --strictPort",
    "api": "node --watch server.js"
  },
  "check": "npm test",
  "land": { "route": "pr" },
  "afterLand": ["npm run build"],
  "timeouts": { "check": "30m" }
}

Keys

The contract

Anything Toyon runs should stay in the foreground, listen on $PORT, and reload itself however it likes.

Sharing the backend

Most changes touch the page and not the API behind it. A run entry with "from": "trunk" is the shared tier: the main checkout runs it once, on main's code, and every copy's page reaches that one at its usual <NAME>_URL. A copy whose own changes touch that command's files starts its own and the address quietly points there instead; a copy that only changes the page never pays for an API of its own. The page itself is never shared.

{
  "run": {
    "web": "vite --port $PORT --strictPort",
    "api": { "cmd": "node --watch server.js", "from": "trunk", "paths": ["server/**"] },
    "db": { "cmd": "docker compose up db", "from": "trunk", "paths": [] }
  }
}

paths are globs from the root that make a copy run its own. Left out, they are read off the command (server.js, a cd into a folder), and a command that names nothing flips on anything outside the page's folders. [] means the copy never runs its own, which suits a database container whose databases are per copy (below). Lockfiles, .env files, compose files and migration folders count for every command that can flip. A copy's row menu also offers the switch by hand, either way.

Main's shared tier is awake while any copy using it is, and sleeps with them. Nothing is shared until you say so: the setup pane has a "shared from main" box under each command but the page, off until you tick it.

Keeping copies apart

Toyon does not know what a database is. Each copy runs your setup and your commands on its own, so a database, a compose stack or a cache they all point at is shared, migrations included. When the tree names one (a DATABASE_URL in an env file, a compose file at the root) and the settings name no copy, the setup pane says so and can ask the agent to write the recipe below; a land whose work changes migrations says so beside the word.

Every command, setup step, teardown step and terminal gets two variables to keep them apart:

A Postgres database per copy, copied from a template and dropped when the copy goes. The copy is made while the spare warms, so it is there before you type; it takes as long and as much disk as the template is big, so keep the template seed-sized rather than a production dump. Postgres refuses to copy a database with connections open, which is why the template is its own database that nothing serves:

{
  "setup": ["npm install", "createdb -T app_template \"app_$TOYON_WORKTREE\""],
  "teardown": ["dropdb --if-exists \"app_$TOYON_WORKTREE\""],
  "run": {
    "web": "DATABASE_URL=\"postgres://localhost/app${TOYON_WORKTREE:+_$TOYON_WORKTREE}\" npm run dev"
  }
}

A migration step in setup needs the same DATABASE_URL in front of it, since setup does not see what run sets.

A Neon branch per copy is the same recipe with a branch in place of a copy: instant, copy-on-write, data included. The commands need to know the project: run neon link once in the checkout, or add --project-id to each. The neonctl package installs both neon and neonctl.

{
  "setup": ["neonctl branches create --name \"wt_$TOYON_WORKTREE\" --parent main"],
  "teardown": ["neonctl branches delete \"wt_$TOYON_WORKTREE\""],
  "run": {
    "web": "DATABASE_URL=\"$(neonctl connection-string \"wt_$TOYON_WORKTREE\")\" npm run dev"
  }
}

A compose stack per copy, under its own project name. Ports the stack publishes on the host still collide, so leave them unpublished or take them from the environment:

{
  "teardown": ["docker compose -p \"app_$TOYON_WORKTREE\" down -v"],
  "run": {
    "db": "docker compose -p \"app_$TOYON_WORKTREE\" up",
    "web": "npm run dev"
  }
}

A SQLite file copied from the main checkout:

{
  "setup": ["cp \"$TOYON_ROOT/data/dev.db\" data/"]
}

node_modules and the .env files come along on their own. Without a teardown, nothing drops a database when a copy goes; that is yours to clean up.

Keeping what a copy built

A new copy starts with the main checkout's node_modules, cloned rather than copied where the disk allows it (APFS, btrfs, XFS), so an install that has nothing to do takes no time. What a copy builds while it works, an incremental type check or a test selection database, is gone with the copy unless the settings say to keep it. Nothing is kept until cache names at least one path:

{
  "check": "uv run mypy . && uv run pytest",
  "cache": ["node_modules", ".mypy_cache", ".testmondata"]
}

When check passes in a copy, those paths are kept, once per base commit, and the next copy gets them before its setup runs, as the "restoring cache" stage of its setup: mypy and testmon start from the last run's answers, and bun install or uv sync finds its work done. A path that is not there when the check passes is simply not kept, and a path the copy already has is never replaced. A SQLite file brings its -wal and -journal along, so nothing committed is left behind.

What is kept is keyed by what built it: the platform, every lockfile in the tree, and the versions of the tools involved. A copy with a different lockfile, or a machine with a different Python, gets nothing rather than something wrong. Among what matches, the copy takes what was kept at its own base commit, else the newest; an incremental cache from a neighbouring commit is mostly right, and its tool checks every answer in it.

The long form names more of the key:

{
  "cache": {
    "paths": [".mypy_cache", ".testmondata"],
    "key": ["pyproject.toml", "mypy.ini"],
    "tools": ["python3 --version", "uv run mypy --version"]
  }
}

key lists files whose contents matter beside the lockfiles. tools lists commands whose output is part of the key; left out, they follow from the lockfiles present (bun --version for a bun.lock, node --version for npm, pnpm and yarn, python3 --version for uv, poetry and pip, rustc --version for cargo), and [] asks none. Entries live under ~/.toyon/cache.noindex, the newest three per key are kept, and a key nothing has used in a month goes at the next start.

What this costs in disk depends on the filesystem. On APFS, btrfs and XFS an entry shares its blocks with the copy it was kept from, so three entries of a gigabyte node_modules cost little beyond the files that differ. On ext4, and on any disk without copy-on-write, each entry is a full copy: three entries per key and a key per lockfile and tool version, so a node_modules there is worth caching only when the install it saves is slower than the copy. The daemon log names which kind of copy each entry took.

Sleep

Copies start when you open them, not when the daemon boots. A copy nobody has looked at for two hours stops running its servers (five minutes on a deployed machine), and sooner when the machine runs low on memory. It starts again when you open it or visit its preview. The chat and the agent keep going either way.

TOYON_PROC_SLEEP_MS sets the delay in milliseconds, and off turns the clock off.

The daemon stops itself too. Once no window or preview has been open and nothing has been working for half an hour, it exits, and the next toyon or Dock launch starts one again. An agent mid-turn, a command, a clone or a build after a land all count as working, so nothing is cut off. A daemon reached over the network, one on a deployed machine, or one you ran yourself in a terminal never stops on its own; TOYON_STOP_AFTER_MS sets the delay in milliseconds anywhere, and off turns it off.

On a Mac the daemon also holds off the machine's own idle sleep while Toyon is in use, so a laptop on battery does not stop the work a minute after your last keypress. In use means an agent is mid-turn, a command is running, or a shell is open from another device. A phone that disconnects, and a question waiting on you where a phone could answer it, each hold the Mac for half an hour more, and then it may sleep; the question is still there when it wakes. Closing the lid sleeps as it always does.

"keep awake" in settings has three values. "while in use" is the default. "always" holds for as long as the daemon runs, on battery too, for a Mac left open as a server. "off" leaves the Mac's sleep alone. TOYON_KEEP_AWAKE=off turns it off for a daemon whatever settings says.