Skip to content

planctl User Manual

planctl is the command-line toolchain for the Planck stack. A single binary takes care of host setup, project scaffolding, .zsx template compilation, deployments to a Workbench-supervised host, runtime lifecycle, and backup / restore.

This manual is the long form. planctl --help (or any invocation without a command) prints a short usage screen.

planctl <command> [args...]

Table of contents

  1. Install and host setup
  2. Configure profiles
  3. Scaffold a project
  4. Add features and services
  5. ZSX template compiler
  6. Deploy
  7. Lifecycle: start, stop, restart, status
  8. Undeploy
  9. Backup and restore
  10. Schema: create and drop stores and indexes
  11. Export and import
  12. Service configuration reference
  13. Workflow recipes
  14. Troubleshooting

1. Install and host setup

Once the planctl binary is on your PATH, the system subcommands handle the rest of the host bootstrap. They lay out the directory tree, install the Workbench along with the system database, and register the same with the OS supervisor (launchd on macOS, systemd on Linux).

SubcommandEffect
system initInstall Workbench + sysdb, register supervised services
system startStart every Planck-supervised process on this host
system stopStop every Planck-supervised process
system deinitUnregister services. Binaries and data files stay on disk.
bash
planctl system init
planctl system start

system init accepts --mode <dev|prod> to pick the install location:

  • dev installs under $HOME/.planck and skips OS-level service registration. planctl spawns the processes directly. This is the macOS default.
  • prod installs under /opt/planck and registers services with the platform supervisor. This is the Linux default.

About system start and system stop on macOS

On macOS these two are thin wrappers around launchctl. system stop walks /Library/LaunchDaemons for every plist owned by Planck and runs launchctl bootout system <plist> on each. system start does the same walk and runs launchctl bootstrap system <plist>. Touching /Library/LaunchDaemons usually needs root, so expect to prefix these with sudo.

bootout and bootstrap are no-ops when the service is already in the target state, so re-running either command is perfectly safe.

On Linux, system start and system stop are only placeholders today and print a notice. Use systemctl directly.

What system init lays down

PathPurpose
~/.planck/binThe planctl binary, the Workbench, the sysdb
~/.planck/appsPer-app deployment directories
~/.planck/logsWorkbench and service logs
~/.planck/systemSystem database files (planck.system.db)
~/.planck/workbenchWorkbench install and config

In prod mode the same layout lives under /opt/planck.


2. Configure profiles

planctl reads its target host(s) from ~/.planctl/config.yaml. The config file declares one or more profiles, and each profile lists one or more workbench nodes.

yaml
profiles:
  - name: dev
    nodes:
      - server: https://127.0.0.1:24011
        uid: admin
        key: <wb-admin-key>

  - name: prod
    nodes:
      - server: https://prod-wb.example.com
        uid: admin
        key: <wb-admin-key>
      - server: https://prod-wb-replica.example.com
        uid: admin
        key: <wb-admin-key>

When a profile has multiple nodes, every command iterates them in order and runs against each one. That is how a prod profile can target both the command and the query workbench in a single deploy.

Every command that talks to a Workbench needs --profile <name>. There is no built-in default profile and no implicit fallback to localhost. If you forget --profile, the command exits with an error.

Per-invocation overrides

Three flags let you override individual fields from a profile for a single call:

FlagPurpose
--server <url>Replace the workbench URL for this call
--uid <user>Replace the admin uid
--key <hex>Replace the admin key
--profile <n>Pick which profile to start from
--dry-runSkip network calls. Print what would happen.

Override precedence is decided field by field: a CLI flag wins, otherwise the profile's value is used. Mix and match freely.

bash
# Use staging's URL and uid, but a one-off rotated key
planctl deploy --all --arch mono --profile staging --key "$ROTATED_KEY"

We do not read environment variables for these values. Config lives in YAML, and that is the only source.

TLS in scaffolded mono apps

Mono templates default to TLS 1.3 with a self-signed certificate, generated on first boot. That covers local development, demos, and small single-node deploys without you having to think about cert plumbing at all.

