Operating model
Lifecycle
Operate an environment through explicit steps and retain data only for as long as you need it.
Reviewed 2026-07-25
The lifecycle is split into local validation, optional data preparation, compute, access, and shutdown. The Taskfile encapsulates the operational logic; do not reproduce its Pulumi commands by hand.
Two entrypoints
Operator commands on this page use the chalupa CLI. It runs from any
directory, needs no Task installation on the operator's machine, and delegates
each action to the exact public Taskfile command described here. Install it
once from a checkout with bun link; see
Local CLI and operator UI.
Repository work keeps using task from the checkout: task setup,
task typecheck, task test, task validate, task preview,
task ci-preview, task report, task cloud:*, the teardown runner and
schedule tasks, and task data-nuke. Those are development, release, or
deliberately unexposed operations rather than the per-environment lifecycle.
At a glance
| Stage | Command | Changes the cloud | Effect |
|---|---|---|---|
| Validate | task validate CONFIG=<path> |
no | Checks types, tests, inferred Compose, and ports. |
| Prepare data | chalupa data-up --config <path> |
yes | Creates the persistent volume. |
| Launch | chalupa up --config <path> |
yes | Creates ephemeral compute. |
| Connect | chalupa tunnel --config <path> |
no | Keeps local SSH tunnels open. |
| Shell | chalupa ssh --config <path> |
no | Opens an interactive session on the droplet. |
| Seed | chalupa seed --config <path> |
changes data | Runs the seed through the tunnel. |
| Inspect | chalupa status --config <path> |
read-only | Reads exact local compute and protected-data stack state. |
| Sink | chalupa down --config <path> |
yes | Destroys compute only. |
| Delete data | task data-nuke CONFIG=<path> |
yes, irreversible | Destroys the persistent volume. |
Validation stays a task command because it runs the checkout's own
type checks and test suite; chalupa validate --config <path> delegates to
exactly the same gate when you prefer one entrypoint.
The local CLI and operator UI presents this model and
delegates the public actions in the table. The Taskfile remains authoritative:
the CLI neither reconstructs the Pulumi commands nor exposes data-nuke.
Preview
The preview runs the same inference as the Pulumi program without creating resources:
cd "$CHALUPA_HOME"
task validate
Without CONFIG, only task preview and task validate use the repository's
synthetic demo. Set DEMO_CONFIG and pass it explicitly to validate a project:
export DEMO_CONFIG="$PWD/chalupa.yml"
task validate CONFIG="$DEMO_CONFIG"
Stop if a service is missing, an unexpected port appears, or TypeScript fails.
Every provider-changing task refuses a missing CONFIG; it never promotes the
synthetic preview default into a billable or destructive operation.
Prepare the data stack
This step applies only when chalupa.yml declares persist:
chalupa data-up --config "$DEMO_CONFIG"
Run it once per volume. The data stack has a separate name so destroying compute does not include it.
Launch
chalupa up creates billable resources:
chalupa up --config "$DEMO_CONFIG"
When cloud.url is configured, the task first validates that the ingest-only
profile is available so a credential error occurs before resource creation.
It then loads only the provisioning credential into Pulumi, selects the local
backend, creates the stack, and delivers agent cloud-init without the ingest
key. After provisioning, a separate ingest-only subprocess streams the HMAC
key over SSH stdin and installs it atomically as a root-only file. Pulumi and
DigitalOcean user data never receive that key. Without private-log capture,
/etc/chalupa/ingest.key is owned by root:root with mode 0600. A
logs-enabled deployment instead uses root:chalupa-logs with mode 0640 and
makes /etc/chalupa root:chalupa-logs mode 0750, granting only the isolated
collector the access required to sign uploads.
When observability.monitor is configured, cloud-init downloads only the
exact declared release archive and verifies the digest for the droplet
architecture before installation. The performance adapter starts only after
the ingest key arrives, then starts the separate network-denied producer.
A checksum mismatch or producer isolation failure fails closed rather than
falling back to another version, source, or less restricted execution mode.
chalupa up can finish before the containers and health check are ready.
Confirm readiness with the first heartbeat and through the tunnel.
If SSH key delivery fails after provisioning, the task exits non-zero and warns that compute may still be billable. It never destroys the environment as an implicit rollback. Correct the same OpenSSH access used for the tunnel and retry without provisioning again:
chalupa agent-bootstrap --config "$DEMO_CONFIG"
The retry fails closed if the current config's cloud.url differs from the
origin recorded by the provisioned stack. In that case, run the normal
validated chalupa up flow so Pulumi can review and apply the configuration
change before any credential is delivered.
Connect and work
Keep the tunnel open in one terminal:
chalupa tunnel --config "$DEMO_CONFIG"
In another terminal, you can run:
chalupa seed --config "$DEMO_CONFIG"
bun run test:e2e
For an interactive shell on the host itself:
chalupa ssh --config "$DEMO_CONFIG"
That task resolves the droplet address from Pulumi output, so operating an environment never requires reading or pasting its IP.
Connections arrive at localhost; the SSH process is the access boundary.
Closing the tunnel does not destroy the environment. Chalupa can show an
observed idle window to remind you to sink it.
Sink
When the work is complete:
chalupa down --config "$DEMO_CONFIG"
When compute was created with a dedicated identity, export the same
SSH_IDENTITY=/absolute/path/to/private-key before down. The destroy still
proceeds if a best-effort SSH flush fails, but using the correct identity gives
the final log and Monitor windows their bounded delivery opportunity.
For an additional local identity check, the operator UI can require a compute
provider ID refreshed within two minutes and pass it to the same Taskfile
destroy path. A direct chalupa down or task down is not
provider-ID-bound; the CLI target summary states that limitation. See
Local CLI and operator UI.
This destroys the droplet and stops its estimated compute cost. The persistent volume remains intact and can continue to incur estimated storage cost.
If the environment uses the control plane, the task captures its provider ID before the destroy. If Monitor is enabled, it first requests a partial-window flush over SSH and waits at most 30 seconds for the producer and adapter's bounded spool to drain. Failure emits a warning but never keeps billable compute alive. The adapter resumes collection after the attempt, so a later provider failure does not leave a surviving droplet unobserved.
The provider-only subprocess then writes non-secret outputs to a private,
short-lived local receipt and exits. Only after a successful destroy does a
separate ingest-only subprocess send the signed, idempotent sunk event. A
reporting failure does not undo the destruction: the task emits a warning.
Reconciliation may recover the state only when Chalupa already knows the
deployment; a short-lived cycle missed between inventory polls is not
observable. Verify the state manually and treat the provider invoice as
authoritative. Chalupa retains events, suite runs, and downsampled performance
windows so the resource's absence does not erase operational context.
Idle window
With idleShutdownMinutes, the agent reports idle checks and the control plane
derives an informational window. Use the warning as a prompt to run
chalupa down.
Optional local expiry runner
session.expiresAfterMinutes is a deadline for a demo or test deployment, not
an instruction for Vercel or the droplet to delete infrastructure. Those
systems never receive a DigitalOcean write credential, and Vercel cannot use
the operator's local Pulumi state. An operator may instead enable a local
runner on the machine that owns that state.
Registration is explicit and happens only after the exact compute stack exists:
chalupa teardown register --config "$DEMO_CONFIG"
Namespaced actions keep their exact Taskfile name in the allowlist; the CLI
accepts the group and the member as two words, so chalupa teardown register
delegates to task teardown:register.
The runner stores an operator-private local record containing the canonical config
path, SHA-256 of that config, the compute stack name, its current provider ID,
and its deadline. It rejects names ending in -data, never enumerates provider
resources, and never receives a data-stack name. Before acting, it rechecks the
canonical config path, stack name and provider ID. An expired registration
still tears down after YAML edits when the path and stack name match; the log
notes the changed file. The deadline was authorized at registration, so editing
the file cannot cancel it. ci.onFinish still requires the exact registered
file digest. A changed path or stack name, or an unreadable config, blocks
teardown and keeps the registration for recovery. A newer deployment is never
destroyed by the old registration; that stale registration is removed.
Review due work without changing anything:
task teardown:run
Only an explicitly armed local command can destroy compute:
task teardown:run-armed TEARDOWN_ENABLED=1
That command delegates to the normal task down path. The task's provider
wrapper acquires the DigitalOcean credential only in its short-lived local
subprocess, checks the registered provider ID immediately before destruction,
and leaves <stack>-data untouched. It also preserves the normal bounded log
and telemetry flush plus signed sunk event behavior.
On macOS, Chalupa can install a user-level launchd job after you have reviewed the dry run. First start a same-user TinyVault agent that remains unlocked for the whole scheduled horizon. The agent runs in the foreground, so keep its terminal or trusted supervisor alive:
tvault agent start --idle 0
Then, in another terminal:
task teardown:run
task teardown:schedule:install \
SSH_IDENTITY="$HOME/.ssh/id_ed25519_chalupa"
task teardown:schedule:status
The default interval is five minutes. To choose another bounded interval:
task teardown:schedule:install TEARDOWN_INTERVAL_SECONDS=600
Installation is explicit and local. Before writing anything, the installer
requires the same-user agent to report idle_remaining_seconds: 0 and proves
that it can read exactly DIGITALOCEAN_API_TOKEN from the direct chalupa
project without prompts. That probe discards both credential output and
diagnostics and does not inherit TVAULT_PASSPHRASE, TVAULT_IDENTITY,
TVAULT_IDENTITY_KEY, or TVAULT_AGENT_TOKEN. The generated job contains no
TinyVault unlock material, private-key material, provider credential, or target
stack.
The job records the absolute Task binary, this checkout, the current executable
search path, and the already resolved TEARDOWN_STATE_DIR—whether the path was
explicit, XDG-derived, or the default. When provided, it also retains only the
validated local SSH_IDENTITY path so a non-interactive final flush can use the
dedicated key. At each interval it runs the armed Taskfile path, which still
rechecks every registration and obtains a provider credential only if an exact,
expired compute deployment remains eligible. The installer refuses to replace
a different job at the same path.
Inspect schedule.stdout.log and schedule.stderr.log in the resolved state
directory after installation and after the next deadline. That is the explicit
TEARDOWN_STATE_DIR, $XDG_STATE_HOME/chalupa/teardown, or
~/.local/state/chalupa/teardown, in that order. The schedule runs only while
that user's launchd domain is available, so it is a best-effort cost guard, not
a real-time destruction guarantee. The TinyVault agent must also remain alive;
if it exits, a due run cannot acquire the credential, no remote shutdown flush
starts, and the scheduler logs the failure for operator recovery. Once the
credential is acquired, log and Monitor flushes run in separate subprocesses
that cannot inherit it; the provider-bound parent rechecks compute identity and
performs the destroy. Remove and reinstall the schedule to change its interval,
state directory, TinyVault directory, checkout, Task/Bun path, or SSH
identity—the installed job pins all of those values.
Disable future scheduled runs without deleting registrations or logs:
task teardown:schedule:remove
Removal disables the label before inspecting it. If launchd still reports a
running PID or state, Chalupa leaves the job file in place and asks you to retry
after that run exits; only an idle job is unloaded and removed. --idle 0
keeps TinyVault's key-encryption key in memory for the life of the agent, and
other same-user processes that can reach its protected socket share that trust
boundary. Use it only on the trusted operator machine, and stop it after the
schedule is removed and task teardown:run confirms that no registered compute
remains:
tvault agent stop
Linux and other platforms continue to require an operator-owned user timer or
cron entry that invokes
task teardown:run-armed TEARDOWN_ENABLED=1 from this checkout. Keep it on the
same trusted machine that owns the local Pulumi backend and preserve the same
resolved state-directory setting. That timer also needs an equivalently
long-lived, same-user, non-interactive TinyVault agent; its process lifetime and
the timer service availability impose the same best-effort limit. The registry
can be retained for audit or removed only after confirming no registered
compute remains.
Delete data
task data-nuke is an independent action:
task data-nuke \
CONFIG="$DEMO_CONFIG" \
CONFIRM_DATA_NUKE=demo-harbor
It deletes the block storage volume. It is not part of the normal shutdown
path and must not be used merely to “clean up” an environment. Confirm the
backup, target, and stack name before you run it. The confirmation value must
exactly match the name in chalupa.yml; otherwise the task refuses to
unprotect or destroy the data stack.
Expiry and billing
Extending a running session is an explicit lifecycle act:
chalupa session extend --hours 2 --config ./chalupa.yml
# Non-interactive authorization:
chalupa session extend --hours 2 --config ./chalupa.yml --confirm "extend <environment>"
Before confirmation, Chalupa prints the new UTC deadline and the estimated added
provider cost using the running host's hourly rate (or says the cost is unknown).
The provider invoice wins. Extending is never a Chalupa charge.
Each extension adds 1–8 whole hours to the current deadline, with at most two
extensions per registration. Total lifetime, counted from original registration,
is capped at 12 hours for inference and 24 hours otherwise. Expired sessions cannot
be extended. The local registration and the console's session clock both receive
the new deadline through a signed session-extended lifecycle event.
If console delivery fails, the local extension still stands, but the console will
stop the host at the original deadline unless reporting is retried. Run the printed
retry command; it resends the same deadline without consuming another extension.
Idle shutdown and ci.onFinish remain separate stop conditions.