Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .changeset/serve-node-env-production-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
---
"@objectstack/cli": minor
---

fix(cli): `os serve` defaults `NODE_ENV` to `production` when unset, exactly as `os start` already does (#11113)

**BREAKING for a deployment that runs `os serve` with `NODE_ENV` unset and
relies on a development-class convenience surviving into a real boot.**
Shipped as `minor` under the repo's launch-window convention for breaking
changes, not `patch` — this is a deliberate default flip, not a bugfix that
restores previously-intended behaviour.

`os start` has forced `NODE_ENV='production'` on the unset case since #5673,
but it does so on the child environment it assembles for its **spawn**
(`start.ts:347`). `os serve` runs **in-process** — there was no equivalent
write, so the whole family of `NODE_ENV !== 'production'` gates across the
tree read the raw `undefined` and took the non-production branch on a boot
that never declared itself anything else. Filed as #11113, the declared
residual of #10366 (which closed the same gate's *set-but-wrong* case and
left this one for its own card, per the disposition precedent on #11035).

One line: `serve.ts` now defaults `process.env.NODE_ENV` to `'production'`
when unset, at the same point it already defaults it to `'development'` under
`--dev` — before any of the runtime modules it dynamically imports, and before
every gate downstream reads the variable. An explicitly-set `NODE_ENV`
(`development`, `test`, anything else) is never overridden.

The full behaviour-flip survey — every `NODE_ENV`-reading predicate in the
tree, which ones flip and which don't, and why — is in the PR body (#11113),
not repeated here. Highlights of what an unset-`NODE_ENV` `os serve` boot now
gets, that it did not before:

- plugin-auth's localhost trusted-origin CSRF substitution closes (the
regression this card pins).
- plugin-auth's CSRF Origin/Referer synthesis for headerless requests closes.
- plugin-auth's missing-`OS_AUTH_SECRET` fallback to a forgeable
`dev-secret-<timestamp>` becomes a refusal to boot instead.
- plugin-auth stops printing invitation / magic-link URLs and OTP codes to
logs.
- plugin-dev's ADR-0115 D6 boot guard now refuses to initialize the dev
assembly (well-known auth secret, seeded dev admin) instead of loading it.
- the SQL driver's auto-DDL guard stops silently applying `safe` schema drift.
- the seed loader stops seeding dev-scoped datasets into what it previously
could not tell apart from production.
- service-settings' local crypto provider now requires a stable key instead
of tolerating an auto-generated / ephemeral one.

Every one of those is the intended tightening this card exists to make: an
operator (or an AI-authored deploy script) that never exported `NODE_ENV` is
running a real deployment, and the safe direction is to treat it as one, loud
failures included, rather than silently keep a development-class door open.
`NODE_ENV=development` / `NODE_ENV=test` — including the flows `os dev` and
`os serve --dev` already carry — are unaffected; only the unset case moves.

<!-- adr-0087: not-required (no-migration-prescription) — this changes a CLI
runtime DEFAULT, not an authorable metadata contract. There is no
`packages/spec` schema key, no declared shape, and no spelling for
`objectstack migrate meta` to rewrite: an operator's config and metadata are
byte-identical before and after. The only actionable step for anyone who is
relying on the previous default is operational (set `NODE_ENV` explicitly),
not a metadata migration, so there is nothing for the ADR-0087 ledger to
register. -->
23 changes: 21 additions & 2 deletions packages/cli/src/commands/serve.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1095,8 +1095,27 @@ export default class Serve extends Command {
// time) see development mode. We deliberately do NOT inherit
// NODE_ENV from the parent `os dev` spawn — see the note in
// commands/dev.ts for why.
if (flags.dev && !process.env.NODE_ENV) {
process.env.NODE_ENV = 'development';
//
// The `else` branch is `os serve`'s side of #11113. `os start` already
// defaults NODE_ENV to 'production' on the unset case, but it does so on
// `localEnv` — a child environment assembled for a SPAWN (start.ts:347).
// `serve` runs in-process, so there is no child env to default; the
// equivalent has to mutate `process.env.NODE_ENV` itself. It has to
// happen HERE, at the same point the --dev branch above already sets it,
// and for the identical reason: every `await import(...)` below, and
// every `NODE_ENV !== 'production'` (or equivalent) gate downstream —
// plugin-auth's localhost trusted-origin CSRF substitution, its Origin
// synthesis, its auth-secret fallback and OTP-deliverability check;
// plugin-dev's production boot guard; the SQL driver's auto-DDL guard;
// service-settings' crypto-key mode; the seed loader's env scoping — must
// observe the default, not the raw unset value. Full survey in #11113.
// An operator who never exported NODE_ENV is booting a real deployment,
// not asking to be treated as development — this is `os serve` agreeing
// with `os start` on that, one line, for the whole gate family at once.
if (flags.dev) {
if (!process.env.NODE_ENV) process.env.NODE_ENV = 'development';
} else if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'production';
}

const requestedPort = parseInt(flags.port);
Expand Down
Loading
Loading