Config

Schema-validated environment configuration. Operators read process.env throughout their app; a typo in the key name OR a value in the wrong shape (port="abc", flag="yas") surfaces three days later as a mysterious 500. b.config.create validates env at boot through b.safeSchema so the app refuses to start with broken config — the throw happens at create() time, not at the first request that touches the broken value.

b.config.coerce.number() and b.config.coerce.boolean() wrap schema leaves with the env-friendly preprocessors most operators want (env values are always strings at the source). loadDbBacked composes create with periodic DB-row polling so a row update in _blamejs_config_overrides surfaces without restart, and falls back to the last-good value on validation failure.

b.config.create(opts) #

stable0.8.0
{
  schema:      b.safeSchema instance (required; built via b.safeSchema.object({...})),
  env:         object  (env bag; default process.env),
  redactKeys:  Array  (keys masked by `.redacted()` for log output),
}

Validate env against a b.safeSchema shape and return a frozen config handle (value / get / has / redacted / subscribe / reload). Throws ConfigError synchronously when validation fails — the operator sees broken config at boot rather than at the first request that touches the value. The handle's reload(overlay) applies a new env-shaped overlay on top of the validated baseline, notifies subscribers on success, and falls back to the prior value on failure.

var s = b.safeSchema;
var cfg = b.config.create({
  schema: s.object({
    NODE_ENV: s.enum_(["development", "test", "production"]),
    PORT:     b.config.coerce.number().default(3000),
  }),
  env: { NODE_ENV: "production", PORT: "8080" },
  redactKeys: [],
});
cfg.value.NODE_ENV;     // → "production"
cfg.value.PORT;         // → 8080  (Number, not "8080")
cfg.has("PORT");        // → true

b.config.loadDbBacked(opts) #

stable0.8.0
{
  schema:         b.safeSchema instance (required),
  env:            object  (env baseline; default process.env),
  redactKeys:     Array,
  fetchRows:      async () => Array<{ key: string, value: string }>  (required),
  intervalMs:     number   (positive finite poll interval),
  transformValue: (row) => string | Promise   (optional per-row
                  transform — receives `{ key, value, ...rest }` so the
                  row can carry envelope metadata; returns the value
                  that flows into the schema. Common shape: unseal a
                  `b.vault`-sealed ciphertext column before validation.
                  Rows whose transform throws or returns a non-string
                  are skipped with a `config.reload.failed` audit so a
                  single bad row never crashes the poller),
  audit:          boolean  (default true; set false to silence the
                  per-poll config.reload.* audit emissions),
}

Compose b.config.create with a periodic DB-row fetch. Operators keep canonical config values in _blamejs_config_overrides(key TEXT PRIMARY KEY, value TEXT); this helper polls every intervalMs, applies the rows as an overlay via the underlying handle's reload, and re-validates. Reload failures emit a config.reload.failed audit row but DO NOT clobber the previous value — the running app stays on the last-good config.

Returns immediately with a synchronous handle, but kicks off one immediate hydration tick on construction so the first DB read happens at t=0 rather than t=intervalMs. Callers that need to wait for first-data-applied can await handle.hydrated before the app starts serving traffic; the Promise resolves after the first tick settles (success OR audit-on-failure path) and never rejects, so the boot path never deadlocks on a temporarily-unreachable DB.

The returned handle is the same shape as create() plus: - .hydrated — Promise for the first tick - .refresh()— run one tick on demand (save-triggered reload); returns Promise that never rejects - .stop() — halts the poller

Three tiers of precedence (highest wins): the DB-row overlay resolved at each _tick > the opts.env baseline > defaults declared on the schema (s.string().default(...) and friends). The .subscribe(fn) callback registered through create() fires synchronously inside every successful reload — operators reach for it to invalidate caches, recompute derived state, or hot-rebuild middleware that closed over the previous config value.

var s = b.safeSchema;
var cfg = b.config.loadDbBacked({
  schema: s.object({
    FEATURE_X: b.config.coerce.boolean().default(false),
  }),
  env:        { FEATURE_X: "false" },
  fetchRows:  async function () {
    return [{ key: "FEATURE_X", value: "true" }];
  },
  intervalMs: 60 * 1000,
});
cfg.value.FEATURE_X;    // → false  (until first poll tick lands)
cfg.stop();             // halt the poller on shutdown

// Sealed values — column stores `b.vault.seal(plain)` ciphertext.
var cfg = b.config.loadDbBacked({
  schema:     s.object({ STRIPE_SECRET: s.string() }),
  fetchRows:  async function () {
    return await db.all("SELECT key, sealed FROM _config WHERE sealed IS NOT NULL");
  },
  transformValue: function (row) {
    return b.vault.unseal(row.sealed).toString("utf8");
  },
  intervalMs: 30 * 1000,
});

// Save-triggered reload — admin UI writes a row, fires refresh()
// so the new value is active immediately without waiting for
// intervalMs. cfg.subscribe(...) sees the change inline.
var cfg = b.config.loadDbBacked({
  schema:     s.object({ FEATURE_X: b.config.coerce.boolean().default(false) }),
  fetchRows:  async function () { return await db.all("SELECT key, value FROM _config"); },
  intervalMs: 5 * 60 * 1000,                  // safety-net interval
});
await cfg.hydrated;                            // boot path waits
cfg.subscribe(function (next) { cache.invalidate(); });

adminApp.post("/settings", async function (req, res) {
  await db.run("INSERT OR REPLACE INTO _config(key,value) VALUES (?,?)",
               req.body.key, req.body.value);
  await cfg.refresh();                         // active immediately
  res.json({ ok: true });
});

Last updated 2026-08-08T16:39:49.652Z by seeder.