Skip to content

feat: micro Supabase stacks phase 1 — @supabase/fleet + stack micro profile#5819

Open
jgoux wants to merge 53 commits into
developfrom
claude/micro-supabase-stacks-spec
Open

feat: micro Supabase stacks phase 1 — @supabase/fleet + stack micro profile#5819
jgoux wants to merge 53 commits into
developfrom
claude/micro-supabase-stacks-spec

Conversation

@jgoux

@jgoux jgoux commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Phase 1 of the micro Supabase stacks design (spec, plan): run 100+ Supabase-compatible Postgres pods on one small machine for agent worktrees, local dev, and eventually free-tier workloads.

@supabase/stack changes

  • Micro Postgres profile (micro.ts + PGDATA conf layering via micro.conf/pod.conf includes): 16MB shared_buffers, jit off, fsync off, wal_level=logical with capped slot retention — settings live in conf files so ALTER SYSTEM still wins.
  • Template-provisioned data dirs (postgres.provisioned): skips the per-boot postgres-init migration pass for data dirs cloned from a golden template.
  • enableExtension(name) — preload-on-enable: appends to shared_preload_libraries in pod.conf and restarts postgres (sub-second with fsync off); serialized per-coordinator with a semaphore. Exposed on StackHandle + daemon HTTP route.
  • lazyServices: start() eagerly starts only postgres; the ApiProxy starts any other service on the first request to its route (memoized, concurrent-safe). ready() scopes to started services.
  • Default postgres bumped to 17.6.1.143.

New package: @supabase/fleet

createFleet() — a host-level pod orchestrator:

  • CoW provisioning: golden base/warm templates (built by running the stack once), cloned per pod via APFS clonefile / reflink in milliseconds.
  • Wake-on-connect / suspend-on-idle: the daemon holds every pod's external TCP port; first connection boots postgres (~100–300ms), idle pods suspend to zero processes. Per-pod lifecycle lock serializes wake/suspend/destroy/reset/fork.
  • forkPod: byte-identical, independently-diverging database branches for agent worktrees.
  • Crash reconciliation: stale postmasters reaped via postmaster.pid process groups; port registry re-seeded from pod manifests.

Measured (gated e2e, Apple Silicon)

  • 100 registered pods: 3 warm postgres ≈ 25MB RSS each, 97 suspended pods at zero processes; clean teardown, no orphans.

Testing

  • Unit/integration suites in both packages (postgres-dependent suites gated behind FLEET_PG_TESTS=1, run under Bun).
  • Density e2e: FLEET_E2E_PODS=100 green (~3min).
  • Repo check:all green (the cli-go:lint-check timeout seen locally is a pre-existing environment artifact; no Go files touched).

