Summary
docs/configuration.md states that system_config is seeded once and that the mapped environment variable is never consulted again. The code re-applies env values on every boot to any row whose updatedBy is NULL. Separately, the workaround the docs recommend (change the value through the admin system-config endpoints) does not actually protect a row, because those endpoints never populate updatedBy. The net effect is that admin changes made in the console are silently reverted by the next restart.
What the docs say
docs/configuration.md, "Environment vs system_config":
system_config is seeded once. After first boot, the row is authoritative and the mapped environment variable is no longer consulted for that key. To change a seeded value you must update the system_config row (via the admin system-config endpoints), not just .env.
What the code does
src/config/bootstrapSystemConfig.ts runs on every boot and, for each env-mapped key:
if (envValue && existing.updatedBy == null) {
// re-applies the parsed env value over the stored row
}
So the env var is consulted on every boot, and it wins for any row with updatedBy IS NULL. The row is authoritative only once updatedBy is set.
Why the documented workaround does not engage
updatedBy is derived from req.clientId (src/controllers/oauthProviders.ts:32, and the same pattern in src/controllers/systemConfig.ts). The only assignment of req.clientId is src/middleware/authenticateServiceToken.ts:112, on the service-token path.
The admin system-config routes are declared with auth: 'access' plus requireAdmin(...) (see src/routes/systemConfig.routes.ts, for example the /oauth-providers GET/POST/PATCH entries). A signed-in admin therefore authenticates with an access token, authenticateServiceToken never runs, req.clientId is undefined, and updatedBy is written as null. The row stays eligible for env re-seeding, so the documented way to make a change stick does not.
Demonstrated impact
Local stack, OAUTH_PROVIDERS=[] and LOGIN_METHODS without oauth in the env:
- Added a Google OAuth provider through the admin console, and enabled the
oauth login method. Both landed in system_config and worked.
- Restarted the auth API as part of an unrelated rebuild.
oauth_providers was back to [] and login_methods had lost oauth. The console login page showed no providers again.
Every env-mapped row in that database currently reports updatedBy IS NULL, including oauth_providers and login_methods that were written through the console:
access_token_ttl | NULL
app_name | NULL
available_roles | NULL
default_roles | NULL
delay_after | NULL
frontend_url | NULL
lockout_policy | NULL
login_methods | NULL
oauth_providers | NULL
origins | NULL
passkey_login_fallback_enabled | NULL
rate_limit | NULL
refresh_token_ttl | NULL
rpid | NULL
There is no warning when a stored value is overwritten, so the change simply disappears.
Possible resolutions
These are alternatives, and the choice is a product decision:
- Treat it as a behavior bug: populate
updatedBy for admin-authenticated writes (the acting user's id) so an admin change is genuinely authoritative, matching what the docs already promise.
- Treat it as a docs bug: describe the real precedence (env wins on every boot until
updatedBy is set, and only service-token writes currently set it), and say plainly that console changes to env-mapped keys do not survive a restart.
Either way it seems worth logging when a boot overwrites a differing stored value, since the current behavior is silent.
References
src/config/bootstrapSystemConfig.ts
src/controllers/oauthProviders.ts:32
src/middleware/authenticateServiceToken.ts:112
src/routes/systemConfig.routes.ts
docs/configuration.md, "Environment vs system_config"
Summary
docs/configuration.mdstates thatsystem_configis seeded once and that the mapped environment variable is never consulted again. The code re-applies env values on every boot to any row whoseupdatedByisNULL. Separately, the workaround the docs recommend (change the value through the admin system-config endpoints) does not actually protect a row, because those endpoints never populateupdatedBy. The net effect is that admin changes made in the console are silently reverted by the next restart.What the docs say
docs/configuration.md, "Environment vssystem_config":What the code does
src/config/bootstrapSystemConfig.tsruns on every boot and, for each env-mapped key:So the env var is consulted on every boot, and it wins for any row with
updatedBy IS NULL. The row is authoritative only onceupdatedByis set.Why the documented workaround does not engage
updatedByis derived fromreq.clientId(src/controllers/oauthProviders.ts:32, and the same pattern insrc/controllers/systemConfig.ts). The only assignment ofreq.clientIdissrc/middleware/authenticateServiceToken.ts:112, on the service-token path.The admin system-config routes are declared with
auth: 'access'plusrequireAdmin(...)(seesrc/routes/systemConfig.routes.ts, for example the/oauth-providersGET/POST/PATCH entries). A signed-in admin therefore authenticates with an access token,authenticateServiceTokennever runs,req.clientIdis undefined, andupdatedByis written asnull. The row stays eligible for env re-seeding, so the documented way to make a change stick does not.Demonstrated impact
Local stack,
OAUTH_PROVIDERS=[]andLOGIN_METHODSwithoutoauthin the env:oauthlogin method. Both landed insystem_configand worked.oauth_providerswas back to[]andlogin_methodshad lostoauth. The console login page showed no providers again.Every env-mapped row in that database currently reports
updatedBy IS NULL, includingoauth_providersandlogin_methodsthat were written through the console:There is no warning when a stored value is overwritten, so the change simply disappears.
Possible resolutions
These are alternatives, and the choice is a product decision:
updatedByfor admin-authenticated writes (the acting user's id) so an admin change is genuinely authoritative, matching what the docs already promise.updatedByis set, and only service-token writes currently set it), and say plainly that console changes to env-mapped keys do not survive a restart.Either way it seems worth logging when a boot overwrites a differing stored value, since the current behavior is silent.
References
src/config/bootstrapSystemConfig.tssrc/controllers/oauthProviders.ts:32src/middleware/authenticateServiceToken.ts:112src/routes/systemConfig.routes.tsdocs/configuration.md, "Environment vssystem_config"