carlos / platform Public

--db is passed to every exec-backed instance; a stateless app cannot opt out, and vet does not test for it Issue open

Migrated from github.com/carlosframework/platform issue #94, opened 2026-08-07. GitHub issues are closed for this repo; see AGENTS.md, "Work is tracked on amadan, not GitHub".

Still outstanding: --db is passed to every exec-backed instance with no stateless opt-out, vet never exercises that half, and a wake timeout hides the child's exit reason.

---

Summary

Every exec-backed instance is spawned with --db, whether or not the app has any use for a database. An app that does not declare the flag does not fail loudly — it dies before binding, and the platform reports "instance is waking; retry shortly" for 30 seconds and then forever.

There is no reason a CARLOS app must have a database. A stateless app should be able to run without declaring a flag it will never read.

Current behaviour

internal/activator/backend_exec.go:440:

cmd := exec.Command(bin, "--socket", rt.Socket, "--db", dbPath)

Unconditional. And every route the provisioning pass mints carries a DBPath — internal/provision/reconcile.go:230 sets DBPath: DefaultDBPath(cfg.DataDir, rec.Host), with DefaultDBPath being <dataDir>/<host>.db (provision.go:333). There is no way to express "this app is stateless", so --db is always passed.

The other backend does not pass it: carlos-app@.service runs live/<host> serve and hands the listener over via LISTEN_FDS. So the two backends enforce different contracts on the same binary.

How it fails

Go's flag package rejects an unknown flag, prints usage and exits non-zero — before the app binds anything. The activator then waits out its 30s health timeout on a socket that will never exist:

flag provided but not defined: -db
Usage of /opt/carlos/live/echo.bab.carlos.exciting.io:
  -addr string
  -socket string

activator: wake echo.bab...: instance not healthy within 30s:
  dial unix /run/carlos/echo.bab....sock: connect: no such file or directory

Every request 503s with "instance is waking; retry shortly", and each one burns another 30 seconds. Nothing names the actual cause; the flag error is the child's stderr in the journal, several lines above the wake failure, and reads like unrelated noise.

Why it was not caught

carlos vet — the app-contract checker — execs the binary with -socket <tmp> only (internal/vet/vet.go:45 and :117), never --db. So vet validates the unit-backed contract and the activator enforces a larger one. An app can pass vet, ship, promote, adopt, and still be unable to boot.

This bit testdata/echoapp — the repo's own reference tenant, whose doc comment says it is "a minimal app honoring the platform contract". It honours the half vet tests. Fixed separately so the reference app is spawnable by both backends, but the underlying asymmetry is this issue.

Suggested directions

Not mutually exclusive.

  1. Only pass --db when the route has a DBPath, and let a stateless app have none. The manifest already has a place to say this — the capabilities block carries "data": { "replicate": "app" }, so statelessness is expressible without new vocabulary. Provisioning would omit DBPath for such apps rather than defaulting one in.
  1. Make vet exec with the activator's exact argv (--socket AND --db), so a mismatch is refused at ship time, where a human is standing, rather than surfacing as a 30s hang in production. This seems worth doing regardless of (1): whatever the contract ends up being, the checker and the runtime should agree on it.
  1. Surface the child's exit reason in the wake failure. The activator knows the process exited non-zero and has its stderr; the timeout message could say "child exited: flag provided but not defined: -db" instead of only reporting the socket that never appeared. That turns a 30-second mystery into a one-line diagnosis.

(3) is the cheapest and would have saved the most time here — the information existed, it just was not attached to the failure that got reported.

Related

Found while provisioning an instance through the documented carlos instances enable + carlos instances create flow. Same first-run surface as #79, #81, #83, #85, #86.