Skip to content

fix(server): align serve backend selection - #195

Merged
andrei-hasna merged 6 commits into
mainfrom
task/remove-deployment-modes
Aug 1, 2026
Merged

fix(server): align serve backend selection#195
andrei-hasna merged 6 commits into
mainfrom
task/remove-deployment-modes

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • route the public emails serve command through the server storage-backend resolver
  • preserve unrelated mcp, remove, root-help, and unknown-command registration when a client API is configured without a server database URL
  • exercise the shipped CLI with only EMAILS_DATABASE_URL configured

Why

PR #160 removed the server deployment-mode axis, but its public emails serve entrypoint still selected the SQLite dashboard through the retired client mode. PR #160 was merged before the adversarial review comment identifying that P1 was posted, so this follow-up carries the tested remedy.

Verification

  • bun run test — exit 0; 4282 pass, 156 skip, 0 fail across 288 files (4438 total)
  • staged credential scan — exit 0
  • PostgreSQL URL only: emails serve --help selects the self-hosted /v1 service and port 8080
  • no database URL: emails serve --help retains the local dashboard and port 3900
  • client API configuration without a server database URL: unrelated mcp --claude --dry-run --json remains available

The repository declares no separate typecheck script.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

`emails-serve` no longer has a mode. It has an internal store, and the store
follows `EMAILS_DATABASE_URL` alone: set means the operator-owned PostgreSQL
`/v1` API, unset means the local SQLite dashboard.

WHY THIS HALF FIRST. The deployment word had two independent definitions of
"am I the second arm?", one per shipped binary, and they meant opposite
things. In the `emails` CLI `self_hosted` means "become an HTTP client of
somebody else's server"; in `emails-serve` it meant "become a PostgreSQL
server". One variable, two contradictory semantics, so a deployment that set
it for one binary silently reconfigured the other. Storage configuration
cannot contradict itself that way.

The word is REFUSED, not ignored. Deleting a variable's last reader and
leaving the variable accepted removes the word and keeps the hole: the next
operator sets it on a PostgreSQL deployment, nothing reads it, nothing
complains, and they believe they configured something. The refusal names the
setting to delete and the setting to use, and quotes no value, because
`EMAILS_DATABASE_URL` carries a password.

Fixes a latent defect found on the way: the container HEALTHCHECK keyed on
the deployment word and read its ABSENCE as the PostgreSQL arm (`/ready`),
while `src/server/index.ts` read the same absence as the SQLite arm, which
serves no `/ready`. A container started without the word ran the dashboard
and was probed for a route it does not have — permanently unhealthy, with no
configuration error to explain it. Both now read one setting.

The ECS task definition stops setting the variable in the same change, so the
image and the environment that must agree move in one task-definition
revision.

Ratchet re-pinned to the measured tree, zero slack on all eleven:
isSelfHostedMode 38 -> 35, resolveEmailsMode 65 -> 64, mode-env 219 -> 209.
The sixteen client families still read the word; none is collapsed here.

Agent: Silvanus
…at the backend

An unused exported type alias keeps the retired vocabulary reachable for the
next module that imports it. Nothing outside bind-options.ts referenced it, so
it goes rather than being redirected.

Agent: Silvanus
The full-suite comparison against a pristine origin/main worktree caught this:
the first version refused the retired deployment setting outright, and that
aborted the branch suite at src/cli/commands/inbox-explain.test.ts. The cause
was not that test — it was the harness change that came with the refusal.

WHY A FLAT REFUSAL IS WRONG WHILE THE CLIENT HALF IS LIVE. Sixteen `emails`
CLI families still route on this word, so one shell legitimately exports it
for the client and runs the server from the same place. Three instances of
exactly that shape exist in this repository today: the hermetic harness
exports it for every test and several of those tests spawn `emails-serve`
with the inherited environment; the container runtime smoke did the same; and
docs/SELF_HOSTED_RUNTIME.md shows a client block and a service block an
operator would paste into one shell. Refusing breaks all three — which is not
failing closed, it is failing on a configuration that works.

So the server now: refuses a value that never selected anything here; refuses
a value that CONTRADICTS the storage configuration, naming both settings and
no value; and tolerates a value that AGREES while announcing, once per
process, that it is ignored and that EMAILS_DATABASE_URL is what decides. An
unannounced ignore would be the same hole with a smaller symptom. The
tolerance is deleted with the client families.

This also reverts the harness edit entirely: scripts/run-hermetic-tests.sh and
scripts/prepublish-local-test.mjs are byte-identical to main again, so this PR
no longer changes what any other suite runs under. The one place that still
needs the setting gone from a child environment — the startup-contract case
that configures PostgreSQL, where an inherited local value is a genuine
contradiction — deletes it by ROLE through the owning module's constant.

Ratchet re-measured on the corrected tree, zero slack: mode-env 219 -> 206
(fifteen spellings out, two back in for the exported retired-settings list).
The new suite contributes zero to that counter: written naturally it spelled
the variable nine times, and it now reads the name by role instead.

Agent: Silvanus
Route the public serve command through the server storage resolver and cover the shipped CLI path.

Agent: unresolved-account003
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #195 @ 51a042e — lens: correctness+security+gates, reviewer unresolved-account003 (1 of 1)

Focused remediation review:

  • Re-read the exact two-file diff against current origin/main and the surrounding CLI router, local/remote serve registrars, storage-backend resolver, and client store-resolution paths.
  • The regression was red before the implementation: bun run test exited 1 with 4281 pass, 156 skip, 1 fail; the new shipped-CLI case selected the SQLite dashboard under EMAILS_DATABASE_URL.
  • An initial eager resolver fix exposed five direct regressions in unrelated command registration; those were corrected by resolving the server backend only for an actual serve invocation.
  • Functional controls now select the PostgreSQL /v1 service and port 8080 with only EMAILS_DATABASE_URL, retain the SQLite dashboard and port 3900 without it, and keep client-only mcp --claude --dry-run --json registration working without a server database URL.

Gates:

  • bun run test — exit 0; 4282 pass, 156 skip, 0 fail across 288 files (4438 total tests).
  • bun run secrets:staged — exit 0 before commit; the pre-push credential scan also passed.
  • git diff --check — exit 0.
  • The repository declares no separate typecheck script; none was invented or run.

Blocking P0/P1 findings: none. The named wrong-store P1 is fixed, and its direct registration regressions are covered by the declared full test gate.

Non-blocking follow-ups: none.

@andrei-hasna
andrei-hasna merged commit d3ece11 into main Aug 1, 2026
5 checks passed
@andrei-hasna
andrei-hasna deleted the task/remove-deployment-modes branch August 1, 2026 18:54
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