Implement Environment-Specific Configuration in Kraken.js with nconf and Shortstop
A practical guide to layering environment-specific configuration in Kraken.js using nconf, shortstop handlers, and the onConfiguration hook — with validation, async secret injection, and fallback strategies.
28 Jun 2026, 05:56 UTC

Desired Outcome
Configure a Kraken.js application so that each deployment environment (development, test, production) loads its own settings, secrets are injected via environment variables rather than committed to source control, and runtime defaults are available when optional variables are unset. The resolved configuration should be accessible in the onConfiguration hook before middleware initializes.
Prerequisites
- Node.js 18.x or 20.x (Kraken.js targets Express 4.x; verify compatibility with your Kraken release)
- Express 4.x installed as a dependency
- Kraken.js installed (
npm install kraken-js) - Project structure with
config/directory at the repository root
Configuration Layering Overview
Kraken.js uses nconf to merge configuration sources in this order:
config/config.json— base values shared across all environmentsconfig/<NODE_ENV>.json— environment-specific overrides (e.g.,config/production.json)- Environment variables
- Command-line arguments
The merged object is passed to the onConfiguration hook before any middleware is registered.
Step 1: Create Base and Environment Config Files
Create config/config.json with shared defaults and shortstop expressions for values that differ per deployment:
{
"app": {
"name": "my-kraken-app",
"port": "env:PORT||3000",
"logLevel": "info"
},
"database": {
"host": "env:DB_HOST||localhost",
"port": "env:DB_PORT||5432",
"name": "env:DB_NAME||myapp_dev",
"user": "env:DB_USER",
"password": "env:DB_PASSWORD"
},
"redis": {
"url": "env:REDIS_URL||redis://localhost:6379"
}
}Create config/development.json for local development overrides:
{
"app": {
"logLevel": "debug"
},
"database": {
"name": "myapp_dev",
"user": "dev_user",
"password": "dev_password"
}
}Create config/production.json with production-only settings (do not commit secrets here):
{
"app": {
"logLevel": "warn"
},
"database": {
"host": "env:DB_HOST",
"name": "env:DB_NAME",
"user": "env:DB_USER",
"password": "env:DB_PASSWORD"
},
"redis": {
"url": "env:REDIS_URL"
}
}Create config/test.json for test runs:
{
"app": {
"port": 0,
"logLevel": "silent"
},
"database": {
"name": "myapp_test",
"host": "localhost",
"user": "test_user",
"password": "test_password"
}
}Step 2: Register Custom Shortstop Handlers (Optional)
If you need to resolve secrets from a vault or decode base64-encoded values, register custom handlers in the Kraken initialization options. In index.js:
const kraken = require('kraken-js');
const express = require('express');
const { Vault } = require('node-vault'); // example dependency
const app = express();
const vault = new Vault({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
const options = {
onConfiguration: (config, next) => {
// async secret fetching happens here (see Step 3)
next();
},
shortstop: {
vault: (value) => {
// synchronous handler — only for already-fetched values
// prefer async fetch in onConfiguration instead
return value;
},
base64: (value) => Buffer.from(value, 'base64').toString('utf8')
}
};
kraken.init(app, options, (err) => {
if (err) {
console.error('Kraken initialization failed:', err);
process.exit(1);
}
const port = app.kraken.get('app:port');
app.listen(port, () => {
console.log(`Server listening on ${port}`);
});
});Note: Shortstop handlers run synchronously. Do not perform async I/O (vault reads, HTTP calls) inside them. Use onConfiguration for async work.
Step 3: Mutate Configuration in the onConfiguration Hook
The onConfiguration hook receives the fully merged config object. Use it to inject runtime secrets, compute derived values, or validate required settings before middleware initializes.
const options = {
onConfiguration: async (config, next) => {
// Example: fetch a secret from Vault asynchronously
if (process.env.VAULT_ADDR && process.env.VAULT_TOKEN) {
try {
const vault = new Vault({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
const secret = await vault.read('secret/data/myapp');
config.database.password = secret.data.data.db_password;
config.redis.url = secret.data.data.redis_url;
} catch (err) {
console.error('Failed to fetch secrets from Vault:', err);
// Decide whether to fail fast or continue with env fallbacks
// return next(err); // uncomment to fail startup
}
}
// Validate required configuration
const required = ['database.host', 'database.name', 'database.user', 'database.password'];
for (const key of required) {
if (!config.get(key)) {
return next(new Error(`Missing required config: ${key}`));
}
}
// Compute derived values
config.app.dbUrl = `postgresql://${config.database.user}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.name}`;
next();
},
shortstop: {
base64: (value) => Buffer.from(value, 'base64').toString('utf8')
}
};Inside the hook, config is an nconf provider instance. Use config.get('path:to:value') and config.set('path:to:value', newValue) to read and write. Call next() (or next(err)) to continue bootstrapping.
Step 4: Access Resolved Configuration in Middleware and Routes
Pass configuration explicitly to middleware rather than relying on global accessors. In a middleware module:
// middleware/db.js
module.exports = (config) => {
const { Pool } = require('pg');
const pool = new Pool({
host: config.get('database:host'),
port: config.get('database:port'),
database: config.get('database:name'),
user: config.get('database:user'),
password: config.get('database:password'),
max: 20
});
return (req, res, next) => {
req.db = pool;
next();
};
};Register it after Kraken initializes, using the config from the app instance:
kraken.init(app, options, (err) => {
if (err) { /* handle error */ }
const config = app.kraken.get('config'); // nconf provider
app.use(require('./middleware/db')(config));
// ... routes
app.listen(app.kraken.get('app:port'));
});Expected Checks
Verify Layering Order
Temporarily add a log statement in onConfiguration to confirm the merge order:
onConfiguration: (config, next) => {
console.log('Resolved config:', JSON.stringify(config.get(), null, 2));
next();
}Start the app with different NODE_ENV values and confirm:
- Values from
config.jsonappear as baselines - Environment-specific files override base values
- Environment variables override file values
- Shortstop
||defaultfallbacks apply when env vars are unset
# Test development (uses config/development.json)
NODE_ENV=development node index.js
# Test production with env overrides
NODE_ENV=production DB_HOST=prod-db.example.com DB_PASSWORD=secret node index.js
# Test fallback when optional env var is unset
NODE_ENV=production node index.js
# Should use config.json default for PORT (3000)Validate Required Secrets
Unset a required environment variable (e.g., DB_PASSWORD) in production and confirm the validation in onConfiguration fails startup with a clear error message.
Recovery Options
Configuration Not Loading
- Verify
config/directory exists at the project root (same level asindex.js) - Confirm
NODE_ENVmatches a file inconfig/(e.g.,production→config/production.json) - Check for JSON syntax errors in config files — Kraken will throw on parse failure
Shortstop Expression Not Resolving
- Ensure the expression uses correct syntax:
"env:VAR_NAME||default"(no spaces around||) - Verify the environment variable name matches exactly (case-sensitive)
- Custom handlers must be registered in the
shortstopoption map beforekraken.init
Async Secret Fetch Fails
- If Vault or external secret store is unreachable, decide between failing fast (
next(err)) or falling back to environment variables - Log the error with context; do not silently continue with empty secrets
- Consider a health-check endpoint that validates secret availability at runtime
Limitations and Version Notes
- Kraken.js is not actively maintained; verify Express and Node.js version compatibility before upgrading
- Shortstop resolution is synchronous — async work must live in
onConfiguration - If
config/<NODE_ENV>.jsonis missing, Kraken falls back to baseconfig.jsonwithout warning; always create explicit files for each deployment environment - The
app.kraken.get('config')accessor is undocumented; prefer passing the config object explicitly to modules
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.