When you want something else:

  • Own cert. Drop your cert and key on disk and point tls.cert_path / tls.key_path in db.yaml at them.
  • Plain HTTP behind a proxy. Set tls.enabled: false. The reverse proxy terminates TLS, and the app speaks HTTP on its bind.

3. Scaffold a project

bash
planctl new <name> --type <hda|spa> --arch <mono|micro>

Four templates ship embedded in the binary, so planctl new works offline:

TypeArchResult
hdamonoHypermedia-driven monolith. Single deployable, server-rendered HTML.
hdamicroHypermedia shell + per-feature WASM services + an SSE service.
spamonoSPA monolith. Vue bundle + a single API.
spamicroSPA shell + per-feature WASM services + an SSE service.

Flags:

FlagMeaning
--typehda (server-rendered HTML) or spa (Vue bundle)
--archmono (one deployable) or micro (shell + services)
--force, -fOverwrite an existing <name>/ directory
bash
planctl new notes --type hda --arch mono
planctl new shop  --type spa --arch micro

Choosing a reverse proxy

When you scaffold, you decide which reverse proxy fits your operations story: Caddy, nginx, or Traefik. planctl new does not pick one for you and does not hardcode supervision. Pick the proxy that matches your environment and drop its config into the project once you have one. The mono TLS default means you can run without a proxy during development. In production, a proxy in front lets you turn TLS off in the app and centralize cert handling.

The SSE subproject

All four templates ship with an sse/ subproject for live updates. It builds on ssehub, an in-process pub/sub library with topic-keyed fan-out. A scaffolded sse/ looks like this:

sse/
  build.zig
  build.zig.zon
  sse.yaml
  src/
    main.zig
    hub.zig
    handlers/
      example.zig
      README.md

For micro projects, the SSE service is deployed last so that the topic is live before browsers connect.

Project name rules

The name has to be a valid Zig package identifier: lowercase letters, digits, and underscores, starting with a letter. No hyphens. planctl new rejects bad names with a clear error.

Legacy planctl init

The old planctl init <name> [--type wasm|app] scaffolders still work and print a deprecation notice. Do use planctl new for any new project.


4. Add features and services

Inside an existing project root, planctl add materializes a new feature (for mono projects) or a new service (for micro projects).

bash
planctl add <name> --type <feature|service> [--arch <hypermedia|rest>] [--port <n>] [--force]
TypeWhere it worksWhat it adds
featureMono projects (hda-mono, spa-mono)src/features/<name>/ with routes + handlers
serviceMicro projects (hda-micro, spa-micro)services/<name>/ with its own build

The --arch flag is optional and accepts hypermedia (zsx-rendered routes) or rest (JSON-only). It is auto-detected from the project layout if you leave it off. --port lets you pin a service port, otherwise the next-available port is scanned.

bash
# In a mono project root:
planctl add billing --type feature

# In a micro project root:
planctl add orders --type service --port 4501

5. ZSX template compiler

ZSX is a JSX-like template syntax that compiles to Zig (or Rust, or Go). Templates write directly into a byte buffer. There is no virtual DOM, no template runtime, and the generated code is inlinable plain code.

Compilation surface

bash
# Single file to stdout (handy for inspection)
planctl src/features/tasks/zsx/task_list.zsx

# Batch transform a directory tree
planctl src/zsx/ src/fragments/

# Explicit codegen backend
planctl --target zig src/zsx/ src/fragments/
planctl --target rust src/zsx/ src/fragments/
planctl --target go   src/zsx/ src/fragments/

# Watch mode (recompile on change)
planctl --watch src/zsx/ src/fragments/

# Remove generated files (only touches files with the AUTO-GENERATED header)
planctl clean src/fragments/

You will rarely run the batch transform by hand. Each project's build.zig invokes it before compiling, so editing a .zsx file is enough. The fragment regenerates itself on the next zig build.

Template syntax

