---
name: run-local
description: "The warm Docker-free dev loop, `mxcli run --local` — model changes live in about a second, with watch, screenshots, metrics, tracing and external preview. Use for the fastest edit-to-running-app cycle, especially when driving the model programmatically with MDL."
---

# Warm Local Dev Loop — `mxcli run --local`

## Overview

`mxcli run --local` runs a Mendix app **without Docker**, keeping a
`mxbuild --serve` process and a standalone runtime hot so model changes apply in
~1 s instead of a ~30–60 s rebuild-and-restart. Use it as the fast inner loop when
iterating on an app; use `mxcli docker run` when you need the fully-rendered browser
client (see the limitation below).

## When to Use This Skill

Use this when:
- You want the fastest edit → running-app loop for a Mendix 11.x project.
- You're driving the model programmatically (`mxcli exec`/MDL) and want each change
  live immediately.
- You're iterating on **page design** (the app renders in a real browser) or doing
  runtime/model/API/headless verification.

Prefer `mxcli docker run` when:
- The project is Mendix 9/10 (JDK 11/17 — not yet supported by `run --local`).
- You want a container-parity deployment rather than a standalone runtime.

## Usage

```bash
# boot once and keep serving (Ctrl-C to stop)
mxcli run --local -p app.mpr

# boot and hot-apply on every project change
mxcli run --local -p app.mpr --watch
```

## How apply is chosen

Every warm rebuild reports whether a restart is required; `run --local` applies the
cheapest action automatically:

| Change | Apply | Cost |
|--------|-------|------|
| page / microflow / nanoflow / text | hot `reload_model` (no restart) | ~1 s |
| entity / view entity / association | runtime restart + DDL | ~9 s |

Structural changes need a restart because the runtime reconciles its entity/
association catalog only at startup; behavioural changes are hot-reloaded.

## Prerequisites

- **Mendix 11.x** project (runtime launches under **JDK 21**).
- A **PostgreSQL** database (defaults: `127.0.0.1:5432`, user `mendix`, db derived
  from the project name; override with `--db-host/--db-name/--db-user/--db-password`).
  - **`--ensure-db`** provisions it for a fresh session: starts local Postgres if the
    port is down and creates the role + database if missing. It uses a service
    manager, or a user-owned `initdb`/`pg_ctl` cluster under `~/.mxcli/postgres`
    when no service becomes ready (e.g. Arch) — needing no `postgres` OS account or `sudo`.
    Remote hosts are only checked, not provisioned.
    The user-owned cluster persists across sessions; its server log is
    `~/.mxcli/postgres/server.log`. Stop it with
    `pg_ctl -D "$HOME/.mxcli/postgres/data" stop`. To remove it, stop it first and
    then delete `~/.mxcli/postgres` (this permanently deletes its databases).
  - Without `--ensure-db`, create it once and the command errors if it's unreachable:

    ```bash
    createdb -h 127.0.0.1 -U mendix "$(basename app.mpr .mpr | tr '[:upper:]' '[:lower:]')"
    ```

### Which mxbuild the loop uses

- **Linux** — the CDN download cached at `~/.mxcli/mxbuild/<version>/`, as before.
- **macOS / Windows** — **Studio Pro's bundled mxbuild**, resolved before the cache.
  The Mendix CDN publishes **Linux archives only** (the URL varies by architecture,
  not by OS), so a cached download on a Mac is a Linux `aarch64` ELF — the arch
  matches, which is why it looks fine until exec.