Follow-ups (non-blocking, from review)

  • IdleMonitor timers lack unref(); templateKey should use ordinal sort; conf-value escaping; PortRegistry deep state validation; realtime WS lazy-start; shared multi-tenant Realtime (spec'd for a later phase).

🤖 Generated with Claude Code

jgoux and others added 27 commits July 7, 2026 10:12
High-density Postgres + minimal stack design: pod architecture,
micro.conf profile, CoW templates, fleet daemon with suspend-on-idle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds installMicroProfile/readPreloadLibraries/writePreloadLibraries to
layer the micro postgres profile onto an existing PGDATA directory via
include_if_exists lines in postgresql.conf, idempotently.
…f writes

Address code review findings on the PGDATA conf-layering utilities:
- fix TS2532 undefined-narrowing in readPreloadLibraries
- match include_if_exists only when active (not commented out) so a
  disabled include no longer silently short-circuits profile install
- accept flexible spacing/quoting in shared_preload_libraries parsing
  and trim comma-separated entries
- writePreloadLibraries now updates/appends the preload line in place
  instead of clobbering the rest of pod.conf
….6.1.143

Wire postgres.provisioned (skip postgres-init for pre-initialized template
clones) and postgres.profile (skip -c runtime args on "micro", since those
settings live in micro.conf/pod.conf inside PGDATA) through StackBuilder and
the native postgres service. Bump DEFAULT_VERSIONS.postgres to 17.6.1.143.
Adds enableExtension(name) across the coordinator, Stack service,
RemoteStack/DaemonServer HTTP surface, and public StackHandle. When an
extension requires shared_preload_libraries (per PRELOAD_REQUIRED_EXTENSIONS)
and isn't already preloaded, it appends to pod.conf and restarts postgres;
otherwise it's a no-op so plain CREATE EXTENSION keeps working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two concurrent enableExtension calls could both read the same
shared_preload_libraries list, then write independently, silently
dropping one extension when the second write clobbered the first.
Concurrent restarts of the same "postgres" service also deadlocked
the orchestrator. Guard the whole enableExtension body with a
per-coordinator Effect Semaphore(1) so calls are serialized.

Add a regression test that fires two enableExtension calls
concurrently through the real StackLifecycleCoordinator (mocked
binary resolver/spawner, temp PGDATA) and asserts both extensions
land in pod.conf; without the fix the test times out instead of
completing.
Adds StackConfig.lazyServices: when true, StackLifecycleCoordinator.start()
only eagerly starts postgres (and postgres-init); every other service starts
on demand the first time the ApiProxy routes a request to it, via a new
ProxyConfig.ensureService hook memoized by lazyServices.ts so concurrent
first requests trigger a single start.

Deviates from the original sketch's `enabled: false` mechanic: process-compose's
buildGraph excludes disabled ServiceDefs from the dependency graph entirely, so
a lazily-disabled service can never be resurrected via startService/
updateServiceDefinition once the orchestrator is built. Services stay enabled
in the graph; laziness is enforced by skipping the eager start in the
coordinator instead.
StackLifecycleCoordinator.waitAllReady() unconditionally delegated to
orchestrator.waitAllReady() over the full graph startOrder. Under
lazyServices, services that are never started on demand (e.g. postgrest)
never resolve their healthy deferred and never emit a Failed state, so
ready() hung forever.

Track the set of services that have actually been started (eager set from
start(), plus startService/restartService/enableExtension/reload* calls).
Under lazyServices, waitAllReady() now waits per-service on that snapshot
instead of the full graph. Non-lazy behavior is unchanged: it still calls
orchestrator.waitAllReady() over the whole graph.
Adds PodManifest interface plus templateKey/baseTemplateKey helpers that
derive stable sha256-based cache keys from a partial VersionManifest,
independent of key order.
cloneDir() now rm -rf's dest before falling back to fs.cp when the
platform-specific cp -Rc/--reflink step exits non-zero, so a partially
written dest from a failed CoW attempt can't silently mix with fresh
fallback output. The fs.cp fallback also now passes verbatimSymlinks:
true so relative symlinks in src aren't rewritten to absolute paths
that point back into the source tree.

Adds an internal, documented `cowCommand` test seam (CloneDirOptions)
to force the CoW step to fail deterministically in tests, exercising
the fallback-cleanup and symlink-preservation paths without depending
on filesystem-specific CoW failure conditions.
Introduces PortRegistry as the single owner of the port space for all
pods on a host, replacing the plan for a cross-stack filesystem scan
(readReservedPorts()). Persists pod->port allocations to a JSON state
file, is idempotent per pod, and reuses freed ports on release.
- load() no longer crashes on invalid JSON or structurally invalid
  state (missing/NaN basePort, non-object pods); it quarantines the
  bad file to `${stateFile}.corrupt` (overwriting any prior
  quarantine) and starts from fresh empty state instead.
- Add restore(podId, ports) so the fleet daemon can re-seed known
  allocations from each pod's manifest after a quarantine/reset,
  without a full port scan. Idempotent for identical ports; throws on
  conflicting ports for the same pod or ports already held by another
  pod.
- Document the single-owner / no-port-probing / quarantine-and-restore
  design assumptions on the class.
Adds TemplateStore, which builds golden Postgres data-dir templates by
running @supabase/stack one-shot: base = postgres-only boot so
postgres-init applies baseline migrations, then installMicroProfile
freezes the result; warm = clone of base + enabled services booted
once to self-migrate. Builds are concurrency-safe via a per-key
wx-flag lockfile with a 10min stale threshold. Also exports
installMicroProfile/readPreloadLibraries/writePreloadLibraries from
stack's index so fleet can consume them.
PodRegistry persists pod manifests at podsRoot/<id>/pod.json. Provisioner
creates pods by allocating ports and CoW-cloning a base or warm template
into the pod's data dir, resets pods by re-cloning from the base template,
forks a pod's data dir under a fresh id and ports, and destroys pods by
removing their directory and releasing ports.
Provisioner.create() and fork() allocated ports before cloning the
data directory and writing the pod manifest. If cloneDir or pods.write
threw, the allocated port pair stayed recorded in the PortRegistry
state file with no pod ever created to release it. Wrap the
post-allocation steps in try/catch and release the ports (plus
best-effort cleanup of any partially-written data/pod dir) before
rethrowing. Pre-checks (duplicate id, unknown source) still run before
allocation so failure cleanup can never release a pre-existing pod's
ports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds EdgeProxy: binds a pod's external port once and keeps it bound
for the pod's lifetime, awaiting wake() per accepted connection to get
the live upstream before splicing bytes both ways. Emits
connect/data/disconnect activity per pod for the idle monitor to
consume. wake() rejection destroys the client without unhandled
rejections; concurrent connections to the same suspended pod both
succeed regardless of how many times wake() runs (dedup is the fleet
layer's job).

Buffers client bytes behind a real 'data' listener (not just pause())
before replaying them to the backend, since some runtimes start
sockets flowing before user code attaches a listener and would
otherwise silently drop data received while wake() is in flight.
Two review findings in EdgeProxy: (1) a client streaming data at a
suspended pod during a slow wake() buffered without limit; add
MAX_PREWAKE_BUFFER_BYTES (1 MiB) and destroy the client socket if it's
exceeded before the backend is reachable. Fixing this exposed that the
prior data+manual-array buffering never actually delivered data events
while the socket was paused (confirmed under both Bun and Node), so the
buffering mechanism was switched to readable+read(), which fires while
paused and makes byte counting/capping actually work. (2) wake() resolving
with a malformed upstream (e.g. an invalid port) could throw synchronously
inside the .then() resolve handler, surfacing as an unhandled rejection;
wrap that branch in try/catch routed to the same client-destroy path, and
correct the doc comment's overstated guarantee. Also drop the redundant
client.resume() after pipe() and note that the activity data listener is
independent of pipe()'s internal listener.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds IdleMonitor: fires onIdle(podId) once a tracked pod has no open
connections and no activity for idleMs. Open connections hold the pod
warm indefinitely; any recordActivity call (re)arms the countdown;
onIdle fires at most once per warm period and untracks the pod.
Ties together TemplateStore/PodRegistry/PortRegistry/Provisioner with
EdgeProxy/IdleMonitor into createFleet(): pods stay suspended until their
first proxied connection wakes an in-process @supabase/stack StackHandle,
and idle warm pods are suspended again after idleMs with no open
connections. Startup reconciliation kills stale run.pid processes from a
previous daemon and re-seeds PortRegistry from each pod's manifest via
restore().

Also exports PRELOAD_REQUIRED_EXTENSIONS from @supabase/stack's index so
the suspended-pod enableExtension path can guard non-preload extensions
without a restart.
Two race/leak fixes surfaced in review of the Fleet facade:

- suspend() deleted from `warm` before awaiting stack.dispose(), and
  wakeUpstream()/destroyPod/resetPod/forkPod had no way to see an
  in-flight wake (tracked only in wakesInFlight, not yet `warm`) before
  mutating the same pod's data dir. Added a small per-pod async lock
  (PodLock, src/podLock.ts) and routed the wake body, suspend's full
  body, and the lifecycle-affecting sections of destroyPod/resetPod/
  forkPod through it, so these can never interleave against the same
  pod's process/data dir. Wake dedup via wakesInFlight stays outside
  the lock so concurrent connections still share one wake.

- Startup reconciliation killed `-run.pid` (the daemon's own pid), but
  process-compose/createStack spawn postgres `detached: true` in its
  own process group, so that kill missed stale postmasters entirely.
  Reap now keys off postgres's own ground truth,
  `<dataDir>/postmaster.pid` (src/reapStalePostmaster.ts): its first
  line is the postmaster pid, which is always its own process-group
  leader, so `-pid` reliably reaches the whole tree. run.pid is kept
  as a "was running" hint but no longer drives the kill decision.

Also documented the enableExtension asymmetry: a suspended pod can
only durably record intent for preload-required extensions; a
non-preload extension request against a suspended pod is a silent
no-op until the pod is woken.

Adds a gated Fleet integration stress test interleaving suspend/wake/
query calls, plus unit tests for both new modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the density guardrail E2E (register N pods, wake a subset, confirm
the rest stay at zero processes, explicit suspend) gated behind
FLEET_PG_TESTS with pod count tunable via FLEET_E2E_PODS. Mirrors
packages/stack's e2e vitest project (fileParallelism: false) so nx picks
up a test:e2e target for @supabase/fleet, and extends the package's knip
entry points to cover tests/**/*.ts.

Also adds the package README covering the public API, wake/suspend/fork
lifecycle, and phase-1 limitations (native-only, kill-then-resuspend
reconciliation, HTTP/realtime lazy-start gaps).
- Add enableExtension stub to Stack mocks (mocks.ts, running-stack.ts)
  to match the Stack service contract added in this branch.
- Fix StackLifecycleCoordinator.unit.test.ts flakiness under Bun: widen
  the projected-status assertion to tolerate the transient "Restarting"
  state, and bind the fake healthy postgrest server on an OS-assigned
  port (listen(0)) instead of a hard-coded port to avoid EADDRINUSE
  under parallel test runs.
- Remove unused effect and @supabase/process-compose dependencies from
  packages/fleet/package.json (zero imports), reformat with oxfmt, and
  move @tsconfig/bun into knip's ignoreDependencies (false positive —
  used only via tsconfig extends).
- Sync pnpm-lock.yaml after the dependency removal.
@jgoux jgoux requested a review from a team as a code owner July 7, 2026 15:37
Comment thread packages/fleet/src/Fleet.ts Outdated
Comment thread packages/fleet/src/PodRegistry.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 327e836a7c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack/src/StackBuilder.ts
Comment thread packages/stack/src/services/postgres.ts
Comment thread packages/stack/src/services/pooler.ts Outdated
Comment thread packages/stack/src/layers.ts
Comment thread packages/fleet/src/Fleet.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04dd2182fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/fleet/src/IdleMonitor.ts
Comment thread packages/stack/src/RemoteStack.ts
Comment thread packages/fleet/src/Fleet.ts
Comment thread packages/fleet/src/Provisioner.ts Outdated
Comment thread packages/fleet/src/PodRegistry.ts
Comment thread packages/stack/src/DaemonServer.ts
…mon errors

- StackPreparation: mode "native" now fails fast (no silent Docker
  fallback, docker-only services rejected) so fleet pods can't boot
  containers that startup reconciliation can't reap.
- Fleet: idle-triggered suspend re-checks open proxy connections inside
  the pod lock, so clients connecting at the idle boundary keep the pod
  warm instead of being dropped mid-dispose.
- Provisioner.reset: backup-dir cleanup is best-effort after the
  manifest rewrite commits, instead of triggering a rollback that
  desyncs data dir and manifest credentials.
- PodRegistry: manifests missing versions.postgres or a version for an
  enabled service are treated as corrupt instead of waking with
  current default versions.
- DaemonServer/RemoteStack: 500 bodies include `service` exactly for
  ServiceReadyError, so remote clients preserve StackBuildError for
  lifecycle/build failures on service start and extension enable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6162cac610

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/fleet/src/Provisioner.ts Outdated
Comment thread packages/stack/src/services/postgres-init.ts Outdated
jgoux and others added 3 commits July 9, 2026 10:05
…argv

- Pod manifests now record whether the pod was provisioned from a warm
  template; Provisioner.reset re-clones the same template kind instead
  of always downgrading warm pods to the base template (which, under
  lazyServices, silently dropped pre-applied service migrations until
  each service next booted).
- postgres-init reads the target password from the PGPASSWORD env var
  instead of interpolating it into the bash -c script, so non-default
  passwords no longer show up in ps / /proc/<pid>/cmdline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…merge

The fleet workspace only exists on this branch, so the PR merge kept its
lockfile importer pinned to catalog versions that develop's dependabot
bumps had already removed from the packages section — a textually clean
but semantically broken merge that failed `pnpm install --frozen-lockfile`
on CI. Re-resolve the importer against the merged catalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/stack/src/services/pooler.ts Outdated
Comment thread packages/fleet/src/PodRegistry.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fd60f8ef0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack/src/services/postgres-init.ts
Comment thread packages/stack/src/services/postgres-init.ts Outdated
Comment thread packages/stack/src/services/postgres.ts
Comment thread packages/stack/src/StackLifecycleCoordinator.ts Outdated
Comment thread packages/stack/src/services/postgres.ts
- pooler: embed the tenant db_password base64-encoded in the Supavisor
  eval script — a JSON.stringify'd literal is still an Elixir string, so
  password bytes containing #{} would execute as code.
- postgres-init: set the pgpass psql variable via an in-heredoc backtick
  reading the env instead of `-v pgpass=...`, which expanded the secret
  into every psql child's argv; fail fast with a clear message when the
  data dir was initialized with a password that is neither the target
  nor the default, instead of cascading psql auth failures through the
  migration path.
- docker postgres: use `printf '%s'` instead of `echo` in the schema SQL
  \set backticks so passwords like "-n" or ones with backslashes are not
  mangled before role rotation.
- fleet: write pod.json (and its pod dir) with owner-only permissions
  since the manifest holds the pod's postgres password in plaintext.
- coordinator: enableExtension returns before touching PGDATA for
  non-preload extensions, which must be a pure no-op even on stacks
  whose data dir doesn't exist yet.
- builder: reject postgres.profile "micro" on non-provisioned data dirs
  instead of silently running with default settings (the conf overlay is
  only installed when templates are built).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38a078ebfc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack/src/StackLifecycleCoordinator.ts Outdated
Comment thread packages/stack/src/pgconf.ts Outdated
Comment thread packages/fleet/src/Provisioner.ts
Comment thread packages/fleet/src/Fleet.ts
Comment thread packages/fleet/src/Fleet.ts
Comment thread packages/fleet/src/PodRegistry.ts Outdated
… writes

- PodRegistry: validate public/internal manifest ports against their
  disjoint fleet ranges so a parseable-but-corrupt pod.json can't make
  wake() bind postgres on a proxy-owned public port; expose listIds()
  covering directories whose manifest no longer parses.
- Fleet startup: reap stale postmasters across every pod directory on
  disk before pruning port reservations — a corrupt manifest is exactly
  the crash case where a stale postmaster may still hold its ports.
- Provisioner: reject unknown or non-boolean service entries up front
  instead of persisting a manifest the registry's strict parser will
  refuse to read back (turning the pod into an "unknown pod").
- pgconf: all postgresql.conf/micro.conf/pod.conf writes go through a
  temp-file + rename so a crash mid-write can't leave a truncated
  config that fails the next boot or drops preload libraries.
- coordinator: enableExtension maps PGDATA I/O failures to typed
  StackBuildErrors so daemon routes serialize them instead of dying
  with an unstructured 500.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad69f35e46

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/fleet/src/Provisioner.ts Outdated
Comment thread packages/stack/src/StackLifecycleCoordinator.ts
Comment thread packages/process-compose/src/Orchestrator.ts Outdated
…cking

- Provisioner: create/fork treat ANY existing pod directory as occupied
  (PodRegistry.exists), not just ones whose manifest still parses — a
  corrupt pod.json previously let the failed clone's cleanup delete pod
  data the create never made.
- coordinator: startService marks the orchestrator's full dependency
  closure as started, so waitAllReady()'s lazy semantics can't ignore a
  failing dependency that was brought up implicitly (e.g. pgmeta under
  studio).
- orchestrator: restart propagation cuts at user-stopped services
  instead of recursing past them — restarting a dependency no longer
  boots dependents against a backend the user explicitly stopped, while
  completed one-shot helpers (postgres-init) stay transparent so live
  dependents behind them still restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db9ea2ff04

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/fleet/src/PodRegistry.ts
Comment thread packages/fleet/src/PodRegistry.ts Outdated
Comment thread packages/stack/src/services/postgres.ts
…rsing

- Docker services (dockerRunService + the postgres entrypoint path) now
  pass environment values through the docker CLI's own environment with
  name-only `-e KEY` flags, so DB passwords, JWT secrets, and the
  Supavisor tenant script no longer appear in `docker run` argv, which
  any local user can read via ps / /proc/<pid>/cmdline.
- PodRegistry: manifests with duplicate ports across ports/internalPorts
  are treated as corrupt and dropped instead of aborting the whole fleet
  when PortRegistry.reconcile() throws on them at startup.
- PodRegistry.listIds: only directories count as pod ids, so a stray
  regular file matching the id regex can't make startup cleanup fail
  with ENOTDIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/fleet/src/reapStalePostmaster.ts
@jgoux

jgoux commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99e1e8b1b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack/src/StackBuilder.ts
Comment thread packages/stack/src/StackBuilder.ts
Comment thread packages/stack/src/StackLifecycleCoordinator.ts
- auth/postgrest docker services now use name-only `-e KEY` flags with
  values on the service env, matching the other docker services, so DB
  URLs with custom passwords and JWT secrets stay out of `docker run`
  argv.
- postgres.profile "micro" now also requires mode "native": under
  "auto" a missing native binary would fall back to a Docker postgres
  that silently ignores the profile.
- waitReady/serviceReady on a lazy service that was never started now
  fails with a clear StackBuildError instead of hanging forever on a
  health deferred that can only resolve after the first proxied
  request.
- reapStalePostmaster re-verifies the pid still belongs to this data
  dir's postmaster before escalating to SIGKILL — the 5s grace period
  is exactly the window where pid reuse could kill an unrelated
  process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

// Wait for state change via SSE
yield* withUnixHttpClient(
sseStream(socketPath, "/status/stream", (data) => {
const raw = decodeStatusServiceEvent(data);
return toServiceState(raw);
}).pipe(
Stream.filter((s) => s.name === name),
Stream.takeUntil((s) => s.status === "Healthy" || s.status === "Running"),

P2 Badge Route remote waitReady through daemon readiness

Fresh evidence after the coordinator-side lazy guard is that daemon-backed callers still bypass it here: RemoteStack.waitReady() only watches /status/stream. With lazyServices enabled, after start() a never-started service such as postgrest remains Pending and emits no readiness event, so serviceReady("postgrest") on a detached/remote stack can hang instead of returning the StackBuildError that the foreground coordinator now returns; add a daemon per-service readiness endpoint or otherwise reuse the server-side stack.waitReady(name) semantics.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack/src/StackLifecycleCoordinator.ts
Before start() records the eager postgres set, a concurrent lazy
waitAllReady() snapshots an empty started set and vacuously reports
ready — a daemon /ready poll during /start could observe { ok: true }
while postgres is still coming up. Lazy readiness now requires the
"running" phase and fails with a typed StackBuildError otherwise
(mid-start or after stop), which /ready serializes as a 500 for health
pollers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d77e18eb5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack/src/DaemonServer.ts
Comment thread packages/stack/src/ApiProxy.ts
Comment thread packages/process-compose/src/Orchestrator.ts
… helpers on restart

- DaemonServer gains GET /services/:name/ready delegating to
  stack.waitReady, and RemoteStack.waitReady now calls it instead of
  polling /status + SSE itself — daemon-backed callers get the
  coordinator's lazy-service guard (fail fast on never-started
  services) instead of hanging on a state event that never comes.
- Orchestrator restart closures re-run completed one-shot helpers
  (postgres-init) when a live dependent behind them is restarting:
  without that, the dependent's "completed" dependency wait resolves
  against the helper's stale pre-restart deferred and it boots without
  waiting for the restarted dependency to become ready. Helpers with no
  live dependents stay skipped, and user-stopped services still cut
  propagation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11e7249f4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/fleet/src/Fleet.ts
Comment thread packages/fleet/src/PodRegistry.ts Outdated
Comment thread packages/stack/src/StackLifecycleCoordinator.ts
Comment thread packages/fleet/src/Provisioner.ts
…idation

- createFleet acquires a pid-file lock at the fleet root before touching
  any pod state, so a second daemon on the same root fails fast instead
  of reaping the live daemon's warm postmasters and then dying on
  EADDRINUSE. Stale locks from crashed daemons are taken over; dispose
  releases the lock.
- PodRegistry.listIds only treats a MISSING pods root as empty; any
  other scan failure (EACCES, ENOTDIR, I/O) propagates instead of
  letting startup reconciliation free every port reservation while pod
  dirs still exist behind an unreadable root.
- Lazy restartService rejects services that were never started
  (mirroring the waitReady guard): restart never starts the dependency
  closure, so the target would boot waiting on dependencies that stay
  Pending forever.
- Provisioner.create validates version entries like service entries,
  so junk (e.g. numeric versions) can't persist into a pod.json the
  registry's strict parser then refuses to read back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d15a4110e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +66 to +67
if (owner?.trim() === String(process.pid)) {
await unlink(lockPath).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a unique owner token for the fleet lock

Because the lock file stores only process.pid, an old FleetLock in the same process can release a newer fleet's lock: dispose fleet A, create fleet B on the same root, then a second A dispose()/asyncDispose rereads B's lock value (the same PID) and unlinks it. That reopens the root for another fleet and defeats the single-daemon guard before the startup reaping and port-registry writes; write a per-acquisition token and compare that exact token on release.

Useful? React with 👍 / 👎.

Comment on lines +680 to +682
new StackBuildError({
detail: `Cannot restart service "${name}": it has not been started (lazyServices starts it on the first proxied request or via startService()).`,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize lazy restart errors through the daemon

When this new lazy guard fires through a daemon-backed stack (for example restartService("postgrest") before the first proxied request), POST /services/:name/restart only catches ServiceNotFoundError and RemoteStack.restartService does not decode 500 responses. The foreground stack returns a typed StackBuildError, but remote callers get an untyped HTTP defect instead; add the same StackBuildError JSON mapping used by start/ready.

Useful? React with 👍 / 👎.

id: opts.id,
versions: resolvedVersions,
services: opts.services ?? {},
flags: { supautils: opts.flags?.supautils ?? false },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate flags before persisting manifests

When a JavaScript caller passes a non-boolean flag value such as flags: { supautils: "true" }, this expression preserves the string and writes it to pod.json. PodRegistry.parseManifest() later requires flags.supautils to be boolean, so the just-created pod is dropped from listPods() and wake() reports it as unknown; validate or coerce the flags object before writing the manifest, as is now done for services and versions.

Useful? React with 👍 / 👎.


const templates = new TemplateStore(join(root, "templates"), postgresPassword);
const pods = new PodRegistry(join(root, "pods"));
const ports = await PortRegistry.load(join(root, "fleet-state.json"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release the fleet lock on early setup failures

If setup fails here before the later startup-reconciliation try is entered (for example PortRegistry.load() throws while quarantining a corrupt or unwritable fleet-state.json), the already-created fleet.lock is never released. Because the lock records this live process's PID, retries from the same long-running process then fail with "another fleet daemon" until the process exits or the lock is manually removed; wrap the post-acquire setup in a cleanup path that releases the lock on failure.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant