Skip to content

ADR-0002: Introduce @speajus/diblob-config for Typed Configuration ​

Status ​

Proposed

Date ​

2025-11-24

Context / Problem ​

  • Configuration in the diblob ecosystem is currently handled ad hoc:
    • Direct process.env access in registration functions and examples.
    • Hand-written loadConfig helpers in individual apps.
    • "Config service" patterns with string-based keys.
  • There is no first-class, reusable configuration library that:
    • Provides strongly typed configuration objects for blobs.
    • Integrates environment variables and optional config files.
    • Performs runtime validation via a schema library.
    • Encodes per-environment behavior (dev, test, staging, prod).
    • Encourages safe defaults for tests and local development.
  • This makes it hard to answer "how do I get configuration into my blobs?" with a single, canonical pattern.

Decision ​

We will introduce a new package, @speajus/diblob-config, that provides:

  1. A generic, schema-driven configuration loader:

    • loadConfig<TConfig>(options): TConfig which:
      • Starts from defaults (optionally per-environment).
      • Merges values from an optional in-memory config object (typically parsed from JSON).
      • Merges values from environment-like key/value records (optionally prefixed).
      • Optionally merges values from CLI-style arguments.
      • Validates the merged result against a runtime schema (initially Zod).
      • Returns a strongly typed TConfig or throws on validation errors.
  2. Container integration helpers that follow existing diblob patterns:

    • registerConfigBlob(container, blob, options)
      • Registers a typed configuration blob as a Lifecycle.Singleton.
      • Uses loadConfig under the hood.
    • registerStaticConfigBlob(container, blob, config)
      • Registers a pre-constructed config object as a singleton, for tests and simple local setups.
  3. A recommended usage pattern where:

    • Applications define a TypeScript interface AppConfig.
    • They define a corresponding runtime schema.
    • They create a createBlob<AppConfig>() config blob.
    • They call registerConfigBlob (or registerStaticConfigBlob) during container setup.
    • Services depend on AppConfig directly, not on raw env or string-key config services.
    • They use Zod for schema definition, description and validation.
  4. A way to read configuration from environment variables and configuration files. So if a user configs APP_PORT=3000 and APP_HOST=127.0.0.1 then the configuration will be { port: 3000, host: '127.0.0.1' }. In addition they should be able to have an Env prefix like APP_ so that the user can do APP_PORT=3000 and APP_HOST=127.0.0.1 and the configuration will be { port: 3000, host: '127.0.0.1' }. The prefix is to avoid collisions with other environment variables. The prefix is configurable.

  5. It should be able to parse command line swithes with -- like --app-port=3000 and --app-host=127.0.0.1 and the configuration will be { port: 3000, host: '127.0.0.1' }. This is to allow for easy testing and debugging. The prefix is configurable.

Design Overview ​

Schema and environment abstraction ​

We introduce the following core types:

  • EnvironmentName – 'development' | 'test' | 'staging' | 'production' | string.
  • ConfigSchema<TConfig> – wrapper type around the chosen schema library.
  • LoadConfigOptions<TConfig> – describes how to load and validate config (schema, env, file, defaults, environment).

The initial implementation will use Zod under the hood, but the public surface will refer to ConfigSchema<TConfig> so we can later swap to Valibot or another runtime schema library if desired.

loadConfig ​

loadConfig<TConfig>(options: LoadConfigOptions<TConfig>): TConfig will:

  1. Determine environment from options.environment (defaulting to "development").
  2. Start from defaults (object or function of environment).
  3. Overlay values from an optional fileConfig object (often parsed from a JSON file on the server or provided by the host environment on the client).
  4. Overlay values derived from an env record (with optional prefix filtering and normalization from SOME_PREFIX_KEY to someKey).
  5. Optionally overlay values derived from CLI-style arguments when cliPrefix and cliArgs are provided.
  6. Pass the merged raw object to schema.parse.
  7. Return the validated TConfig or throw a descriptive validation error.

Precedence: defaults < file < env < command line switches (switches wins).

registerConfigBlob ​

registerConfigBlob wires the loader into a container as a singleton configuration blob:

ts
registerConfigBlob(container, appConfigBlob, {
  schema: AppConfigSchema,
  envPrefix: 'APP_',
  environment: process.env.NODE_ENV ?? 'development',
  defaults: (env) => env === 'test' ? testDefaults : prodDefaults,
});

Services then depend on the typed AppConfig interface via the blob, rather than directly on env or ad hoc helpers.

registerStaticConfigBlob ​

registerStaticConfigBlob supports tests and simple local setups by registering an already-constructed configuration object:

ts
registerStaticConfigBlob(container, appConfigBlob, {
  port: 0,
  host: '127.0.0.1',
});

This avoids coupling tests to process.env and makes it trivial to inject known-safe defaults.

Rationale ​

  • Unification: Today, each example and package rolls its own configuration story. A dedicated package gives a single answer: use @speajus/diblob-config.
  • Type safety: Injecting a typed TConfig object is safer and more maintainable than scattered process.env reads or string-key lookup services.
  • Validation: Runtime schemas (initially Zod) provide consistent error messages and default handling, and keep validation logic close to the shape definition.
  • Testability: Being able to inject a static config or a synthetic env object makes tests more reliable and less coupled to process-wide state.
  • Alignment with existing patterns: The design mirrors existing packages such as @speajus/diblob-logger and @speajus/diblob-telemetry, which expose "register*Blobs" helpers.

Consequences ​

Positive ​

  • Clear, documented configuration story for all diblob-based applications.
  • Stronger typing of configuration throughout services.
  • Centralized, reusable implementation of env/file/defaults merging.
  • Easier testing by avoiding direct process.env dependence in services.

Negative / tradeoffs ​

  • New dependency on a schema library (Zod initially), with some startup cost for parsing and validation.
  • Users must learn the schema DSL to define their configuration.
  • Misconfigured environments will now fail fast at startup rather than being silently tolerated (intended but behavior-changing).

Implementation Plan ​

  1. Create packages/diblob-config with the same build/test conventions as other packages.
  2. Introduce ConfigSchema, EnvironmentName, LoadConfigOptions, and loadConfig in src/.
  3. Implement registerConfigBlob and registerStaticConfigBlob following the patterns used in diblob-logger and diblob-telemetry.
  4. Add node:test-based tests covering:
    • Env-only configuration.
    • Env + defaults + environment-specific behavior.
    • File + env precedence.
    • Validation failures.
  5. Update public environment-configuration docs to reference @speajus/diblob-config as the recommended approach.
  6. Update examples to use @speajus/diblob-config for configuration.
  • Update example-grpc-server
  • Update example-web-svelte
  • Update diblob-visualizer if needed adding registerConfigBlob
  • Update diblob-telemetry if needed
  • Update diblob-logger if needed
  • Update diblob-connect if needed
  • Update diblob-svelte if needed
  • Update diblob-mcp if needed
  • Update diblob-visualizer if needed

Released under the MIT License.