Configuration
The Problem
A database tool needs connection details: host, port, credentials, paths. But where should these come from? Hardcoding is inflexible. Environment variables alone don't persist. Config files risk credential leaks.
noorm solves this with layered configuration. Multiple sources merge together with clear precedence. Sensitive data stays encrypted. Environment variables enable CI/CD without changing stored configs.
Configuration Sources
Configs come from five sources, merged in priority order:
| Priority | Source | Purpose |
|---|---|---|
| 1 (highest) | CLI flags | One-time overrides |
| 2 | Environment variables | CI/CD, per-session tweaks |
| 3 | Stored config | Your saved database configs |
| 4 | Stage defaults | Team-defined templates |
| 5 (lowest) | Defaults | Sensible fallbacks |
Higher priority sources override lower ones. This means you can set a base config, override the host via environment for CI, and override the port via CLI for a specific run.
import { resolveConfig } from './core/config'
// Merges all sources into final config
const config = resolveConfig(state, {
name: 'dev',
flags: { connection: { port: 5433 } }, // Override just the port
})Config Structure
A complete config defines everything needed to connect and run:
interface Config {
name: string // Unique identifier: 'dev', 'staging', 'prod'
type: 'local' | 'remote' // Connection type
isTest: boolean // Test database flag
protected: boolean // Requires confirmation for dangerous ops
connection: {
dialect: 'postgres' | 'mysql' | 'sqlite' | 'mssql'
host?: string // Required for non-SQLite, defaults to 'localhost'
port?: number // Default by dialect
database: string // Database name or file path
user?: string
password?: string
ssl?: boolean | SSLConfig
pool?: { min?: number, max?: number } // Defaults to { min: 0, max: 10 }
}
paths: {
schema: string // Path to schema files
changes: string // Path to change files
}
identity?: string // Override audit identity
}Environment Variables
Every config property maps to an environment variable using nested naming. Underscores separate nesting levels:
NOORM_{PATH}_{TO}_{VALUE} → { path: { to: { value: '' } } }Connection variables:
| Variable | Config Path | Notes |
|---|---|---|
NOORM_CONNECTION_DIALECT | connection.dialect | postgres, mysql, sqlite, mssql |
NOORM_CONNECTION_HOST | connection.host | |
NOORM_CONNECTION_PORT | connection.port | Auto-parsed as integer |
NOORM_CONNECTION_DATABASE | connection.database | |
NOORM_CONNECTION_USER | connection.user | |
NOORM_CONNECTION_PASSWORD | connection.password | Kept as string |
NOORM_CONNECTION_SSL | connection.ssl | Use 'true'/'false' |
NOORM_CONNECTION_POOL_MIN | connection.pool.min | |
NOORM_CONNECTION_POOL_MAX | connection.pool.max |
Path variables:
| Variable | Config Path |
|---|---|
NOORM_PATHS_SQL | paths.sql |
NOORM_PATHS_CHANGESETS | paths.changes |
Top-level variables:
| Variable | Config Path | Notes |
|---|---|---|
NOORM_NAME | name | |
NOORM_TYPE | type | 'local' or 'remote' |
NOORM_PROTECTED | protected | Use 'true'/'false' |
NOORM_IDENTITY | identity | |
NOORM_isTest | isTest | camelCase preserved |
Note: For camelCase properties like isTest, preserve the case: NOORM_isTest (not NOORM_IS_TEST).
Behavior variables (not merged into config):
| Variable | Purpose |
|---|---|
NOORM_CONFIG | Which config to use |
NOORM_YES | Skip confirmations |
NOORM_JSON | JSON output mode |
# CI/CD example: use stored config with overridden host
export NOORM_CONFIG=staging
export NOORM_CONNECTION_HOST=db.ci-runner.local
noorm run buildConfig Resolution
The resolver determines which config to use and merges all sources.
const config = resolveConfig(state, options)Resolution follows this flow:
Determine config name from (in order):
options.name(explicit)NOORM_CONFIGenv var- Active config in state
If no name found, check if env vars provide enough to run:
- Need at least
NOORM_CONNECTION_DIALECTandNOORM_CONNECTION_DATABASE - If yes, build config from env only (named
__env__) - If no, return
null
- Need at least
Load stored config by name (throws if not found)
Merge sources: defaults ← stage ← stored ← env ← flags
Validate the merged result
// Explicit name
resolveConfig(state, { name: 'production' })
// From NOORM_CONFIG
process.env.NOORM_CONFIG = 'staging'
resolveConfig(state) // uses 'staging'
// From active config
state.setActiveConfig('dev')
resolveConfig(state) // uses 'dev'
// Env-only (CI mode)
process.env.NOORM_CONNECTION_DIALECT = 'postgres'
process.env.NOORM_CONNECTION_DATABASE = 'ci_test'
resolveConfig(state) // creates __env__ configValidation
Configs are validated using Zod schemas. Key rules:
name- Required, alphanumeric with hyphens/underscoresconnection.dialect- Must be one of the four supportedconnection.host- Required for non-SQLiteconnection.port- Integer 1-65535connection.database- Required
Default ports by dialect:
| Dialect | Default Port |
|---|---|
| postgres | 5432 |
| mysql | 3306 |
| mssql | 1433 |
| sqlite | N/A |
import { validateConfig, parseConfig } from './core/config'
// Throws on invalid config
validateConfig(config)
// Returns config with defaults applied
const full = parseConfig(partial)Protected Configs
Production databases need safeguards. Protected configs require confirmation for dangerous operations and block some entirely.
const config = {
name: 'prod',
protected: true,
// ...
}Action classification:
| Action | Protected Behavior |
|---|---|
change:run | Requires confirmation |
change:revert | Requires confirmation |
change:ff | Requires confirmation |
change:next | Requires confirmation |
run:build | Requires confirmation |
run:file | Requires confirmation |
run:dir | Requires confirmation |
db:create | Requires confirmation |
db:destroy | Blocked entirely |
config:rm | Requires confirmation |
Check protection before executing:
import { checkProtection } from './core/config'
const check = checkProtection(config, 'change:run')
if (!check.allowed) {
console.error(check.blockedReason)
process.exit(1)
}
if (check.requiresConfirmation) {
const input = await prompt(`Type "${check.confirmationPhrase}" to confirm:`)
if (input !== check.confirmationPhrase) {
process.exit(1)
}
}
// Proceed with actionSkip confirmations in CI with NOORM_YES=1:
export NOORM_YES=1
noorm change run # No prompt, even on protected configStages
Stages are team-defined config templates from settings.yml. They provide defaults and enforce constraints.
# .noorm/settings.yml
stages:
prod:
description: Production database
locked: true # Cannot delete this config
defaults:
dialect: postgres
protected: true # Cannot be overridden to false
secrets:
- key: DB_PASSWORD
type: password
required: trueWhen resolving a config linked to a stage, stage defaults merge in:
const config = resolveConfig(state, {
name: 'prod',
stage: 'prod', // Required when passing settings - must explicitly specify stage name
settings: settingsManager,
})
// Stage defaults applied, then stored config, then env, then flagsNote: When passing
settings, you must also passstageexplicitly. Auto-detection of stage from config name is not yet implemented.
Config Completeness
A config is "complete" when all required secrets (from its stage) are set. Incomplete configs have limited functionality.
import { checkConfigCompleteness } from './core/config'
// With explicit stage name (recommended)
const check = checkConfigCompleteness(config, state, settings, 'prod')
// Without stage name - only works if stage name matches config name exactly
const check = checkConfigCompleteness(config, state, settings)
if (!check.complete) {
console.log('Missing secrets:', check.missingSecrets)
console.log('Constraint violations:', check.violations)
}Home Screen Status
The home screen displays setup status for all stage-linked configs:
Stage Configs:
✓ dev
✓ staging
prod ✗ secrets (2)✓indicates all required secrets are set✗ secrets (N)shows how many secrets are missing
This helps track which environments are ready to use and which need secret values configured.
Stage constraints that can't be violated:
| Constraint | Behavior |
|---|---|
protected: true in defaults | Cannot set protected: false |
isTest: true in defaults | Cannot set isTest: false |
locked: true | Config cannot be deleted |
import { canDeleteConfig } from './core/config'
// Basic usage
const { allowed, reason } = canDeleteConfig('prod', settings)
// With explicit stage name (optional 3rd parameter)
const { allowed, reason } = canDeleteConfig('prod', settings, 'production')
if (!allowed) {
console.error(reason) // "Config 'prod' is linked to a locked stage..."
}CI/CD Mode
In CI pipelines, configs can be built entirely from environment variables:
# GitHub Actions example
env:
NOORM_CONNECTION_DIALECT: postgres
NOORM_CONNECTION_HOST: ${{ secrets.DB_HOST }}
NOORM_CONNECTION_DATABASE: ${{ secrets.DB_NAME }}
NOORM_CONNECTION_USER: ${{ secrets.DB_USER }}
NOORM_CONNECTION_PASSWORD: ${{ secrets.DB_PASSWORD }}
NOORM_YES: 1
steps:
- run: noorm run buildMinimum required env vars:
NOORM_CONNECTION_DIALECTNOORM_CONNECTION_DATABASE
// Check if in CI mode
import { isCi, shouldSkipConfirmations } from './core/environment'
if (isCi()) {
// Running in CI environment
}
if (shouldSkipConfirmations()) {
// NOORM_YES is set
}Observer Events
Config operations emit events:
observer.on('config:created', ({ name }) => {
console.log(`Created config: ${name}`)
})
observer.on('config:updated', ({ name, fields }) => {
console.log(`Updated ${name}: ${fields.join(', ')}`)
})
observer.on('config:deleted', ({ name }) => {
console.log(`Deleted config: ${name}`)
})
observer.on('config:activated', ({ name, previous }) => {
console.log(`Switched from ${previous} to ${name}`)
})Config Summary
For listings, use ConfigSummary which omits sensitive connection details:
const summaries = state.listConfigs()
// [
// { name: 'dev', type: 'local', isTest: false, protected: false, isActive: true, dialect: 'postgres', database: 'dev_db' },
// { name: 'prod', type: 'remote', isTest: false, protected: true, isActive: false, dialect: 'postgres', database: 'prod_db' },
// ]The ConfigSummary interface:
interface ConfigSummary {
name: string
type: 'local' | 'remote'
isTest: boolean
protected: boolean
isActive: boolean
dialect: Dialect
database: string
}