HTML elements look like HTML:

jsx
<div class="container">
  <h1>Hello World</h1>
  <br />
</div>

Expressions live in { ... }. Strings are HTML-escaped automatically.

jsx
<span>{user.name}</span>
<p>Total: {count + 1}</p>

For-loops and conditionals use opening / closing tags:

jsx
{for item in self.items}
  <tr>
    <td>{item.id}</td>
    <td>{item.name}</td>
  </tr>
{/for}

{if self.items.len == 0}
  <p>No items found.</p>
{else}
  <p>{self.items.len} items</p>
{/if}

Attributes can be dynamic:

jsx
<div class={my_var}>
  <button data-id="{item.id}">Click</button>
</div>

PascalCase tags are component calls:

jsx
<Card title="Title">Content</Card>

A worked example

Source (src/features/items/zsx/item_list.zsx):

zig
const std = @import("std");
const Item = @import("../domain/item.zig").Item;

pub const ItemList = struct {
    items: []const Item,

    pub fn render(self: ItemList, out: *std.ArrayList(u8), allocator: std.mem.Allocator) !void {
        return (
            <div class="page">
                <h1>Items</h1>
                {if self.items.len == 0}
                    <p>No items found.</p>
                {else}
                    <table>
                        {for item in self.items}
                            <tr>
                                <td>{item.id}</td>
                                <td>{item.name}</td>
                            </tr>
                        {/for}
                    </table>
                {/if}
            </div>
        );
    }
};

Output (src/features/items/fragments/item_list.zig) is plain Zig with appendSlice for the static HTML chunks and appendValue (with escaping) for the dynamic expressions. The generated file starts with // AUTO-GENERATED by planctl, which is the marker planctl clean looks for.


6. Deploy

Builds artifacts and uploads them to the Workbench for the chosen profile.

