fix: TOML config loading, DSN conflict, and .env interpolation#368
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes TOML-based multi-database configuration mutually exclusive with single-database DSN configuration, rejecting ambiguous setups (e.g., dbhub.toml present alongside --dsn, DSN, DB_*, or DSN in .env) with an explicit, actionable error. It also updates the documentation to reflect the clarified configuration model and adds tests around DSN conflict detection and side-effect-free .env inspection.
Changes:
- Split env file discovery into
findEnvFile()so DSN conflict detection can inspect.envcontents without mutatingprocess.env. - Add
detectDSNConfig()and enforce TOML/DSN mutual exclusivity inresolveSourceConfigs(), reporting the DSN’s origin in the error. - Update docs (and CLAUDE.md) to describe the two-rule model: “sources” are TOML-or-DSN, while all other settings follow the normal CLI/env precedence; add tests for the new detection logic.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/config/env.ts | Adds side-effect-free DSN detection and rejects TOML+DSN combinations; refactors env file discovery. |
| src/config/tests/env.test.ts | Adds tests for DSN detection and TOML/DSN conflict handling. |
| docs/config/toml.mdx | Documents TOML/DSN mutual exclusivity and the startup error behavior. |
| docs/config/command-line.mdx | Rewrites the priority explanation into separate “database sources” vs “other settings” rules and documents the exclusivity constraint. |
| CLAUDE.md | Updates configuration guidance and priority rules to match the new behavior. |
TOML config and a DSN express conflicting intents: TOML defines sources for one or more databases, a DSN configures exactly one. Previously resolveSourceConfigs() returned the TOML config before resolveDSN() was ever consulted, so a DSN — including an explicit --dsn flag — was silently ignored with no indication of why. Now the combination is rejected at startup with an error naming the conflicting DSN source, mirroring the existing --id guard. Detection is side-effect free. It deliberately does not call resolveDSN(), which loads .env files into process.env: in TOML mode .env is otherwise never loaded, and loading it would silently start applying unrelated settings such as PORT and TRANSPORT. Instead findEnvFile() is split out of loadEnvFiles() so a .env file can be inspected with dotenv.parse without being applied. Also corrects the documented priority order, which claimed command-line arguments outrank TOML. That was never true for source configuration. CLAUDE.md and the docs now separate source selection (TOML or DSN, mutually exclusive) from every other setting (CLI > env > .env > defaults), which TOML cannot express. BREAKING CHANGE: a setup with both a dbhub.toml and any DSN configuration now fails to start instead of silently using the TOML config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveTomlConfigPath() fell back to ./dbhub.toml in the current working
directory when --config was absent. Because TOML config selects the
database outright and cannot be combined with a DSN, that implicit
discovery meant running DBHub from the wrong directory could silently
repoint it at a different database, or make an explicitly supplied DSN
appear to be ignored for no visible reason.
TOML is now loaded only when --config names it. This also shrinks the
blast radius of the DSN/TOML conflict error added in the previous commit:
the conflict can now only arise from an explicit --config flag, never
from a stray file in the working directory.
The MCPB bundle already passes --config ${__dirname}/dbhub.toml
explicitly, and every documented invocation does the same, so no shipped
configuration depends on auto-discovery.
BREAKING CHANGE: a dbhub.toml in the current directory is no longer
loaded automatically. Pass --config ./dbhub.toml to load it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the configuration docs to state how DBHub picks a database rather than contrasting against earlier behavior, and trim the same framing from the surrounding code comments. Also restores resolveDSN's doc comment, which had been separated from its function by the detectDSNConfig insertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TOML `${VAR}` interpolation reads process.env, so `dsn = "${DSN}"` is a
supported way to keep credentials out of the config file. Treating a DSN
environment variable — exported or read from .env — as a conflict would
break that pattern, which is the whole point of interpolation.
Only the --dsn flag is unambiguous intent, so only that throws. This also
drops the .env inspection the broader check needed, along with the
findEnvFile export added for it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tianzhou
force-pushed
the
fix/dsn-toml-conflict
branch
from
July 19, 2026 15:35
41c8d59 to
880fdc9
Compare
interpolateEnvVars() reads process.env, but .env was only loaded inside
resolveDSN() — the fallback path that never runs when --config is given.
So the recommended pattern of keeping credentials in .env and referencing
them from the config file did not work:
# .env
DSN=postgres://user:pass@localhost:5432/mydb
# dbhub.toml
dsn = "${DSN}"
resolved to the literal "${DSN}" and failed with "No connector found".
Loading is scoped to TOML mode rather than unconditional. On the DSN path
resolveDSN() loads .env itself, and it has to do so after checking
process.env so it can report whether a value came from the environment or
from the file; preloading would relabel every .env-sourced DSN as an
environment variable.
Same fix as #358 by @kevinke, narrowed in scope and with a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It pointed at an isReadOnlyMode() function that does not exist, and described a deprecation warning where parseCommandLineArgs() actually exits on --readonly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--config now goes through requireFlagValue(), so a bare `--config` or `--config=` reports "--config requires a value" instead of resolving to the sentinel "true" and failing as `not found: true`. That helper already existed for --host and --allowed-hosts and documents itself as reusable. This matters more now that --config is the only way to load TOML. findEnvFile() also checked the working directory twice: a bare relative filename resolves against process.cwd(), so it was identical to the explicit path.join(process.cwd(), ...) entry. Removed the trailing duplicate rather than the leading one, which would have flipped precedence so the package root won over the working directory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tests mocked loadTomlConfig() into returning a config while argv had no --config. Since --config is the only way TOML loads, that state cannot occur at runtime — and it specifically skipped the .env-before-TOML load, which is gated on the flag, so those tests exercised a path production never takes. They now pass --config, and run from an empty temp directory so an ambient .env cannot reach the load the flag now triggers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines
+116
to
+120
| if (envPath) { | ||
| dotenv.config({ path: envPath }); | ||
|
|
||
| // Check for deprecated environment variables | ||
| if (process.env.READONLY !== undefined) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three related fixes to how database sources are configured. Two are breaking. Follow-up to #367, where investigating a test failure surfaced the first.
1. TOML config is loaded only via
--configresolveTomlConfigPath()fell back to./dbhub.tomlin the current working directory when--configwas absent. Since TOML config selects the database outright, that meant running DBHub from the wrong directory could silently repoint it at a different database — or make an explicitly supplied DSN look ignored for no reason:The user asked for SQLite and got a Postgres error, never having mentioned TOML.
Nothing shipped depends on the old fallback: the MCPB bundle already passes
--config ${__dirname}/dbhub.toml(mcpb/manifest.json), and every documented invocation passes--configexplicitly.2.
--configand--dsncannot be combinedresolveSourceConfigs()returned the TOML config beforeresolveDSN()was ever consulted, so--dsnwas silently ignored. The documented priority order said the opposite — CLAUDE.md anddocs/config/command-line.mdxboth claimed "command-line arguments (highest priority)" over TOML.TOML wins, and the docs were wrong. The two are conflicting rather than layered: TOML is multi-database, a DSN is single-database. Passing both flags now errors, mirroring the existing
--idguard:The guard is deliberately limited to the flag.
DSNandDB_*environment variables are not conflicts, whether exported or read from.env— see below.3.
.envis loaded before TOML, so${VAR}interpolation resolvesinterpolateEnvVars()readsprocess.env, but.envwas only loaded insideresolveDSN()— the fallback path that never runs when--configis given. So the recommended pattern of keeping credentials out of the config file did not work:# .env DSN=postgres://user:pass@localhost:5432/mydbThis is the same fix as #358 by @kevinke, narrowed in scope and with a regression test — that PR can be closed once this merges.
The narrowing matters: #358 calls
loadEnvFiles()unconditionally. On the DSN pathresolveDSN()already loads.envitself, and it does so after checkingprocess.envso it can report whether a value came from the environment or from the file. Preloading unconditionally relabels every.env-sourced DSN asenvironment variablein the startup log. Scoping the load to TOML mode avoids that; verified the DSN path still reportsConfiguration source: .env file.Why fixes 2 and 3 belong together
They pull in opposite directions, and getting one right requires the other. An earlier revision of this PR rejected any DSN-shaped environment variable as a conflict — which would have broken the interpolation pattern above outright. Only
--dsnexpresses unambiguous intent; environment variables are config material for TOML, not a competing setup.dbhub.tomlin the current directory is no longer loaded. Pass--config ./dbhub.toml.--configtogether with--dsnnow fails to start instead of silently using TOML.Both warrant a release note. The second was chosen over a warning because silent-ignore is what produced the confusing failure above, and a stderr warning is easy to miss in stdio mode.
Docs
Both docs described a single priority ladder. The accurate model is two independent rules, since TOML only defines
sourcesandtoolsand cannot express transport/port/host:--config) or a DSN. Within DSN mode:--dsn>DSN>DB_*>.env..env> defaults (unchanged, always correct).Rewritten to describe the model directly rather than contrast against previous behavior. Updated
CLAUDE.md,docs/config/command-line.mdx,docs/config/toml.mdx.Verification
--dsn+--configrejected;DSNenv var +--configallowed;DB_*+--configallowed;.envloaded before TOML parsing; a cwddbhub.tomlignored..env-before-TOML test was confirmed to fail without the fix, not merely pass alongside it.toml-loadertests that wrotedbhub.tomlinto a temp cwd now point--configat it via one line inbeforeEach, rather than 106 edits.dsn = "${DSN}"resolves from both an exported variable and a.envfile; the DSN path still reports.env fileas its source;--dsn+--configerrors; cwddbhub.tomlignored; missing--configpath still errors.sqlserver.integration.test.ts(container won't boot on arm64) — fails identically on cleanmain.Rebased onto
mainafter #367 merged.🤖 Generated with Claude Code