Skip to content

Architecture ​

Parcela turns one checkout's configuration into a supervised environment. It owns identity, process lifecycle, and reservations. A dedicated Caddy instance owns TLS and HTTP routing. The project owns commands, preparation, and data policy.

Ownership ​

ComponentResponsibility
CLIFinds configuration, selects an environment, and renders text or JSON.
RegistryStores projects, environments, sessions, names, and port leases in SQLite.
Environment ownerHolds the ownership lock, runs the process graph, records state, and handles lifecycle requests.
Shared proxy processSupervises Caddy and refreshes routes for running environments.
CaddyTerminates TLS and routes requests using private configuration, storage, and an admin socket.
ProjectDefines commands, dependencies, hooks, and external effects in parcela.yaml.

Startup and readiness ​

parcela up performs these steps:

  1. Find and validate the nearest parcela.yaml.
  2. Register the checkout on first use. A main checkout gets the project URL. A linked jj workspace or Git worktree gets a preview name.
  3. Reserve service hostnames and lease backend ports from 20000 through 29999, including each port a compose service's containers publish. Port leases use one transaction.
  4. Start the shared proxy if needed.
  5. Build the process graph. Start hook dependencies, setup if due, prestart, and the remaining services in dependency order.
  6. Confirm that every HTTP listener belongs to the graph and that its readiness path returns 2xx.
  7. Publish the environment's routes. A detached start returns when its owner reports readiness.

The owner holds owner.lock, serves its control socket, and saves owner.json. Each command runs under bash -c, or sh -c without bash, in a separate process group. Each process writes its own log.

A dependency is ready after its readiness check passes, after it starts if it has no check, or after it exits 0 if it is a hook. Setup enters the graph only until it has succeeded. Ordinary services do not restart automatically. Sharing publishers can restart up to 10 times after failure.

Shutdown and recovery ​

down asks the owner to withdraw routes and stop the graph. Once nothing that depends on a service is running or stopping, the owner runs the service's stop command, if it has one, then sends SIGTERM to its process group. After 10 seconds, remaining groups receive SIGKILL. A stop command runs even when its service already exited, because it manages what lives outside the process tree, like containers. Each success is saved in owner.json, so an owner that adopts a half-stopped run runs only the commands still owed. The run is stopped only when no recorded process is alive and every stop command succeeded. A failed one leaves the run interrupted until down runs it again.

restart validates the replacement configuration before stopping the old run, then starts a new generation. Sharing flags belong to that generation and must be supplied again.

If the owner dies, services may survive. The next lifecycle operation acquires the ownership lock and compares recorded PIDs, start times, and boot identity with live processes. Matching processes are adopted. Parcela never signals a bare PID from disk. A small check-to-signal PID reuse race remains possible.

Proxy and route leases ​

The background parcela proxy run process supervises one Caddy instance. It renews active routes twice per second. Routes expire three seconds after the last renewal, preventing stale routes from remaining active.

If the proxy dies, environment owners attempt to restart it within about two seconds and republish routes. An explicit proxy stop leaves a stopped marker that prevents this recovery. App services keep running while the proxy is stopped.

The proxy does not start at login or reboot. The next up starts it. By default it listens only on 127.0.0.1:1443. A busy listener produces an error naming its owner.

Sharing for one run ​

LAN sharing records the selected interface address with the run. Caddy adds a server on that address and the HTTPS port for the shared environment's .local names. It serves those names only while the interface still holds the address. If the LAN listener cannot load, the proxy retries configuration without it so loopback routes can remain available.

mDNS publishers use dns-sd on macOS or avahi-publish on Linux. Tailscale sharing chooses free HTTPS ports from tailscale serve status and starts one foreground serve process per service. Tailnet requests reach backend ports directly.

Publishers belong to the process graph and stop with the run. Foreground Tailscale serve entries disappear when their owning process exits, including after SIGKILL. Parcela never uses Funnel.

Durable state and sockets ​

The state directory follows this precedence:

  1. --state-dir.
  2. PARCELA_STATE_DIR.
  3. $XDG_STATE_HOME/parcela.
  4. ~/.local/state/parcela.

The directory has mode 700. It contains the following records:

Path under stateContents
registry.sqlite3Projects, registrations, names, and port leases.
proxy/Caddy configuration, frontdoor configuration, logs, certificates, and the CA.
environments/<uuid>/Context and owner records, hook markers, logs, data, temporary files, and cache.

Control and Caddy admin sockets use a hash of the state path under a per-user temporary directory. The base is parcela inside macOS's per-user temporary directory or /tmp/parcela-<uid> on Linux. Socket placement does not depend on environment variables. This keeps processes in agreement and paths within Unix socket length limits.

rm removes registration, not source or runtime files. A configured teardown hook can change external data. Read-only inspection does not register a checkout or start processes.

Source map ​

ModuleResponsibility
configParses format version 1 and validates manifests.
composeRuns a compose service: reads the ports its containers publish from docker compose config on up, writes the environment's Compose override, and forwards a compose.http service's $PORT to its container, so routing still reaches only listeners the graph holds.
registry, portsStores identity and reservations, leases backend ports.
startup, runtimeRegisters a checkout and coordinates startup and readiness.
supervisor, logsRuns the process graph and retains process output.
owner, control, lifecycleOwns environments and handles stop, restart, and recovery.
router, proxy, frontdoor, relayBuilds Caddy configuration and runs the proxy and optional macOS relay.
sharePlans LAN and Tailscale sharing and allowed host variables.
process, httpChecks process identity, listener ownership, and readiness. http is a client.
discoveryReads jj and Git checkout inventory without modifying it.
private, socketsOwns private files, locks, and socket placement.
src/cliParses arguments, selects commands, and renders output.

Privileged changes ​

CA trust changes occur only through parcela trust or a first terminal up, behind the system password prompt. Nonterminal runs never change trust.

parcela proxy install is the only system-service installation path. On macOS, launchd owns loopback port 443 and relays connections through nc as the user. Parcela code does not run as root. Packet-filter redirection is not used.

For contributor commands and live verification, see Contribute to Parcela. For site checks, see Build the documentation site.