Configuration & Options
MasterController builds an ASP.NET Core-style IConfiguration in setupServer() from master.root and master.environmentType: JSON files, environment-specific overrides, user secrets, environment variables, and command-line arguments, merged in a fixed precedence. On top sits the Options pattern (IOptions<T>) — typed, validated at boot — and master.environment (IHostEnvironment).
Sources and precedence#
Later sources win. Keys are case-insensitive and :-separated.
| # | ASP.NET Core | MasterController |
|---|---|---|
| 1 | appsettings.json | config/appsettings.json |
| 2 | appsettings.{Environment}.json | config/appsettings.{env}.json |
| 3 | — | config/environments/env.{env}.json (the existing server / MasterRecord config, unchanged) |
| 4 | user secrets (dotnet user-secrets) | ~/.master/usersecrets/<id>/secrets.json — master.userSecretsId = 'my-app' or MASTER_USER_SECRETS_ID; base dir MASTER_USER_SECRETS_PATH |
| 5 | environment variables Section__Key | same (__ → :); master.configurationEnvPrefix = 'MYAPP_' to read only prefixed vars |
| 6 | command line --Section:Key=value | same (--a:b=1, --a:b 1, --flag) |
| 7 | in-memory | master.configuration.set('Section:Key', value) |
Values from env vars and argv are coerced ("true" → true, "42" → 42).
{
"Smtp": { "host": "smtp.example.com", "port": 25, "from": "noreply@example.com" },
"ConnectionStrings": { "Default": "postgres://app@localhost/app" },
"Logging": { "LogLevel": { "Default": "info", "Orders": "debug" } }
}Reading configuration#
master.root = import.meta.dirname;
master.environmentType = process.env.NODE_ENV || 'development';
master.userSecretsId = 'my-app'; // optional
const server = master.setupServer('http'); // configuration is built here
master.configuration.get('Smtp:host'); // IConfiguration["Smtp:Host"] — keys are case-insensitive
master.configuration.get('Smtp:port', 25); // with default
master.configuration.has('Smtp:password');
master.configuration.getSection('Smtp'); // { host, port, ... }
master.configuration.getConnectionString('Default');// ConnectionStrings:Default
master.configuration.bind('Smtp', { tls: true }); // defaults + section
master.configuration.set('Feature:beta', true); // in-memory (highest precedence)
master.configuration.sources; // [{ name, kind, path }]
master.configuration.onChange((cfg, key) => { /* reloads and set() */ });Custom layering#
master.configurationEnvPrefix = 'MYAPP_'; // read only MYAPP_Section__Key variables
master.configurationReloadOnChange = true; // fs.watch the JSON sources
// replace the default layering entirely
master.buildConfiguration({
sources: (cfg) => cfg
.addJsonFile('settings.json', { optional: false })
.addUserSecrets('my-app')
.addEnvironmentVariables('MYAPP_')
.addCommandLine()
.addInMemory({ 'Feature:beta': true }),
});Options pattern#
master.options.configure(name, { section, defaults, schema, validate, postConfigure }) declares a typed options block. schema uses the same rules as model binding. Every configured block is validated at master.start() (ValidateOnStart) — a misconfigured app fails at boot with every error listed rather than at the first request.
master.options.configure('Smtp', {
section: 'Smtp', // defaults to the name
defaults: { port: 25, tls: true },
schema: { host: 'string!', port: { type: 'integer', min: 1, max: 65535 }, from: { type: 'email' } }, // same rules as model binding
validate: (v) => (v.tls && v.port === 25 ? 'use port 587 with TLS' : null), // custom rule(s)
postConfigure: (v) => ({ ...v, url: `smtp://${v.host}:${v.port}` }),
});
await master.start(server); // ValidateOnStart: every configured options type is validated here — invalid config fails the boot
master.options.get('Smtp'); // frozen, validated value (sync after start)
await master.options.getAsync('Smtp');
master.options.onChange('Smtp', (value) => { /* IOptionsMonitor.OnChange (after configuration reload / set) */ });In controllers#
Every request gets this.options (all validated values), this.configuration, and this.environment:
export default class MailController {
async send() {
const { host, port } = this.options.Smtp; // validated options
const region = this.configuration.get('App:region'); // raw configuration
if (this.environment.isDevelopment) this.logger.debug('sending via {host}', { host });
// …
}
}Host environment#
master.environmentType (from NODE_ENV in the usual server.js) drives both the config layering and master.environment:
master.environment.name; // 'development' | 'staging' | 'production' | 'test' | …
master.environment.isDevelopment; // IHostEnvironment.IsDevelopment()
master.environment.isStaging;
master.environment.isProduction;
master.environment.isTest;
master.environment.is('qa');Secrets workflow#
Keep secrets out of the repo. In development the Master CLI writes them to ~/.master/usersecrets/<id>/secrets.json; in production use real environment variables. Both land on the same keys, so code only ever reads master.configuration.get('Smtp:password') / this.options.Smtp.password.
master secrets set Smtp:password "s3cret" # ~/.master/usersecrets/my-app/secrets.json
master secrets list
master secrets remove Smtp:password
# production: real environment variables land on the same keys
Smtp__password=s3cret node server.jsconfig/environments/env.development.json keeps its role for server and MasterRecord settings ({ "server": { "httpPort": 3001, "hostname": "127.0.0.1" } }) — it is simply layer 3 of the same configuration now, so master.configuration.get('server:httpPort') works too.Extending controllers & views#
Share helpers across all controllers or the view layer:
master.extendController(SharedHelpers);
master.useView(MyViewAdapter);config/initializers/config.js) are the conventional place to register middleware, services, CORS, and view engines — they run once at startup.