BoxLang 🚀 A New JVM Dynamic Language Learn More...
A per-application .env for ColdBox apps that share one
CFML engine with other sites.
On a typical shared Windows/IIS server, every site runs in the same
Adobe ColdFusion (or Lucee) JVM. Loading .env the usual
way puts each value into JVM system properties or the OS environment.
Then DB_PASSWORD from one site overwrites another's, and
every site can read every other site's secrets. ColdBox's
getSystemSetting() only looks there, so it can't help.
AppDotEnv keeps each app's values in that app's own
application scope:
Application.cfc parses the app's
.env into application.dotenv before
ColdBox boots.config/ColdBox.cfc and
config/modules/*.cfc call application.dotenv.get(
"KEY", default ).DotEnv@appdotenv.?fwreinit=<password> re-reads the file, so
changing a value needs no restart.| Environment | File | Why |
|---|---|---|
| Development | <webroot>/.env
| The same file CommandBox loads for the dev server. |
| Production | <webroot>/../.env
| One level above the webroot, so the web server can never serve it. |
The webroot file wins when both exist. When neither exists the app still boots on defaults, and the startup log names the production path it expected:
C:\websites\example.com\
.env <- production secrets
wwwroot\ <- IIS physical path and the app root
box install appdotenv
ColdBox loads modules after it reads your config, so the module can't
load the file itself. Two small steps in Application.cfc
do that.
public boolean function onApplicationStart(){
// Before the ColdBox bootstrap: config CFCs read application.dotenv.
application.dotenv = new modules.appdotenv.models.DotEnv( appRoot = COLDBOX_APP_ROOT_PATH ).load();
application.cbBootstrap = new coldbox.system.Bootstrap( COLDBOX_CONFIG_FILE, COLDBOX_APP_ROOT_PATH, COLDBOX_APP_KEY, COLDBOX_APP_MAPPING );
application.cbBootstrap.loadColdbox();
return true;
}
public boolean function onRequestStart( string targetPage ){
// Re-read .env on ?fwreinit (URL or POST). The first branch covers an app that
// started before these lines were deployed.
if ( !application.keyExists( "dotenv" ) ) {
application.dotenv = new modules.appdotenv.models.DotEnv( appRoot = COLDBOX_APP_ROOT_PATH ).load();
} else {
application.dotenv.syncWithRequest();
}
application.cbBootstrap.onRequestStart( arguments.targetPage );
return true;
}
The dotted path assumes the default install location,
modules/appdotenv/. If the module loads without
application.dotenv, it throws
AppDotEnv.NotBooted with the line to add.
TestBox: integration tests boot ColdBox through
BaseTestCase, not your root Application.cfc.
Add the same line to tests/Application.cfc
(onApplicationStart, plus the keyExists
guard in onRequestStart), using your root mapping:
new root.modules.appdotenv.models.DotEnv( appRoot = rootPath ).load().
// config/ColdBox.cfc, config/modules/*.cfc
reinitPassword : application.dotenv.get( "COLDBOX_REINIT_PASSWORD", "" ),
defaultSecret : application.dotenv.get( "CMS_SECRET" ), // no default: a missing value stops the boot
// handlers, models, interceptors
property name="dotenv" inject="DotEnv@appdotenv";
var to = dotenv.get( "CONTACT_TO", "[email protected]" );
| Method | Does |
|---|---|
init( path = "", appRoot = "" )
| path names the file exactly;
appRoot resolves it as described above. |
load() / reload()
| Read the file. A missing file isn't an error. |
syncWithRequest( [params] )
| reload() when the request has
fwreinit (URL or form). |
get( key, [default] )
| File, then Java system property, then OS env, then
default. Without a default, throws
SystemSettingNotFound. A blank value counts as unset. |
has( key )
| Whether get() would succeed without a default. |
keys()
| Sorted key names from the file. Safe to log. |
describe()
| One log-safe line: key count and path. The module logs it on load. |
getPath(), isLoaded(), getLoadedAt()
| What was read, and when. |
The file wins over JVM and OS values so another site's JVM-global
setting can never leak in. Values live in the component's
variables scope, so dumping the application scope doesn't
print them.
KEY=value and export KEY=value.
# starts a comment on its own line, or after an unquoted
value when whitespace comes before it. One pair of outer quotes is
stripped. Inside double quotes \n, \r,
\t, \" and \\ are
unescaped; single quotes are literal. There is no ${VAR}
interpolation. CRLF line endings and a UTF-8 BOM are fine.
box install appdotenv.Application.cfc lines above (and to tests/Application.cfc).getSystemSetting( "X", d ) in
config/ with application.dotenv.get(
"X", d ). Inject DotEnv@appdotenv
wherever code read getSystemSetting() at runtime..gitignore: .env, .env.*, !.env.example
.env.example with every key and only
harmless values..deployignore): /.*
web.config: add .env to
<hiddenSegments> under requestFiltering..env.production at the repo root. The
.env.* pattern already ignores it. This module never
reads that name; it's only the copy you edit and deploy from.<webroot>\..\.env. Remove inherited permissions
and grant Read only to the CFML engine's service account and Administrators.?fwreinit=<your
reinit password>.The file is outside the webroot and IIS has no permission to read it,
so it can't be served. But every site in a shared engine runs as the
same service account, so CFML code in another site on that
server could still fileRead() it. Use a separate engine
instance or a sandbox when that matters.
Adobe ColdFusion 2023 and 2025, BoxLang 1 (CFML compatibility), Lucee 6. Tested on ColdBox 8.
Apache 2.0.
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
DotEnv parses an app's own .env into
application.dotenv before ColdBox boots, so config CFCs can read it without the JVM or the
OS environment. It reads the webroot .env in development and the one above the webroot in
production.syncWithRequest() reloads the file on a ColdBox reinit, including a reinit sent as a POST
field.DotEnv@appdotenv and logs where it was loaded from,
key names and counts only.
$
box install appdotenv