MasterController

Configuration & Options

Layered settings, validated options, and secrets that never touch the repo.

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 CoreMasterController
1appsettings.jsonconfig/appsettings.json
2appsettings.{Environment}.jsonconfig/appsettings.{env}.json
3config/environments/env.{env}.json (the existing server / MasterRecord config, unchanged)
4user secrets (dotnet user-secrets)~/.master/usersecrets/<id>/secrets.jsonmaster.userSecretsId = 'my-app' or MASTER_USER_SECRETS_ID; base dir MASTER_USER_SECRETS_PATH
5environment variables Section__Keysame (__:); master.configurationEnvPrefix = 'MYAPP_' to read only prefixed vars
6command line --Section:Key=valuesame (--a:b=1, --a:b 1, --flag)
7in-memorymaster.configuration.set('Section:Key', value)

Values from env vars and argv are coerced ("true"true, "42"42).

config/appsettings.json
{
  "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#

server.js
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#

javascript
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.

javascript
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:

mailController.js
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:

javascript
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.

terminal
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.js
Server settings still live in env.{env}.json
config/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:

javascript
master.extendController(SharedHelpers);
master.useView(MyViewAdapter);
The init file
Component and app initializers (config/initializers/config.js) are the conventional place to register middleware, services, CORS, and view engines — they run once at startup.