# @memberjunction/config
Central configuration loading and merging utilities for the MemberJunction framework. This package provides a standardized way for MJ packages to define default configurations, discover user override files, and merge them together with a deterministic precedence order.
## Overview
MemberJunction applications are configured through a layered system where each package defines its own defaults and users supply overrides through a shared `mj.config.cjs` file. This package provides the infrastructure that makes that layering work: config file discovery (via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig)), deep merge with customizable strategies, structure validation, and environment-variable parsing helpers.
```mermaid
flowchart TD
A["Package Defaults
(hardcoded in each package)"] --> D["mergeConfigs()"]
B["mj.config.cjs
(user overrides)"] --> D
C["Environment Variables"] --> E["parseBooleanEnv()"]
E --> F["Final Runtime Config"]
D --> F
style A fill:#2d6a9f,stroke:#1a4971,color:#fff
style B fill:#2d8659,stroke:#1a5c3a,color:#fff
style C fill:#b8762f,stroke:#8a5722,color:#fff
style D fill:#7c5295,stroke:#563a6b,color:#fff
style E fill:#7c5295,stroke:#563a6b,color:#fff
style F fill:#2d6a9f,stroke:#1a4971,color:#fff
```
## Installation
```bash
npm install @memberjunction/config
```
If you are working inside the MemberJunction monorepo, add the dependency to the consuming package's `package.json` and run `npm install` at the repository root.
## Configuration File Discovery
When `loadMJConfig()` is called it uses cosmiconfig to walk up the directory tree looking for the first matching file in the following order:
| Priority | File / Location | Format |
|----------|----------------|--------|
| 1 | `mj.config.cjs` | CommonJS |
| 2 | `mj.config.js` | ESM / CommonJS |
| 3 | `.mjrc` | JSON / YAML |
| 4 | `.mjrc.js` | ESM / CommonJS |
| 5 | `.mjrc.cjs` | CommonJS |
| 6 | `"mj"` key in `package.json` | JSON |
The recommended convention across MemberJunction is `mj.config.cjs` placed at the repository root.
## Usage
### Loading Configuration (Async)
The primary entry point. It discovers the user's config file, merges it with the package defaults, and returns the result along with metadata about what was loaded.
```typescript
import { loadMJConfig } from '@memberjunction/config';
interface MyPackageConfig {
port: number;
debug: boolean;
database: {
host: string;
pool: { max: number; min: number };
};
}
const MY_DEFAULTS: MyPackageConfig = {
port: 4000,
debug: false,
database: {
host: 'localhost',
pool: { max: 50, min: 5 }
}
};
const result = await loadMJConfig({
defaultConfig: MY_DEFAULTS,
verbose: true // logs discovery / merge details
});
console.log(result.config); // merged configuration object
console.log(result.hasUserConfig); // true if mj.config.cjs was found
console.log(result.configFilePath); // path to the discovered file
console.log(result.overriddenKeys); // top-level keys the user changed
```
### Loading Configuration (Sync)
For cases where an async call is not possible (CommonJS bootstrap code, for example), `loadMJConfigSync` accepts an explicit file path instead of searching the directory tree.
```typescript
import { loadMJConfigSync } from '@memberjunction/config';
const config = loadMJConfigSync(
'/absolute/path/to/mj.config.cjs',
{ defaultConfig: MY_DEFAULTS }
);
```
### Building a Multi-Package Configuration
`buildMJConfig` composes defaults from several MJ packages into a single configuration object before applying user overrides. This is typically called at application startup.
```typescript
import { buildMJConfig } from '@memberjunction/config';
const config = buildMJConfig(
{
server: serverDefaults,
codegen: codegenDefaults,
mcpServer: mcpDefaults,
a2aServer: a2aDefaults,
queryGen: queryGenDefaults
},
userOverrides // optional -- from mj.config.cjs
);
```
```mermaid
flowchart LR
S["Server
Defaults"] --> B["buildMJConfig()"]
C["CodeGen
Defaults"] --> B
M["MCP Server
Defaults"] --> B
A["A2A Server
Defaults"] --> B
Q["QueryGen
Defaults"] --> B
U["User
Overrides"] --> B
B --> R["Unified Config"]
style S fill:#2d6a9f,stroke:#1a4971,color:#fff
style C fill:#2d6a9f,stroke:#1a4971,color:#fff
style M fill:#2d6a9f,stroke:#1a4971,color:#fff
style A fill:#2d6a9f,stroke:#1a4971,color:#fff
style Q fill:#2d6a9f,stroke:#1a4971,color:#fff
style U fill:#2d8659,stroke:#1a5c3a,color:#fff
style B fill:#7c5295,stroke:#563a6b,color:#fff
style R fill:#b8762f,stroke:#8a5722,color:#fff
```
### Merging Configurations Directly
`mergeConfigs` performs a deep merge of two plain objects. It is used internally by the loader functions and is also exported for packages that need to merge configuration fragments on their own.
```typescript
import { mergeConfigs } from '@memberjunction/config';
const merged = mergeConfigs(defaults, overrides, {
concatenateArrays: false, // true to append arrays instead of replacing
allowNullOverrides: false // true to let null values clear defaults
});
```
#### Merge Rules
| Source Type | Behavior |
|-------------|----------|
| Primitive (string, number, boolean) | Override replaces default |
| Object | Deep recursive merge |
| Array | Override replaces default (or concatenates if `concatenateArrays: true`) |
| `null` / `undefined` in override | Ignored by default; replaces if `allowNullOverrides: true` |
| Key with `_append` suffix | Concatenates the array onto the matching base key |
The `_append` suffix is especially useful when a user wants to add items to a default array without wiping it out:
```javascript
// mj.config.cjs
module.exports = {
// Instead of replacing excludeSchemas entirely, append to the defaults
excludeSchemas_append: ['staging', 'archive']
};
```
### Validating Configuration Structure
`validateConfigStructure` checks a merged configuration object against a set of expected top-level keys and logs warnings for any unexpected entries. This helps catch typos and deprecated settings early.
```typescript
import { validateConfigStructure } from '@memberjunction/config';
const allowedKeys = new Set(['port', 'debug', 'database', 'logging']);
validateConfigStructure(config, allowedKeys);
// Warns: "Unexpected configuration keys found: databse"
```
### Parsing Boolean Environment Variables
`parseBooleanEnv` normalizes the many string representations of boolean values commonly found in environment variables into a strict `true` / `false`.
```typescript
import { parseBooleanEnv } from '@memberjunction/config';
const debugMode = parseBooleanEnv(process.env.MJ_DEBUG);
```
Truthy values (case-insensitive): `true`, `1`, `yes`, `y`, `on`, `t`.
Everything else -- including `undefined`, empty string, and `null` -- returns `false`.
## API Reference
### Functions
#### `loadMJConfig(options?): Promise>`
Asynchronously discovers and loads an MJ configuration file, merges it with the provided defaults, and returns the result.
#### `loadMJConfigSync(configPath, options?): T`
Synchronously loads a configuration file from an explicit path and merges it with defaults. Does not search the directory tree.
#### `buildMJConfig(packageDefaults, userConfigOverrides?): Record`
Merges default configurations from multiple MJ packages into a single object, then applies optional user overrides.
#### `mergeConfigs(defaults, overrides, options?): T`
Deep-merges two plain objects using customizable strategies for arrays, nulls, and the `_append` suffix convention.
#### `validateConfigStructure(config, allowedKeys): void`
Logs warnings for any top-level keys in `config` that are not present in `allowedKeys`.
#### `parseBooleanEnv(value): boolean`
Parses a string (typically from `process.env`) into a boolean using common truthy conventions.
#### `isValidConfig(value): value is MJConfig`
Type guard that checks whether a value is a non-null object suitable for use as a configuration.
### Interfaces
#### `LoadConfigOptions`
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `searchFrom` | `string` | `process.cwd()` | Directory to start searching for the config file |
| `requireConfigFile` | `boolean` | `false` | Throw if no config file is found |
| `mergeOptions` | `MergeOptions` | `{}` | Controls array and null merge behavior |
| `verbose` | `boolean` | `false` | Log discovery and merge details to console |
| `defaultConfig` | `Record` | `{}` | Base configuration provided by the calling package |
#### `LoadConfigResult`
| Property | Type | Description |
|----------|------|-------------|
| `config` | `T` | The final merged configuration |
| `configFilePath` | `string \| undefined` | Path to the discovered user config file |
| `hasUserConfig` | `boolean` | Whether a user config file was found |
| `overriddenKeys` | `string[]` | Top-level keys that differ from defaults |
#### `MergeOptions`
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `concatenateArrays` | `boolean` | `false` | Append override arrays to defaults instead of replacing |
| `allowNullOverrides` | `boolean` | `false` | Allow `null` in overrides to clear default values |
### Types
#### `MJConfig`
```typescript
type MJConfig = Record;
```
Generic configuration type. Each consuming package defines its own specific configuration interface.
## Architecture
```mermaid
flowchart TB
subgraph ConfigPackage ["@memberjunction/config"]
direction TB
CL["config-loader.ts
loadMJConfig / loadMJConfigSync / buildMJConfig"]
CM["config-merger.ts
mergeConfigs / validateConfigStructure"]
CT["config-types.ts
MJConfig / isValidConfig"]
EU["env-utils.ts
parseBooleanEnv"]
CL --> CM
end
subgraph Consumers ["Consuming Packages"]
direction TB
SRV["@memberjunction/server"]
CG["@memberjunction/codegen-lib"]
MCP["@memberjunction/mcp-server"]
A2A["@memberjunction/a2a-server"]
CLI["@memberjunction/cli"]
MS["@memberjunction/metadata-sync"]
end
Consumers --> ConfigPackage
style ConfigPackage fill:#2d6a9f,stroke:#1a4971,color:#fff
style CL fill:#7c5295,stroke:#563a6b,color:#fff
style CM fill:#7c5295,stroke:#563a6b,color:#fff
style CT fill:#7c5295,stroke:#563a6b,color:#fff
style EU fill:#7c5295,stroke:#563a6b,color:#fff
style Consumers fill:#2d8659,stroke:#1a5c3a,color:#fff
style SRV fill:#2d8659,stroke:#1a5c3a,color:#fff
style CG fill:#2d8659,stroke:#1a5c3a,color:#fff
style MCP fill:#2d8659,stroke:#1a5c3a,color:#fff
style A2A fill:#2d8659,stroke:#1a5c3a,color:#fff
style CLI fill:#2d8659,stroke:#1a5c3a,color:#fff
style MS fill:#2d8659,stroke:#1a5c3a,color:#fff
```
## Dependencies
| Package | Purpose |
|---------|---------|
| [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) | Configuration file discovery and loading |
| [lodash.mergewith](https://lodash.com/docs/#mergeWith) | Deep merge with custom merge strategy |
| [zod](https://zod.dev/) | Schema validation (available for consumers) |
## Related Packages
| Package | Relationship |
|---------|-------------|
| `@memberjunction/server` | Defines `MJServerConfig` defaults; uses `mergeConfigs` and `parseBooleanEnv` |
| `@memberjunction/codegen-lib` | Defines `CodeGenConfig` defaults; uses `mergeConfigs` and `parseBooleanEnv` |
| `@memberjunction/mcp-server` | Defines `MCPServerConfig` defaults; uses `mergeConfigs` |
| `@memberjunction/a2a-server` | Defines `A2AServerConfig` defaults; uses `mergeConfigs` |
| `@memberjunction/cli` | CLI tool config loading; uses `mergeConfigs` and `parseBooleanEnv` |
| `@memberjunction/metadata-sync` | Metadata sync config; uses `mergeConfigs` and `parseBooleanEnv` |