bash
planctl deploy --app     --arch <mono|micro> --profile <name>
planctl deploy --service <svc>               --profile <name>
planctl deploy --sse                         --profile <name>
planctl deploy --all     --arch <mono|micro> --profile <name>
TargetAction
--appBuild + upload the shell. Mono ships a WASM hosted by planck/db; micro a native bin.
--service <name>Build + upload one WASM service (micro projects).
--sseBuild + upload only the sse/ subproject. Same builder used by --all.
--allMono: --app + --sse. Micro: --app + every services/* + --sse.

A pre-deploy port validator runs before any upload. It rejects port: 0, duplicate ports inside the project, and cross-app port collisions across the host.

Why --sse exists

When you are iterating on SSE templates or topic logic, you do not want to rebuild and re-upload the WASM app and the static assets every cycle. --sse runs only the sse/ build and uploads only that artifact.

What --all does, in order

Mono:

  1. Build app/ with zig build -Doptimize=ReleaseFast (WASM).
  2. Upload the WASM, db.yaml, app.yaml, and public/ under the app name.
  3. If ./sse/build.zig exists, build and upload the SSE service as <app>_sse.
  4. Workbench restarts the supervised processes.

Micro:

  1. Build and upload the native shell binary + app.yaml + public/.
  2. Iterate app/services/* and run the WASM service deploy on each. A single service failure is logged but does not abort the rest.
  3. Build and upload sse/ last so the topic is alive before browsers connect.
  4. Workbench restarts each updated process.

Flags

FlagMeaning
--arch <m>mono or micro. Defaults to micro for backward compatibility. Set this explicitly.
--profile <n>Required. Picks a profile from ~/.planctl/config.yaml.
--dry-runSkip network calls. Print what would happen.

7. Lifecycle: start, stop, restart, status

Lifecycle commands act on deployed artifacts. Use them after a deploy to bring something back up, or just to peek at health.

bash
planctl start   --app | --service <name> | --sse <app> | --all --profile <p>
planctl stop    --app | --service <name> | --sse <app> | --all --profile <p>
planctl restart --app | --service <name> | --sse <app> | --all --profile <p>
planctl status [--app | --service <name> | --sse <app> | --all] --profile <p>

--sse <app> is just sugar for --service <app>_sse. It matches the naming convention that the deploy step uses for the SSE artifact.

planctl status defaults to --all when no target is given.

Example status output

SERVICE              APP        STATE      PORT     PID      CPU%    RSS(MB)
-------------------- ---------- ---------- -------- -------- ------- --------
product              eshop      running    24006    12345    2.3     45.6
orders               eshop      running    24016    12346    1.1     32.1
eshop_sse            eshop      running    24020    12347    0.5     28.4

8. Undeploy

Removes previously deployed artifacts. The target flags mirror those of deploy.

bash
planctl undeploy --app                     --profile <p> [--force]
planctl undeploy --service <name>          --profile <p> [--force]
planctl undeploy --all                     --profile <p> [--force]
TargetAction
--appDelete the shell app. Services must be removed first.
--service <name>Undeploy one WASM service.
--allUndeploy every service, then delete the app.

--force (or -f) skips the confirmation prompts, which is useful in CI.


9. Backup and restore

planctl backup and planctl restore give you CLI parity with the Workbench UI's create-backup / restore actions. Handy when ops wants to drive things from outside the browser.

Backup

bash
planctl backup --app <name> --profile <p> [--output <dir>]
FlagMeaning
--app <name>App to back up
--output <dir>Local directory to download the archive into. Default: cwd.
--profile <n>Required

The Workbench produces the archive, and planctl downloads the same. Archives are self-contained: data files, WASM, config, and a manifest with integrity hashes.

Restore

There are three modes, picked by the flags you pass.

bash
# App restore
planctl restore --app <name> --backup <path> --profile <p>

# App + single service restore (micro projects)
planctl restore --app <name> --service <svc> --backup <path> --profile <p>

# Whole-system restore (Workbench + sysdb)
planctl restore --system --backup <path>
FlagMeaning
--app <name>Target app to restore
--service <svc>Narrow the restore to a single service inside the app
--systemRestore the Workbench + system DB instead of an app
--backup <path>Path to the archive (required)
--profile <n>Required for app / app-service modes

The Workbench drives the restore. It verifies the manifest hashes, stops the running service, writes new files, then asks the supervisor to start it back up. If any step after stop fails, the service is left stopped so that an operator can inspect, which is by design. There is no auto-rollback.


10. Schema: create and drop stores and indexes

planctl create and planctl drop do data-definition (DDL) against a deployed service's database. They drive the Workbench's /api/schema endpoint, so they run wherever the service is deployed. Writes go to the primary node in the profile, and async replication propagates them to any replicas. You never point these at a replica directly.

bash
planctl create store <store> [--description <d>] [--app <a>] [--service <s>] --profile <p>
planctl drop   store <store>                     [--app <a>] [--service <s>] [--force] --profile <p>

planctl create index <store>.<index> [--type <t>] [--unique] [--field <f>] [--app <a>] [--service <s>] --profile <p>
planctl drop   index <store>.<index>                                        [--app <a>] [--service <s>] [--force] --profile <p>

Targeting a service

A store or index lives in exactly one service, addressed by the slug <app>_<service_name>. For a mono app shop whose db.yaml service is db, that slug is shop_db.

  • Inside the project tree, you can omit --app and --service; they resolve from app.yaml / db.yaml the same way deploy does. For a mono app this resolves to <app>_db.
  • From anywhere else, pass --app <name>. --service still defaults to db, which is correct for mono.
  • Micro apps have multiple named services, so always pass --service <name>.

Stores

bash
# Create a store (optionally with a description)
planctl create store orders --app shop --service orders --description "customer orders" --profile dev

# Drop a store (prompts unless --force)
planctl drop store orders --app shop --service orders --force --profile dev

# Mono, from inside the project tree (resolves to shop_db):
planctl create store orders --profile dev

Indexes

The index name is <store>.<index>, and the indexed field defaults to the last segment of that name. Use --field when the field differs from the index name (for example a nested path, or a custom index name).

bash
# Index name and field are both "status"
planctl create index orders.status --app shop --service orders --profile dev

# A unique float index on "total"
planctl create index orders.total --app shop --service orders --type f64 --unique --profile dev

# Custom index name + nested field path
planctl create index customers.address_city --field Address.City --type string --profile dev

# Drop an index
planctl drop index orders.status --app shop --service orders --force --profile dev

Flags

FlagMeaning
--type <t>Index field type: string (default), i32 / i64 / u32 / u64 / f32 / f64 / bool, or int (alias for i64)
--uniqueMake the index unique (default: non-unique)
--field <name>Indexed field, when it differs from the index name's last segment
--description <d>Optional store description
--app <name>Target app (else resolved from the project tree)
--service <name>Target service name (else resolved from db.yaml, default db)
--force, -fSkip the confirmation prompt on drop
--profile <name>Required

Schema operations on the system database are rejected.

The engine treats a re-created store or index as idempotent, but the Workbench currently surfaces that "already exists" result as a generic error. So re-running create store / create index reports a failure rather than a clean no-op. The store or index is still present.


11. Export and import

planctl export and planctl import move data out of and into a store via a YAML manifest. planctl forwards the manifest to the Workbench's /api/export / /api/import endpoints, and the engine reads and writes the data files itself, on the planck host. The manifest is the only input planctl sends.

bash
planctl export --manifest orders-export.yaml --app shop --service orders --profile dev
planctl import --manifest orders-import.yaml --app shop --service orders --force --profile dev

Targeting works exactly like the schema commands above: inside the project tree --app / --service are optional and resolve to <app>_db for mono; from elsewhere pass --app; micro apps always pass --service <name>.

The manifest

A manifest names one store, a format, and the entity layout. Only the non-string fields need a type; strings pass through as-is.

yaml
store: orders
format: json            # bson | json | csv
# output_dir: /data/exim   # optional; defaults to <base_dir>/exim on the host
# query: orders.filter(status = "shipped")   # optional export filter
entities:
  - name: orders
    role: parent
    file: orders.json
    fields:
      - name: OrderID
        type: int
      - name: Total
        type: double

Files live on the planck host

This is the part that surprises people: exports are written to, and imports read from, a folder on the planck host, and not on the machine running planctl. When the manifest omits output_dir, the engine defaults it to <base_dir>/exim (or the exim_dir set in db.yaml) and creates the folder, the same way backup_dir works. For a large import source, copy the data files into that folder out of band (scp / rsync); planctl only ever transfers the manifest.

Flags

FlagMeaning
--manifest <file>Required. Path to the YAML manifest.
--app <name>Target app (else resolved from the project tree)
--service <name>Target service name (else db)
--force, -fSkip the confirmation prompt on import
--profile <name>Required

12. Service configuration reference

Every Planck service has two YAML files that live next to its build:

  • db.yaml: storage and engine tuning. The planck/db process reads it.
  • service.yaml: identity, the WASM runtime, and the outbound upstream allowlist. The Planck runtime reads it.

planctl new and planctl add generate both with sensible defaults, and you edit them later. The two are independent: changing one does not require you to touch the other.

db.yaml

Storage engine settings. Address, port, TLS, buffers, durability, GC, replication, and change streams.

yaml
address: "0.0.0.0"
port: 24010
primary: true              # true for a primary; false for a replica
base_dir: "/var/lib/planck/product"
backup_dir: "/mnt/backups/product"
max_sessions: 128

tls:
  enabled: true            # Mono apps default to TLS 1.3 self-signed
  cert_file: ""            # Leave empty for auto self-signed
  key_file: ""

session:
  idle_timeout_ms: 604800000  # 7 days

buffers:
  memtable: 16777216         # 16 MB
  vlog: 4194304              # 4 MB
  wal: 262144                # 256 KB

durability:
  enabled: true
  flush_interval_in_ms: 1000
  log_archive:
    enabled: false
    dest_path: ""
    retain_logs_days: 7

file_sizes:
  vlog: 1073741824           # 1 GB segment
  wal: 16777216              # 16 MB segment

gc:
  dead_ratio: 30             # %, when to compact a vlog segment

replica:
  enabled: true              # Set on the primary; points at the replica below
  sync_interval_ms: 5000
  address: "10.0.0.42"       # Replica host
  port: 24011                # Replica's planck/db port

change_streams:
  ring_capacity: 16384
  stores:
    - ns: "orders"
      operations: [insert, update, delete]

Notes on the key fields:

  • primary: true marks this instance as the primary. To ship WAL to a replica, fill in the replica block below with the replica's address and port; nothing is auto-created. On the replica's own db.yaml, set primary: false and leave the replica block disabled.
  • backup_dir is the destination for snapshots. Pick a path on a disk other than base_dir. The Workbench refuses to default backup_dir to anything under base_dir, since that defeats the very point of a backup.
  • gc.dead_ratio controls when a value-log segment gets compacted. The workbench-driven gc task respects this threshold and skips the tail segment so that foreground writes are never contended.
  • durability.log_archive.retain_logs_days controls how long rotated WAL segments are kept around. The workbench-driven wal_truncate task respects this.

service.yaml

Identity, the WASM runtime, and the upstream allowlist for outbound calls from inside WASM.

yaml
name: "product"
description: "Product catalog service"
route: "/product"

wasm:
  enabled: true
  min_instances: 2
  max_instances: 8
  autoscale: true
  http:
    host: "0.0.0.0"
    port: 3010               # You set this; pick a free port for the service
    max_connections: 10000
    max_body_size: 1048576   # 1 MB
    idle_timeout_ms: 30000

upstreams:
  - name: "payments"
    url: "https://payments.example.com"
    timeout_ms: 5000
    max_in_flight: 16
    breaker:
      failure_threshold: 5
      success_threshold: 2
      open_duration_ms: 30000

Notes on the key fields:

  • name is the service identity. It has to match the directory name that the deploy CLI uploads under.
  • wasm.http.port is the port the service listens on. You set it explicitly, since nothing auto-assigns. The pre-deploy validator rejects port: 0, refuses duplicates inside the same project, and refuses ports already taken by another deployed app on the same workbench.
  • wasm.min_instances / wasm.max_instances size the WASM instance pool for concurrent request handling. autoscale: true lets the runtime grow up to max_instances under load.
  • upstreams is an explicit allowlist. WASM cannot reach a URL that is not listed here. Each entry has its own timeout, circuit breaker, and in-flight cap.

Workbench, the control plane

The Workbench (wb) is the control plane for everything planctl does. It owns identity, orchestration, the identity provider, JWT signing keys, and the supervised process tree. When you run planctl deploy, you are asking the control plane to accept a new artifact and reconcile the running fleet against it. planctl is only the client; wb does the real work.

Operational tasks (backup, gc, WAL truncate, stats, export, import, restore) are scheduled and driven from workbench against each planck it supervises. They are not scheduled inside the service itself.


13. Workflow recipes

From zero to deployed in five steps

bash
# 1. One-time host setup (on macOS, prefix system start with sudo)
planctl system init
planctl system start

# 2. New project
planctl new mystore --type hda --arch mono
cd mystore

# 3. Configure a profile in ~/.planctl/config.yaml.
#    See the Configure profiles section.

# 4. Deploy (builds, uploads, restarts)
planctl deploy --all --arch mono --profile dev

Add a feature to a mono project

bash
cd myapp
planctl add billing --type feature
planctl deploy --app --arch mono --profile dev

Add a service to a micro project

bash
cd myapp
planctl add orders --type service --port 4501
planctl deploy --service orders --arch micro --profile dev

Provision and seed a store

After a deploy, create the store and its indexes, then load seed data from a manifest. For a mono app run from the project root, the slug resolves automatically.

bash
cd myapp
planctl create store orders --description "customer orders" --profile dev
planctl create index orders.CustomerID --type i64 --profile dev
planctl create index orders.status     --profile dev

planctl import --manifest app/seed/import.orders.yaml --profile dev

For a micro app, pass --service <name> on each command. Later, dump the store back out:

bash
planctl export --manifest app/seed/export.orders.json.yaml --profile dev

Iterate on the SSE service only

bash
planctl deploy --sse --profile dev
planctl restart --sse myapp --profile dev

Tear down and rebuild

bash
planctl undeploy --all --profile dev --force
planctl deploy --all --arch mono --profile dev

Backup before a risky migration

bash
planctl backup --app product --profile prod --output ./backups

# Run the change.
planctl deploy --service product --arch micro --profile prod

# If it went sideways:
planctl restore --app product --backup ./backups/product-2026-06-13.tar --profile prod

Fresh clone or CI build

bash
git clone git@github.com:yourorg/yourapp.git
cd yourapp
zig build test     # fetches deps and runs tests
planctl deploy --all --arch mono --profile staging

Live-edit ZSX templates

bash
# Terminal 1: watch templates
planctl --watch src/features/tasks/zsx src/features/tasks/fragments

# Terminal 2: rebuild + redeploy on demand
planctl deploy --app --arch mono --profile dev

14. Troubleshooting

planctl: command not found

Add ~/.planck/bin (dev install) or /opt/planck/bin (prod install) to your PATH. In your shell profile:

bash
export PATH="$HOME/.planck/bin:$PATH"

planctl deploy: --profile <name> is required

There is no default profile. Pass --profile <n>, or list one in ~/.planctl/config.yaml and reference it explicitly.

profile '<name>' not found

The CLI prints the available profile list when this happens. Check the name in ~/.planctl/config.yaml; do note that profile names are case-sensitive.

Deploy aborted: PortInUse (or DuplicatePort or PortZero)

The pre-deploy port validator has caught a problem. PortZero means a db.yaml still has port: 0 after resolution. DuplicatePort means two services in the same project want the same port. PortInUse means a different app on the same host is already using that port. Edit the offending db.yaml and re-run.

app.yaml not found

Run planctl from the project root, where app.yaml lives. For micro projects, you can also run from app/services/<name>/; the deploy code knows to look up the tree for the parent app.

zig build failed

Check the Zig compile output. The most common cause on a fresh clone is missing dependencies. Re-run from the project root so that the build.zig can fetch them.

Modules disambiguated to bson0, utils0, etc.

A dep is being constructed twice in your build graph. This is almost always caused by calling b.dependency("foo", .{}).module("foo") somewhere, which asks the dep's own build.zig for a fresh module instance. In deep transitive graphs you end up with multiple instances of the same logical package. The fix is to switch to b.dependency("foo", .{}).path("src/root.zig") + b.createModule(...) + explicit addImport calls.

planctl clean did not remove a file

clean only removes files starting with // AUTO-GENERATED by planctl. Hand-written .zig files in the fragments directory stay put.

Workbench connection refused

Verify that the Workbench is running: curl https://127.0.0.1:24011/api/apps (or whatever your profile's server is). Check the profile's server field and any --server override.

planctl system stop did nothing on macOS

launchctl bootout needs root to touch /Library/LaunchDaemons. Re-run with sudo.

Service did not come back up after restore

This is by design. Restore stops the service, writes new files, then asks the Workbench to start it. If any step after stop fails, the service stays stopped for operator inspection. Check ~/.planck/logs/workbench-*.out.log for the failure, fix the cause, then run planctl start --service <name> --profile <p>.

planctl restore: integrity check failed

The archive's SHA-256 does not match the data / WASM bytes. The backup is corrupt. Do not edit the manifest; pick a different backup, or re-run the original planctl backup.

Backup archive missing after planctl backup

The Workbench writes the archive into its own backup directory and planctl downloads a copy into --output (default: cwd). In case the download landed but you cannot find it, check the working directory you ran from and the printed output path.