config-pulse
v1.0.1
Published
Push-based runtime config client backed by a single MongoDB document. Pushes runtime/business config into running services without restarts.
Maintainers
Readme
config-pulse
Framework-agnostic, push-based runtime configuration client backed by a single MongoDB document.
The intended architecture separates static startup values from runtime-managed values:
- Kubernetes env/ConfigMaps are valid for static service-owned values that runtime pushes cannot override.
- Runtime/business config is pushed by the configuration-management service through the service well-known API.
- Runtime pushes are saved as
current, even when the running service version does not recognize every pushed key yet. - The service resolves effective values from hardcoded entries, static startup values, runtime
current, and hardcoded defaults, then exposes the resolved state for observability.
The package uses one MongoDB collection and one document:
- collection:
__config - document id:
runtime
The document stores service-declared entries, the current pushed config, the resolved effective config, and the state of service pods. A service can start in receiver-only mode when config is missing, receive config through PUT /internal/runtime-config, persist it, and become ready when all required entries are resolved.
Compact shape:
{
"_id": "runtime",
"static": {
"entries": {
"timeoutMs": {
"scope": "runtime",
"type": "number",
"required": true,
"description": "Request timeout",
"defaultValue": 1000
},
"serviceUrl": {
"scope": "static",
"type": "string",
"required": true,
"value": "http://orders"
}
}
},
"current": {
"serviceName": "orders",
"version": 7,
"checksum": "...",
"config": {
"timeoutMs": 2500
}
},
"resolved": {
"config": {
"timeoutMs": 2500,
"serviceUrl": "http://orders"
},
"keys": {
"timeoutMs": {
"status": "used",
"source": "runtime",
"scope": "runtime",
"type": "number",
"required": true,
"description": "Request timeout"
}
},
"errors": {
"missing": [],
"unrecognized": []
}
},
"pods": {}
}Components
EnvConfigLoader: reads and validates required bootstrap env values (loadthrows,loadOrExitprints and exits).configPulse: typed entry builders for static/runtime strings, numbers, booleans, and JSON values.createConfigPulse: high-level setup helper that wires store, manager, receiver, status reporter, handlers, middleware, health, subscriptions, and shutdown.ConfigDocumentStore: reads and updates the single__config/runtimedocument.RuntimeConfigManager: registers service-declared entries, resolves effective config, owns readiness state, and notifies whole-config and per-key subscribers whenversionorchecksumchanges.PodStateManager: updates only the current pod's nested state and stampslastSeenAtwhen that pod applies, fails, enters receiver-only mode, or shuts down. It does not run heartbeats.ConfigPushReceiver: validates pushed config (serviceName, version, checksum), persists strictly newer versions, applies them, and returns a durable ack. It also accepts notification-only reload requests that verify Mongocurrentbefore applying locally.ConfigStatusReporter: summarizes pod divergence and readiness across all known pod records.calculateConfigChecksum: deterministic SHA-256 over a stable-stringified config; use it to build thechecksumfield of a push payload.createRuntimeConfigHealthStatus: bridges runtime config readiness into the existinghealthCheckHandler(serverStatus)shape.createRuntimeConfigHttpHandlers: exposes framework-neutral push, status, and readiness handlers.
Bootstrap Env
import { EnvConfigLoader } from 'config-pulse';
const bootstrap = EnvConfigLoader.loadOrExit({
SERVICE_NAME: { required: true },
MONGO_URI: {
required: true,
type: 'connectionString',
sensitive: true,
},
CONFIG_RECEIVER_TOKEN: { required: true },
PORT: { required: true, type: 'number' },
});The loader exits early for missing or invalid required bootstrap values. Static values loaded from env/ConfigMaps are service-owned startup values. Runtime pushes cannot override keys declared with scope: "static"; static keys present in pushed current.config are reported under resolved.errors.unrecognized.
Service Wiring
Recommended setup: declare each key once with defineConfig(), then pass the generated entries to createConfigPulse().
import {
configPulse,
createConfigPulse,
} from 'config-pulse';
export const serviceConfig = configPulse.defineConfig({
static: {
serviceName: configPulse.staticString({
env: ['SERVICE_NAME', 'APP_NAME'],
defaultValue: 'orders',
}),
serviceUrl: configPulse.staticString({
env: 'SERVICE_URL',
required: true,
description: 'Base URL from deployment config',
}),
},
runtime: {
timeoutMs: configPulse.runtimeNumber({
env: 'TIMEOUT_MS',
required: true,
defaultValue: 1000,
description: 'Request timeout controlled by config-manager',
}),
logLevel: configPulse.runtimeString({
env: 'LOG_LEVEL',
defaultValue: 'info',
}),
},
}, { exitCode: 2 });
export const staticConfig = serviceConfig.staticConfig;
export const runtimeConfig = serviceConfig.runtimeConfig;
export const config = serviceConfig.config;
export const applyRuntimeConfig = serviceConfig.applyRuntimeConfig;
const runtime = await createConfigPulse({
mongo: mongo.db(),
serviceName: staticConfig.serviceName,
podId: process.env.HOSTNAME!,
receiverToken: process.env.RUNTIME_CONFIG_RECEIVER_TOKEN,
entries: serviceConfig.getEntries(),
onChange: ({ config: nextConfig }) => {
applyRuntimeConfig(nextConfig);
},
onKeyChange: {
logLevel: ({ currentValue }) => {
// Reconfigure only the dependency that uses logLevel.
},
},
});
app.use(runtime.koaMiddleware());
app.use(healthCheckHandler(runtime.health));
process.on('SIGTERM', async () => {
await runtime.stop();
});Low-level setup remains available when a service needs custom composition:
import {
ConfigDocumentStore,
ConfigPushReceiver,
ConfigStatusReporter,
PodStateManager,
RuntimeConfigManager,
createRuntimeConfigHealthStatus,
createRuntimeConfigHttpHandlers,
} from 'config-pulse';
const store = new ConfigDocumentStore(mongo.db());
const podStateManager = new PodStateManager({
store,
serviceName: 'orders',
podId: process.env.HOSTNAME!,
});
const manager = new RuntimeConfigManager({
store,
podStateManager,
serviceName: 'orders',
podId: process.env.HOSTNAME!,
});
await manager.registerEntries([
{
key: 'serviceUrl',
scope: 'static',
type: 'string',
required: true,
description: 'Base URL from deployment config',
value: process.env.SERVICE_URL,
},
{
key: 'timeoutMs',
scope: 'runtime',
type: 'number',
required: true,
description: 'Request timeout controlled by config-manager',
defaultValue: 1000,
},
]);
await manager.start();
manager.subscribe(({ config }) => {
// Reconfigure in-memory dependencies here, such as logger level or feature flags.
});
manager.subscribe('logging', ({ currentValue }) => {
// Reconfigure only the dependency that uses config.logging.
});
// Existing services can keep using healthCheckHandler by passing this derived status.
const healthStatus = createRuntimeConfigHealthStatus(serverStatus, manager);
app.use(healthCheckHandler(healthStatus));
const receiver = new ConfigPushReceiver({ serviceName: 'orders', store, manager });
const runtimeConfigHandlers = createRuntimeConfigHttpHandlers({
receiver,
notifier: receiver,
podStatePruner: { prune: (podIds) => store.deletePodStates(podIds) },
statusReporter: new ConfigStatusReporter({ store }),
readiness: () => manager.isReady(),
});Call runtimeConfigHandlers.push(body), runtimeConfigHandlers.notify(body), runtimeConfigHandlers.prune(body), runtimeConfigHandlers.status(), and runtimeConfigHandlers.readiness() from the service's own routing layer. Each returns { status, body } so any framework can map it to a response.
For Koa services, use the dependency-free adapter. It does not import Koa or require Koa as a package dependency; it only expects the small context shape used below.
app.use(createKoaRuntimeConfigMiddleware({
handlers: runtimeConfigHandlers,
authorize: (ctx) => ctx.get?.('authorization') === `Bearer ${process.env.RUNTIME_CONFIG_RECEIVER_TOKEN}`,
}));Routes
Suggested mapping (paths are yours to choose):
PUT /internal/runtime-config→push(body): validates, persists strictly newer versions, applies them, and returns aConfigPushAck. Status is202whenack.ok, otherwise400.POST /internal/runtime-config/notify→notify(body): reloads Mongocurrent, verifiesserviceName,version, andchecksum, applies synchronously, updates this pod's state, and returns aConfigPushAck. Status is202whenack.ok, otherwise409.POST /internal/runtime-config/pods/prune→prune(body): removes stale pod records named bybody.podIds. This is called by configuration-management after it compares stored pod state with current EndpointSlice pod names.GET /internal/runtime-config/pods→status(): returns200with theConfigStatusSummary(currentpayload,resolvedview, per-statuscounts, and per-podpods).GET /internal/runtime-config/readiness→readiness(): returns200with{ ready: true }when config is loaded, otherwise503.
Push Payload
A push is a RuntimeConfigPayload: { serviceName, version, checksum, config } (plus optional packageId, pushedAt, appliedAt). Build the checksum with the exported helper so it matches what the receiver recomputes:
import { calculateConfigChecksum } from 'config-pulse';
const config = { logging: { level: 'debug' }, featureX: true };
const payload = {
serviceName: 'orders',
version: 7,
checksum: calculateConfigChecksum(config),
config,
};The receiver rejects the push (ok: false) on a serviceName mismatch, a non-integer/negative version, or a checksum mismatch. A re-push of the current version is idempotent: it acks ok: true, applied: false. Only a strictly higher version is persisted.
Config-shape validation is non-blocking:
- unknown pushed keys are saved in
current.config, reported inerrors.unrecognized, and excluded fromresolved.config; - pushed keys declared as
scope: "static"are saved incurrent.config, reported inerrors.unrecognized, and do not override the static value; - missing required entries are reported in
errors.missingand mark the pod failed/not ready.
Example ack:
{
"ok": true,
"durable": true,
"accepted": true,
"persisted": true,
"applied": false,
"version": 7,
"currentVersion": 7,
"errors": {
"missing": ["apiToken"],
"unrecognized": ["futureFlag", "serviceUrl"]
}
}Notification Payload
A notification is a small payload sent after one pod has seeded Mongo with the full config:
{
serviceName: 'orders',
packageId: 'pkg-id',
version: 7,
checksum: '...'
}The notified pod reads __config/runtime.current from Mongo, verifies the stored version/checksum matches the notification, applies that config in-process, and writes its own pod state. It does not trust the notification as the config source.
Pod Status Lifecycle
ConfigStatusReporter derives each pod's status from the pod record:
receiver_only: started without matching config; accepting pushes but not ready.ready: latest config loaded and applied.diverged: marked ready but on an olderversion/checksumthancurrent.failed: a subscriber threw while applying config.down: explicitly marked down on shutdown.
There is no heartbeat loop and no time-based expiry. Pods that disappear permanently are removed by configuration-management: reconciliation watches current EndpointSlice membership, computes stored pod names that are no longer current, and calls POST /internal/runtime-config/pods/prune on a current pod to delete those stale nested records from the service's Mongo document.