- `--mxbuild-path` overrides both, and is now honoured by the local loop (it used
  to be documented and ignored — #916).

If nothing runnable is found, the command says so up front instead of failing with
`fork/exec …: exec format error`:

```
mxbuild from the Mendix CDN is a Linux binary and cannot run natively on darwin
  Install Mendix Studio Pro 11.12.0 and use its bundled mx …
  Or point mxcli at it explicitly with --mxbuild-path.
```

### Windows and macOS toolchain

- **JDK 21** — Mendix Studio Pro does not bundle one; its installer puts **Eclipse
  Temurin JDK 21** in the usual place, which is where mxcli looks (`Eclipse
  Adoptium` / `Java` / `Microsoft` under both Program Files, plus the per-user
  `%LOCALAPPDATA%\Programs\…` installs winget produces). `JAVA_HOME` wins over all
  of them. When nothing is found the error now lists every location it searched.
- **Gradle** — bundled inside mxbuild (`modeler/tools/gradle`) and invoked by
  mxbuild, not by mxcli. Studio Pro extracts its own copy to the parent of its
  install directory (usually `C:\Program Files\Mendix`). A "Gradle not found"
  from a local run therefore points at an incomplete or foreign mxbuild bundle —
  check which mxbuild was resolved before looking for a system Gradle.

## The intended loop

```bash
# terminal 1: keep the app hot
mxcli run --local -p app.mpr --watch

# terminal 2 (or an agent): edit the model — the change hot-applies automatically
mxcli exec add-page.mdl -p app.mpr
```

`--watch` observes two source trees and rebuilds when either changes: the **model
source** (`.mpr` + `mprcontents/`, v1 and v2 layouts) and the **theme source**
(`theme/` and `themesource/<module>/web/` — SCSS/CSS/JS). It ignores build output
(`deployment/`, `theme-cache/`, `.mendix-cache/`, `.mxcli/`). Both signals are mtime
polling, so they work on container filesystems where inotify is silent. Each apply is
logged with a build-generation counter (`build #2`, …) so you can confirm a change
landed.

## Editing themes (SCSS) — rebuild, never clear caches

A theme edit (`theme/web/main.scss`, module SCSS) needs a **rebuild**, not a
cache-clear. With `--watch`, just save the `.scss` — the theme source is watched and
the loop rebuilds and hot-applies. Without `--watch`, nothing is watched, so the save
does nothing until you restart `run --local` (or re-run with `--watch`). `mxbuild
--serve` recompiles the theme on its next build and correctly picks up SCSS content
changes, so **never** `rm -rf theme-cache/ .mendix-cache/ deployment/` — clearing
caches is a red herring.

A **theme compile error fails the build step, before the runtime starts** — the
error text is in the serve `/build` output (printed by `run --local`), not a
runtime problem. A common Dart Sass trap: `rgba()` needs a real color, so a
comma-list variable is rejected —

```scss
// WRONG: Dart Sass reports "$color: 168, 50, 30 is not a color"
$cf-over: 168, 50, 30;
background-color: rgba($cf-over, 0.1);

// RIGHT: make the variable a color, then adjust alpha
$cf-over: rgb(168, 50, 30);
background-color: rgba($cf-over, 0.1);
```

## `--watch` waits for a write to finish before it builds

The watcher polls the model source and rebuilds when it changes — but it does
**not** rebuild the instant it sees the first change. It waits for the source to
stop moving first.

That matters because an `mxcli exec` of a real script rewrites the `.mpr` and
many `mprcontents/*.mxunit` files over several seconds. Building on the first
change deploys whatever is on disk at that instant: a **half-applied model**,
which looks like an ordinary stale build until you notice the app is missing
things the script definitely created.

The escape hatch that used to cover this — "just run the script again" — stopped
working when `exec` became byte-idempotent. A re-run of an already-applied script
writes nothing, so nothing re-triggers the watcher, and the stale build is what
you are left with. Waiting for the write to settle is what makes that
unreachable rather than merely unlikely.

Practical consequences:

- A rebuild starts a couple of poll intervals after your last change, not
  immediately. A single editor save is unaffected in practice.
- A long `exec` produces **one** build, of the finished model, instead of a
  build of the first file it happened to touch.
- If you ever do need to force a rebuild without changing anything, `touch` the
  `.mpr` — the watcher keys on mtime, so that re-triggers it.

## "My edit didn't show up" — stale process, not stale cache

`run --local` refuses to boot when its ports (8080/8090/6543) are already answering,
because a leftover `run --local` / `mxbuild --serve` / runtime would otherwise be
silently adopted and keep serving old output (it looks like a cache but is a stale
**process**). If a background `run --local` died while its serve+runtime kept serving,
**the refusal names the pid** — on Linux it resolves the listener through `/proc`, so
recovery is one command:

```
port 8080 (app) is already in use.
  Held by pid 11893: /root/.mxcli/mxbuild/11.13.0/modeler/mxbuild --serve …
  That is a leftover from an earlier run that did not shut down cleanly
  (a kill -9 or a reaped container skips mxcli's own teardown).
    kill 11893
```

It also distinguishes the two cases, which need opposite remedies: a leftover of a
previous run is safe to kill, while a **foreign** listener (someone else's server on
8080) is not — for that it says so and points at `--app-port`.

Note that a *graceful* stop already reaps everything: `run --local` puts each child
(mxbuild's JVM, the runtime, the rollup bundler) in its own process group and kills the
group on Ctrl-C/SIGTERM. Reaching this error means the previous run was killed with
`kill -9`, crashed, or had its container reaped — none of which run any handler. Do
**not** `pkill -f 'mxcli run'`: that pattern also matches the shell you type it in.

Launch `run --local` as the **sole** command in its invocation (don't chain a trailing
`sleep`/`curl` whose non-zero exit can kill the backgrounded run); poll separately.

## Flags

| Flag | Default | Purpose |
|------|---------|---------|
| `--local` | — | Required; run without Docker (implied by `--hub`) |
| `--hub` | — | Expose the app in a browser at a tunnel-hub URL (see below) |
| `--hub-secret` | — | Shared auth (`user:pass`) matching an **open** hub's `--secret` |
| *(hub API key)* | — | For an **authenticated** hub: get one from `https://<hub>/cli`, set `MXCLI_HUB_KEY` (see below) |
| `--watch` | off | Rebuild + hot-apply on each change |
| `--ensure-db` | off | Provision local Postgres + app database if missing |
| `--setup` | off | Cache MxBuild+runtime + ensure DB, then exit (SessionStart bring-up) |
| `--screenshot` | off | Playwright PNG after boot + each change |
| `--screenshot-path` / `--screenshot-url` | `.mxcli/run-local.png` / app root | Screenshot output / page (URL or `/path`) |
| `--screenshot-user` / `--screenshot-password` | — | Log in once, reuse session (pages behind login) |
| `--runtime-log` | `.mxcli/runtime.log` | Runtime log file: JVM stdout/stderr **and** the application log (microflow `LOG` output + server stack traces, via an attached file log subscriber). `-` disables. |
| `--test-endpoint` | off | Host mxcli's token-guarded test endpoint so `mxcli test … --attach` can run a suite against this app with no boot of its own. Installed **before** the boot (the handler registers from after-startup), your own after-startup microflow is chained not displaced, and both are removed on exit. See `test-microflows`. |
| `--debug` | off | Enable the microflow debugger at boot + start a session, so `mxcli debug break/paused/…` works from another terminal (see `debug-microflows`). No breakpoints = no behaviour change; disabled on shutdown. |
| `--debug-pass` | `mxdebug` | Debugger password when `--debug` is set |
| `--metrics` | off | Register a Prometheus meter registry at boot; the runtime serves metrics at `http://127.0.0.1:<admin-port>/prometheus` |
| `--trace` | off | Enable OpenTelemetry tracing (bundled agent, console exporter → the runtime log) with default span filters |
| `--trace-service` | `.mpr` name | `OTEL_SERVICE_NAME` under `--trace` |
| `--trace-otlp` | off (console) | Export traces to this OTLP collector endpoint (e.g. `http://127.0.0.1:4318`) instead of the console. Implies `--trace`. Needed for flame charts |
| `--runtime-setting Key=Value` | — | Merge an extra runtime setting into the boot config (Value parsed as JSON when possible). Repeatable. |

## Metrics and OpenTelemetry

**Metrics (`--metrics`):** the Mendix runtime ships Micrometer registries but starts
with none. `--metrics` registers a **Prometheus** registry at boot, so
`http://127.0.0.1:8090/prometheus` (the admin port) serves ~70+ metric families
(`connectionbus_*`, `handler_requests_total`, `sessions_*`, `taskqueue_*`, …). For a
different registry use `--runtime-setting`, e.g.
`--runtime-setting 'Metrics.Registries=[{"type":"otlp","settings":{"step":"PT10S"}}]'`
(also `influx`, `statsd`, `jmx`).

**Why this is a flag, not a post-boot API call:** the admin `update_configuration`
action **replaces** the whole config (there's no read-back), so a separate call to add
metrics would wipe the DB/BasePath settings. `--metrics`/`--runtime-setting` merge into
mxcli's single boot `update_configuration`, which is the only safe way.

**Traces (`--trace`):** the runtime bundles the OpenTelemetry Java agent;
`--trace` attaches it to the runtime JVM and applies the default span filters:

```bash
mxcli run --local -p app.mpr --trace                 # spans -> runtime.log
mxcli run --local -p app.mpr --trace --runtime-log - # spans to console only
```

Spans (tracer `com.mendix.runtime`, attrs `mx.microflow.name`/`mx.microflow.depth`) go
to the console exporter → `runtime.log` (so `tail -f .mxcli/runtime.log` shows them).
`--trace` sets `OTEL_SERVICE_NAME` (default the `.mpr` name, override with
`--trace-service`) and, unless you set them yourself, `OTEL_TRACES_EXPORTER=console`
with metrics/logs exporters off.

**Why the default span filters matter:** unfiltered per-activity tracing is
**~10× slower**, so `--trace` ships `OpenTelemetry._RuntimeSpanFilters` =
`["CreateOrChangeVariable","Loop","Gateway","RetrieveFromCache"]` by default (keeping
the microflow-level spans). Override with
`--runtime-setting 'OpenTelemetry._RuntimeSpanFilters=[…]'`.

**Export to a collector (OTLP) instead of the console:** the console exporter
omits start/end timestamps and parent span IDs, so call trees and durations can't
be reconstructed from it — for flame charts, export to an OTLP collector. Pass
`--trace-otlp <endpoint>` (implies `--trace`); mxcli sets
`OTEL_TRACES_EXPORTER=otlp`, `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, and
`OTEL_EXPORTER_OTLP_ENDPOINT` for you:

```bash
mxcli run --local -p app.mpr --trace-otlp http://127.0.0.1:4318
```

You can still set the `OTEL_*` env yourself for full control — `--trace` /
`--trace-otlp` never override an exporter you've already set:

```bash
export OTEL_TRACES_EXPORTER=otlp OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
mxcli run --local -p app.mpr --trace
```
| `--app-port` / `--admin-port` / `--serve-port` | 8080 / 8090 / 6543 | Ports |
| `--db-host` / `--db-name` / `--db-user` / `--db-password` | 127.0.0.1:5432 / derived / mendix / mendix | Database; bracket IPv6 endpoints (`[::1]:5432`) |

## Pages render in the browser

`run --local` bundles the browser client (`web/dist/`) with mxbuild's rollup tooling
after the deploy build, so the app renders in a real browser (verified with
Playwright + the devcontainer's Chromium).

- **`--watch`** keeps a long-lived incremental bundler hot (the client-side mirror of
  `mxbuild --serve`): a page/widget edit re-bundles in ~3–4 s; a microflow/entity edit
  skips the bundle and just hot-reloads. It uses `CHOKIDAR_USEPOLLING` because inotify
  is silent on container filesystems.
- Without `--watch`, a single one-shot bundle (~7 s) runs before boot.
- **The bundle is re-checked after the boot**, because bundling before it is not
  enough: the runtime's boot runs Gradle `clean-custom-classes compile package`,
  and when Gradle has work to do (a new Java action, a full recompile) its package
  pass repopulates `deployment/web` and deletes `dist/` — the bundle written
  seconds earlier by the same command. If that happened, `run --local` says
  `re-bundling` and rebuilds it. When Gradle had nothing to do the check is a
  `stat` and costs nothing.

**If you ever see a black page:** that is this failure, and nothing else reports
it — `mxcli check` passes, the build succeeds, the runtime log is quiet, `curl /`
returns **200** with a valid HTML shell, and the OData services all answer. Only a
browser sees it. Confirm with `curl -o /dev/null -w '%{http_code}' <app>/dist/index.js`;
a 404 there is the whole diagnosis.

`mxcli test --local` boots the same way and destroys the bundle too. Tests are
headless so it is not rebuilt for them (that would cost ~30 s on a loop whose point
is two seconds) — the run prints a note instead, and a subsequent `run --local`
restores it.

### Screenshots when the app has an https root URL (`--hub`)

Under `--hub` the runtime boots with the public **https** root URL, so it marks
its session cookies `Secure` and prefixes them `__Host-`. The screenshot login
therefore declares the real scheme with `X-Forwarded-Proto: http`, which on
Mendix 10.24+ takes precedence over `ApplicationRootUrl` and drops both — so the
captured session is usable over http.

Measured on 11.12.1, `__Host-XASSESSIONID (secure)` becomes `XASSESSIONID`
(not secure). Real users still arrive over https through the hub without that
header and still get `Secure` cookies.

One correction to a common assumption: this is **not** needed for `127.0.0.1`.
Loopback is a *trustworthy origin*, so Chromium accepts `Secure` cookies there
and an app with an https root URL renders and logs in fine over
`http://127.0.0.1:8080`. It matters when the browser reaches the app from a
non-loopback host — a container name, a LAN address — where the origin is not
trustworthy and the session cannot be held at all.

## Pixel-perfect page loop

`--screenshot` captures a PNG (default `<projectDir>/.mxcli/run-local.png`) after boot
and after each applied change, via Playwright's built-in `screenshot` command
(Chromium from `PLAYWRIGHT_BROWSERS_PATH`):

```bash
mxcli run --local -p app.mpr --watch --screenshot
# edit a page -> auto rebuild -> re-bundle -> reload -> fresh screenshot
```

- `--screenshot-url /p/customers` shoots a specific page (bare path resolved against
  the app root; a full URL is used as-is). Repeat it for a multi-page set — each page
  gets its own PNG (`run-local-p-customers.png`, `run-local-home.png`).
- `--screenshot-user`/`--screenshot-password` log in once (Mendix form auth) and
  reuse the session, so pages behind login render authenticated. Best-effort: an
  anonymous app with no login form proceeds unauthenticated.

## Debugging a server-side error

When a page action throws, the browser shows the generic Mendix error dialog with no
detail. The runtime log — server stack traces and your microflow `LOG ERROR`/
`LOG INFO` output — is written to `<projectDir>/.mxcli/runtime.log` (the path is printed
at boot). `tail -f .mxcli/runtime.log` while you reproduce the action to see the stack
and correlate it.

Two things feed that file, because a standalone runtime attaches **no** log subscriber
by default (a Studio Pro / m2ee run does):

- mxcli tees the runtime **JVM's** stdout/stderr to it (startup output, JVM-level crashes).
- After start, mxcli attaches a Mendix **file log subscriber** so the **application** log
  — microflow `LOG` output and server-side exception stack traces — lands there too.
  Without this, application logs go nowhere and the file is nearly empty.

The file is appended across restarts (each boot writes a `=== runtime start … ===`
marker); the subscriber is re-attached on every restart and never rotates the file (so
the JVM tee's handle stays valid). Override the path with `--runtime-log <path>`, or
pass `--runtime-log -` to disable the file (and the subscriber) entirely.

## "Sign in failed" that is not about the password

The local runtime is **unlicensed**, and an unlicensed runtime caps concurrent
sessions at a handful. Past the cap it refuses the sign-in, and the login page
reports that as a plain **"Sign in failed"** — exactly what a wrong password
looks like. The real reason is written only to the runtime log:

```
Maximum number of sessions exceeded! (You are currently using a trial license)
```

So: when a login you know is correct starts failing, `grep -c "Maximum number of
sessions" .mxcli/runtime.log` before touching the credentials or the user's
password in the model. `--screenshot-user` does this for you — a rejected sign-in
now reads the log and says so instead of quietly screenshotting the login page.

Sessions are held until they expire; restarting `run --local` clears them all.
A script that drives the app through a browser should **sign out at the end**,
otherwise each run leaks a slot and the fifth or sixth run is the one that fails —
which makes it look like a change you just made broke authentication.

## External browser preview (`--hub`)

> **Linux builds only.** `--hub` and `mxcli tunnel-hub` ship in the **Linux** build
> only. The tunnel embeds a general-purpose tunnelling tool that gets the Windows
> and macOS binaries flagged by Defender and enterprise EDR for a capability they
> can never use, so it is left out of them. On Windows/macOS the commands exist and
> show help, but fail with an explanatory message — run mxcli inside the project's
> devcontainer (where the warm loop already runs) to use `--hub`. See
> [ADR-0009](https://github.com/mendixlabs/mxcli/blob/main/docs/13-decisions/0009-tunnel-is-linux-only.md).

`--hub <url>` exposes the running app in a **browser at a public URL** without the app
leaving this machine and without committing — for reviewing work-in-progress from a
phone/tablet, or from an egress-only environment like Claude Code on the web. The app
stays here; a **reverse tunnel** dials *out* to a hub over 443 and the hub proxies
browser requests back down it. Nothing is pushed — only live HTTP — and everything rides
one 443 connection, so it works through an egress-only proxy.

**You run your own hub — there is no hosted service.** Stand up `mxcli tunnel-hub` once on
a host you control (a small VPS with a domain), then point apps at it.

```bash
# on your VPS: *.example.com + hub.example.com -> this host, inbound 80+443 open
mxcli tunnel-hub --domain example.com --secret alice:s3cret

# where the app runs:
mxcli run --hub https://hub.example.com --hub-secret alice:s3cret -p app.mpr
#   -> registers and prints e.g. "Preview available at https://app.example.com"
```

The hub is **multi-tenant**: it fronts many previews at per-preview subdomains
(`<project>-<branch>.example.com`; `main`/`master` collapses to `<project>`) with a
sortable overview at `https://hub.example.com/`. Each `run --hub` self-registers:

- **Project** and **branch** auto-detect (`.mpr` name + git); override with
  `--hub-project` / `--hub-branch`. `--hub-worktree` distinguishes worktrees of one branch.
- **`--hub-prefix`** namespaces the hostname (org/solution/team/env) →
  `<prefix>-<project>-<branch>`; **`--hub-solution`** groups a solution's apps in the overview.
- The overview shows availability (a reaped/idle container goes **stale**), sortable by
  last-used / registered / project. Re-registering keeps a **stable URL**.
- `--hub` **implies `--local`**, boots the runtime with `ApplicationRootUrl` set to the
  assigned URL (so the SPA/`originURI` work), and the tunnel reconnects forever. Combine
  with `--watch` for the full loop: edit here → hot-apply → refresh the browser.

**Hub setup:** wildcard `*.example.com` A record (+ `hub.example.com`) → the VPS; inbound
80+443 open (per-subdomain Let's Encrypt on demand).

### Authenticated hub (GitHub)

A self-hosted hub started **without** the GitHub flags is open — the shared `--hub-secret`
is all `run --hub` needs. A hub started **with** `--github-oauth-client-id` (see
`mxcli tunnel-hub --help`) adds per-user isolation: browsers sign in with GitHub and see
only their own previews, and registration needs a **per-user hub API key** instead of the
shared secret.

**Get a key from the hub's browser page** (works from any device — desktop, or Claude
Code web/mobile, whose container can't reach GitHub's device endpoints):

1. Open `https://hub.mxcli.org/cli` in a browser and sign in with GitHub.
2. Click **Create a hub key** and copy it.
3. Set it as an environment/repo secret in your Claude Code environment — it's picked up
   automatically and survives container reaping:

```bash
export MXCLI_HUB_KEY=<key>
mxcli run --hub https://hub.mxcli.org -p app.mpr   # registers previews as you
```

The key is **durable** (no expiry, survives hub restarts) — you set it once. It stays valid
until you revoke it (sign out on the hub, or `mxcli auth hub logout`).

Headless alternative (CI, or a machine with a GitHub token): `mxcli auth hub login --token
<github-pat>` mints and caches a key in `~/.mxcli/auth.json` (mode 0600). The GitHub token
is used once for the mint and never stored.

**Registration failure is non-fatal:** if the hub is unreachable or the key is stale/rejected,
`run --hub` prints a warning and continues as a normal local run (the app boots on localhost;
only the public preview URL is lost). On an authenticated hub the shared `--hub-secret` still
works as a fallback registration credential — a valid key stamps the preview as yours, a valid
secret registers it owner-less.

## Validation checklist

- [ ] Project is Mendix 11.x.
- [ ] Postgres is running and the target database exists.
- [ ] `curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/` returns `200`,
      and `.../dist/index.js` also returns `200` (client bundle served).
- [ ] With `--watch`, editing a microflow logs `applied via reload`; adding an entity
      logs `applied via restart` and creates the table in Postgres.

## Constant values come from a configuration

`mxcli run --local` applies the constant values of the project configuration it
is running, merged over each constant's default:

```text
Applying 1 constant value(s):
  Encryption.EncryptionKey  configuration "Default"
```

Before this they were ignored: mxbuild writes each constant's **default** into
`deployment/model/config.json`, and that map is what the runtime is handed — so
`alter settings constant … in configuration 'Default'` executed, round-tripped
through `describe settings`, and did nothing. An app ran for hours with an empty
encryption key while the model said otherwise.

- `--configuration <name>` picks one. With several configurations and none named
  `Default`, mxcli applies **none** and says so rather than guessing which
  environment this run means.
- A **private** override has no value in the model at all (the value lives on the
  developer's workstation), so the default is used and the constant is named.
- The line prints in every case, including "no overrides" — silence used to mean
  "your override is in effect" when it was not.
- `--constant Module.Name=value` (repeatable) sets a value for **this run only**.
  It wins over the configuration, is never written to the project, and is
  reported as coming from `--constant` so the output says which layer won. A
  constant the project does not declare is refused before the app boots: the
  runtime ignores a value for a constant that does not exist, so a typo would
  otherwise be reported as applied and do nothing.

Setting a constant on an app that is *already running* is a different mechanism
again: `MicroflowConstants` over the M2EE admin port, which is how Mendix Cloud
injects per-environment values. Measured on 11.12.1, `update_configuration` is
**staged rather than applied** — the running app keeps the old value until the
next `reload_model`, while the call answers `result:0` — and the admin API has no
read-back to check against. Do not reach for
`--runtime-setting 'MicroflowConstants={…}'`: it replaces the map mxcli built
rather than adding to it, and at boot there is nothing to fall back on for
`BasePath`/`DatabaseName`. Use `--constant`.
