This module is ESM 🔆. Please read this.
📖 Full documentation & guides: onury.io/configuard
Builds a nested, typed configuration object from a flat list of
configuration items (typically rows from a config database table) — with
${...} value templating, reusable option lists, and accessor-based
(ABAC) filtering. Built on
notation.
import { Configuard, AccessorType } from 'configuard';
const rows = [
{ accessor: 'system', key: 'company.name', type: 'string', listType: 'none', value: 'Acme', editable: true, requiresReboot: false, encrypt: false },
{ accessor: 'system', key: 'ai.vision.provider', type: 'string', listType: 'none', value: 'gemini', editable: true, requiresReboot: false, encrypt: false }
];
const cfg = new Configuard(rows, { accessor: AccessorType.SYSTEM });
cfg.data; // { company: { name: 'Acme' }, ai: { vision: { provider: 'gemini' } } }
cfg.get<string>('ai.vision.provider'); // 'gemini' (a scalar, not ['gemini'])
cfg.has('company.name'); // trueWhy a vertical key → value config table? Configuration is a long,
ever-growing list of individual settings that changes across environments and
over the life of a product. A tall key/value table lets you add, edit, or remove
one setting at a time — each row carrying its own metadata (type, access
level, options, requiresReboot, editable, …) — and administer it all from a
UI without schema migrations. That's far more maintainable than wide,
one-column-per-setting tables or settings scattered across files.
But flat rows are great to store and administer, not to consume: values are strings, related keys are scattered, and the same value is often duplicated. Configuard sits across the whole lifecycle:
- Store — keep your settings as
IConfigItemrows in oneconfigtable. - Build & use safely —
new Configuard(rows)produces a nested, type-cast, ABAC-filtered object for your runtime. It is frozen by default (immutable), so backend code can't accidentally mutate live config. Read it via.data/.get()/.has(). - Edit in a UI —
Configuard.parseFlat()returns the same flat list with templates resolved and option lists expanded — ideal for an admin/editor UI where each row is a form field with its own allowed values. - Save back —
Configuard.serializeFlat()validates the admin's edits, serializes them to DB strings (optionally re-encrypting), and returns the diff of changed rows to persist to theconfigtable.
Configuard also fails loud: a corrupt config row throws immediately at construction rather than silently producing a partial object (see Validation).
npm i configuardnotation is a runtime dependency and is installed automatically.
- Flat → nested + typed. Each item's
keyis a dot/bracket notation (ai.vision.provider); its stringvalueis parsed according totype(see Value types) and assembled into a nested object vianotation. - Scalar by default. A value is parsed to its declared type —
"gemini"stays the string"gemini". Lists are opt-in vialistType(see List types). - Templating. Values may reference other keys:
"${files.content}/img"or"${port}". References are resolved recursively; circular references are detected (no infinite recursion) and missing references are reported. - Option lists.
@-prefixed rows define reusable lists of allowed values that other rows point to from theiroptionsfield (see Option lists). - ABAC.
accessor(system/application/all) plus a bitwiseappAccessvs the client'sappLeveldecide, per item, what a given client may see (see Access control). - Section markers. Keys whose last note starts with
$(e.g.cp.config.$title) are treated as section metadata and skipped. - Immutable by default. The built object is deep-frozen, so it can't be
mutated at runtime. Pass
{ lock: false }for a mutable result; check.isLockedto see the current state. - Fail loud. A corrupt row (unparseable value, missing/circular template, or malformed item) throws at construction — never a silent partial build (see Validation).
Each item declares an accessor — system, application, or all — and the
client constructing the Configuard declares its own accessor. Only items the
client is allowed to see are included in the built object:
- A
systemclient seessystemandallitems. - An
applicationclient seesallitems, plusapplication/allitems whoseappAccessbit flags intersect the client'sappLevel(a bitwise&).applicationitems must declare anappAccess, and anapplicationclient must be constructed with anappLevel.
import { Configuard, AccessorType } from 'configuard';
// Application client flags (bitwise).
const WEB = 1 << 0; // 0b001
const MOBILE = 1 << 1; // 0b010
const KIOSK = 1 << 2; // 0b100
const rows = [
{ accessor: 'application', appAccess: WEB | MOBILE, key: 'ui.theme', type: 'string', listType: 'none', value: 'dark', editable: true, requiresReboot: false, encrypt: false },
{ accessor: 'application', appAccess: KIOSK, key: 'kiosk.timeout', type: 'integer', listType: 'none', value: '30', editable: true, requiresReboot: false, encrypt: false },
{ accessor: 'all', appAccess: null, key: 'app.name', type: 'string', listType: 'none', value: 'Acme', editable: true, requiresReboot: false, encrypt: false }
];
// A mobile client (appLevel = MOBILE):
const cfg = new Configuard(rows, { accessor: AccessorType.APPLICATION, appAccess: MOBILE });
cfg.has('ui.theme'); // true — (WEB|MOBILE) & MOBILE !== 0
cfg.has('kiosk.timeout'); // false — KIOSK & MOBILE === 0
cfg.has('app.name'); // true — `all` item, no appAccessCombine with accesscontrol for
property-level filtering of the built object.
Configuration is foundational, so Configuard treats a corrupt row as a hard error: the constructor throws immediately rather than logging a warning and building a partial object. It throws when:
- a
valuecan't be parsed to its declaredtype(e.g. badjson/hexadecimal/time/date, or anumberthat resolves toNaN); - a
${...}template reference is missing or circular; - an item is malformed (invalid
accessor/listType/type, or a non-stringkey).
try {
const cfg = new Configuard(rows, { accessor: AccessorType.SYSTEM });
// use cfg.data / cfg.get(...)
} catch (err) {
// e.g. 'Configuard: Value "abc" of key "port" is not a valid number.'
// the original parser error (when any) is available as `err.cause`.
}Warning
parseFlat() likewise throws on missing/circular templates, a missing option
list, or a value outside its option list.
All failures throw a ConfiguardError (exported from the package root), so
consumers can react to a configuration fault specifically:
import { Configuard, ConfiguardError } from 'configuard';
try {
new Configuard(rows, { accessor: AccessorType.SYSTEM });
} catch (err) {
if (err instanceof ConfiguardError) {
err.key; // the offending config item key, when known
err.cause; // the underlying error (e.g. the parser failure), when any
}
}The object returned by build() (the constructor) is deep-frozen by default —
every nested object and array is recursively Object.freezed — so runtime config
can't be mutated by accident. Opt out with { lock: false }:
const locked = new Configuard(rows, { accessor: AccessorType.SYSTEM });
locked.isLocked; // true
Object.isFrozen(locked.data); // true
// locked.data.x = 1; // throws in strict mode
const mutable = new Configuard(rows, { accessor: AccessorType.SYSTEM }, { lock: false });
mutable.isLocked; // falseItems flagged encrypt: true can be stored encrypted at rest. Configuard is
crypto-agnostic — you supply a synchronous decrypt hook, which it applies
(before templating/parsing) to those items while building:
const cfg = new Configuard(rows, { accessor: AccessorType.SYSTEM }, {
decrypt: (value, item) => myDecrypt(value) // return the plaintext string
});
cfg.isEncrypted('db.password'); // true
cfg.get('db.password'); // the decrypted plaintext valueDecryption is opt-in: without a decrypt hook, encrypt: true values are
used as-is. A failing hook throws a ConfiguardError. (Re-encrypting edited
values on save is handled by serializeFlat() — see below.)
type declares how a row's raw string value is parsed. See the ValueType
enum.
type |
Parses into | Notes |
|---|---|---|
null |
null |
|
string |
string |
Value used as-is. |
boolean |
boolean |
"true"/"1" → true. |
number |
number |
Integer or float. |
integer |
number |
Base-10 integer. |
float |
number |
Floating-point. |
hexadecimal |
number |
Hex string, with or without 0x. Invalid hex throws. |
datetime |
Date |
Date and time (RFC 2822 / ISO 8601). |
date |
string |
Calendar date without a time part (e.g. "2026-06-15"). Kept as the validated string; a value with a time, or an invalid date, throws. |
time |
string |
Clock time HH:mm or HH:mm:ss (e.g. "14:30"). Kept as the validated string; out-of-range values like "90:77" throw. |
regexp |
RegExp |
/pattern/flags or a plain pattern. |
json |
any |
JSON.parse (object, array, …). |
any |
inferred | Best-effort auto-detection. |
Note
date and time are intentionally kept as validated strings (not Date
objects), since they carry no full timestamp — use datetime when you need a
Date.
listType controls whether a value is a single value or a list.
listType |
Result | Empty value → |
|---|---|---|
none |
A single parsed value of type. |
null (or "" for string). |
array |
An array of values, each parsed to type (value is split on commas). |
[] |
csl |
A normalized comma-separated string (whitespace around separators trimmed). | "" |
// listType: 'array', type: 'integer', value: '1, 2, 3' → [1, 2, 3]
// listType: 'csl', type: 'string', value: 'a , b ,c' → 'a,b,c'An admin UI often needs to constrain a field to a set of allowed values (dropdowns, multi-selects). Configuard models this with option lists:
- A row whose
keystarts with@is an option list definition, not a config value. Only itsvaluematters, and it is always treated as a comma-separated list (regardless oftype/listType). - Other rows reference an option list from their
optionsfield using a template:"${@UIColors}".
Option-list rows are excluded from build() output (they aren't real
config). To resolve them, use parseFlat().
How a field's value relates to its option list:
listType: none→ the value must be one member of the option list.listType: csl/array→ the value may contain several members.- A value outside the option list throws during
parseFlat().
Configuard.parseFlat(configList) returns the same flat list (not a nested
object) with:
- every
${...}placeholder invalueresolved — the value stays a string (it is not cast to itstype); - every
@-key option list extracted into a separate@object, each as a trimmed, uncast string array (ready to populate a UI control); and - every
optionsreference expanded into that string array.
import { Configuard } from 'configuard';
const { '@': optionLists, configList } = Configuard.parseFlat([
{ accessor: 'system', key: '@UIColors', type: 'string', listType: 'csl', value: 'Blue,Red,Green', /* … */ },
{ accessor: 'system', key: 'device.ui.colors', type: 'string', listType: 'csl', value: 'Blue,Red', options: '${@UIColors}', /* … */ },
{ accessor: 'system', key: 'port', type: 'integer', listType: 'none', value: '8081', /* … */ },
{ accessor: 'system', key: 'environment.internalPort', type: 'integer', listType: 'none', value: '${port}', /* … */ }
]);
optionLists;
// { UIColors: ['Blue', 'Red', 'Green'] }
configList;
// [
// { key: '@UIColors', value: 'Blue,Red,Green', options: null, listType: 'csl', … },
// { key: 'device.ui.colors', value: 'Blue,Red', options: ['Blue','Red','Green'], listType: 'csl', … },
// { key: 'port', value: '8081', options: null, listType: 'none', … },
// { key: 'environment.internalPort', value: '8081', options: null, listType: 'none', … } // ${port} resolved
// ]build() vs parseFlat():
build() (constructor) |
parseFlat() |
|
|---|---|---|
| Output shape | Nested object | Flat list (+ @ option lists) |
value |
Parsed/cast to type |
Resolved template, kept as string |
@-keys |
Excluded | Kept (in list and @) |
options |
Ignored | Expanded to string arrays |
| ABAC filtering | Yes | No |
Configuard.serializeFlat(configList, edits, options?) is the inverse of
parseFlat(): it turns the admin's edits back into DB-ready rows. For each edit
it enforces editable, validates the value against its type and options,
serializes it to the storage string, optionally re-encrypts encrypt: true
values, and returns the diff of changed rows.
editsmaps a configkeyto the changed fields (Partial<IConfigItem>— avalueand/or metadata such aseditable). Only keys you pass are processed; every other row is left untouched (so${...}templates are preserved).- Returns
{ updates, requiresReboot }—updatesis the changed rows ({ key, id, value, requiresReboot }),requiresRebootis the aggregate. Pass{ diffOnly: false }to also get the full mergedrows.
const { updates, requiresReboot } = Configuard.serializeFlat(rows, {
'ui.theme': { value: 'dark' },
port: { value: '9090' },
'db.password': { value: 'newSecret' } // encrypt:true row → re-encrypted below
}, {
encrypt: (value, item) => myEncrypt(value) // for encrypt:true items
});
// updates: [{ key:'ui.theme', id, value:'dark', requiresReboot:false }, …]
// requiresReboot: true (if any changed row requires it)Serialization is validate-then-store: numbers/booleans are canonicalized,
lists are comma-joined, and other types (hex/date/time/regexp/json/…)
are stored as the validated string. Invalid values, option-list violations, a
non-editable value change, or an encrypt hook error throw a ConfiguardError.
rawConfigList: IConfigItem[]— the flat config rows.accessorInfo?: { accessor?: AccessorType; appAccess?: number }— defaults toapplication. Throws ifaccessorisall, orapplicationwithout anappAccesslevel.options?: { debugLogs?: boolean; lock?: boolean; decrypt? }—debugLogsenables verboseconsolelogging;lock(defaulttrue) deep-freezes the built object (setfalsefor a mutable result);decryptdecryptsencrypt: trueitems (see Encryption).- Throws on a corrupt config (see Validation).
.data— the built, nested configuration object (frozen unlesslock: false)..get<T>(path, defaultValue?)— typed read of a property by notation..has(path)— whether a property exists at the given notation..accessor— the resolved accessor for this instance..isLocked— whether the built object is locked (deep-frozen)..getMeta(key)— a read-only view of a visible item's metadata (type,editable,requiresReboot,encrypt,options,id, …), orundefined..isEncrypted(key)— whether the item is flagged to be encrypted at rest..requiresReboot(key)— whether changing the item requires a reboot.
Note
The metadata accessors are ABAC-consistent: they only answer for keys
visible to this instance's accessor (mirroring .has()), returning
undefined/false otherwise.
Configuard.parseFlat(configList)— resolve templates + option lists into a flat structure (see above). Returns{ '@': Record<string, string[]>, configList: IFlatConfigItem[] }.Configuard.serializeFlat(configList, edits, options?)— validate + serialize admin edits into a diff of DB-ready rows (the inverse ofparseFlat; see Saving edits). Returns{ updates: IConfigUpdate[], requiresReboot: boolean, rows? }.Configuard.isConfigItem(o)—IConfigItemtype guard.
Each row implements IConfigItem, mirroring the reference config table (see
docs/config.sql):
| Field | Type | Meaning |
|---|---|---|
accessor |
AccessorType |
system / application / all. |
appAccess |
number | null |
Bitwise flags for application clients (ABAC). |
key |
string |
Dot/bracket notation; leading @ marks an option list. |
type |
ValueType |
How value is parsed (see Value types). |
listType |
ListType |
none / array / csl (see List types). |
value |
string | null |
The raw value to parse; may contain ${...} templates. |
options |
string | null |
Allowed values, or a "${@name}" option-list reference. |
defaultValue |
string | null |
Factory value for reverting. |
requiresReboot |
boolean |
Whether changing it requires a reboot. |
editable |
boolean |
Whether the accessor may edit it. |
encrypt |
boolean |
Whether the value should be encrypted when fetched. |
description |
string | null |
Human-readable description. |
See docs/config.sql for the reference config table.
Read the full documentation — guides, concepts and the API reference.
- 100% test coverage (statements, branches, functions, lines) — enforced in
CI via Vitest thresholds. Run
npm run cover. - ~90% mutation score via StrykerJS. Run
npm run mutation.
Mutation testing goes a step beyond
coverage: it makes hundreds of tiny edits ("mutants") to the source — flipping
> to >=, && to ||, returning undefined, etc. — and checks that a test
fails for each. It catches tests that execute code without actually
asserting its behavior (the trap where function getPositive(x){ return x }
hits 100% coverage but never verifies anything). The remaining surviving mutants
here are equivalent mutants
(redundant fast-path guards that produce identical results). The general-purpose
date utilities (parseDate/createUTCDate) are fully unit-tested but excluded
from the mutation scope, as their date-format regexes are dominated by
equivalent mutants.
- nestjs-configuard — The NestJS integration for this package: DB-backed, typed, ABAC-filtered runtime config with live reload and TTL auto-refresh.
- accesscontrol — Role and attribute based access control (RBAC + ABAC) with conditions, enforced ownership, custom actions and mandatory gates.
- notation — Read, modify, and filter the contents of objects and arrays via dot/bracket notation strings or glob patterns.
© 2026, Onur Yıldırım. MIT License.