# How Cordis Handles Configuration Merging Across Plugins

> Cordis merges plugin configurations using a custom merge method or Object.assign, combining intercept chain, base config, and head config for robust settings management.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-09-12

---

**Cordis merges configuration across plugins by treating each plugin’s configuration as a service-level object that combines three distinct sources—intercept chain, base config, and head config—using a custom static `Config.merge` method when available, or falling back to shallow `Object.assign` merging.**

Cordis treats plugin configuration as a first-class service primitive within the cordiverse/cordis ecosystem. When multiple plugins contribute configuration fragments, the framework resolves these into a single coherent configuration object through a deterministic merging pipeline implemented in the core service layer.

## The Three Sources of Configuration

Cordis collects configuration from three distinct sources during the resolution process:

- **Intercept chain** – Intermediate configuration objects collected while traversing the `Context.intercept` prototype chain that other plugins may augment
- **Base config** – The default configuration passed when the service is constructed, serving as the starting point for the merge
- **Head config** – The configuration object supplied by the caller of `ctx.plugin` (or similar), providing final overrides

These three layers ensure that plugins can both provide defaults and allow consumers to override them while respecting the interceptor pattern.

## The resolveConfig Implementation

In [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts), the `Service.resolveConfig` method implements the core merging logic:

```typescript
// packages/core/src/service.ts#L51-L66
if (this['Config']?.merge) {
  return this['Config'].merge(...configs)   // custom merge logic
} else {
  return Object.assign({}, ...configs)     // shallow merge fallback
}

```

This implementation checks for a static `merge` method on the service’s `Config` class. If present, Cordis delegates merging to the plugin’s custom implementation. Otherwise, it performs a shallow merge using `Object.assign`, spreading all collected configurations into a new object.

## Custom Merge Strategies

Plugins requiring sophisticated merging semantics—such as deep merging, array deduplication, or validation—can implement a static `Config.merge` method. This method receives all configuration fragments as arguments and returns the final merged object.

```typescript
import { Service } from 'cordis'

export class MyPlugin extends Service {
  static Config = class {
    static merge(...configs: any[]) {
      const result = {}
      for (const cfg of configs) {
        for (const [k, v] of Object.entries(cfg)) {
          if (Array.isArray(v)) result[k] = [...(result[k] ?? []), ...v]
          else if (typeof v === 'object' && v !== null) result[k] = { ...(result[k] ?? {}), ...v }
          else result[k] = v
        }
      }
      return result
    }
  }
}

```

The custom merge strategy above handles array concatenation and deep object merging, contrasting with the default shallow behavior.

## Persistent Configuration with the Include Plugin

The **Include** plugin extends Cordis configuration merging to persistent storage. It maintains a **journal** to record tree-side changes and reconciles these with external file modifications.

In [`packages/include/src/journal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/include/src/journal.ts), the `merge` function reconciles journal entries:

```typescript
// packages/include/src/journal.ts#L45-L66
export function merge(journal: Journal, id: string, record: JournalRecord) {
  const merged = mergeRecords(journal.get(id), record)
  if (merged) {
    journal.set(id, merged)
    return
  }
  if (record.kind === 'remove') journal.delete(id)
}

```

The `mergeRecords` function combines older records with newer ones while preserving creation flags and handling upserts versus removals. After reconciliation, the Include service writes the merged configuration back to disk atomically, ensuring consistency between runtime edits and external file changes.

## Configuration Resolution Flow

In practice, Cordis configuration merging follows this sequence:

1. **Plugin registration** – Plugins expose a `Config` class with an optional static `merge` method
2. **Config resolution** – `Service.resolveConfig` collects all fragments and invokes custom merge or defaults
3. **Persistence** – The Include plugin records changes in the journal, reconciles with file contents, and writes merged state back to disk

This pipeline guarantees deterministic configuration composition across plugin boundaries while supporting extensible merging semantics.

## Summary

- Cordis configuration merging operates on three layers: intercept chain, base config, and head config
- `Service.resolveConfig` in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) orchestrates the merge, preferring custom `Config.merge` implementations over shallow `Object.assign`
- Plugins can implement sophisticated merging logic—including deep merges and array handling—via static `Config.merge` methods
- The Include plugin persists configurations using a journaling system in [`packages/include/src/journal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/include/src/journal.ts) that reconciles runtime changes with file edits
- All merges are deterministic and respect the prototype chain order of the Cordis context

## Frequently Asked Questions

### How does Cordis choose between custom merge and default merge?

Cordis checks for the presence of a static `merge` method on the plugin's `Config` class via `this['Config']?.merge`. If the method exists, Cordis passes all configuration objects to it; otherwise, it falls back to `Object.assign({}, ...configs)` for a shallow merge.

### Can multiple plugins modify the same configuration keys?

Yes. Cordis accumulates configuration contributions from the intercept chain, allowing multiple plugins to provide values for the same keys. The final head config supplied to `ctx.plugin` takes precedence in the merge order unless a custom `merge` method implements different conflict resolution logic.

### What happens when configuration changes at runtime?

The Include plugin captures runtime changes through its journaling mechanism. When `ctx.include('config.yml').commit()` is called, it creates a journal entry. The `merge` function in [`journal.ts`](https://github.com/cordiverse/cordis/blob/main/journal.ts) reconciles these entries with the current file state before writing the merged result back to disk.

### Is deep merging supported by default?

No. The default behavior in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) uses `Object.assign`, which performs shallow merging. Plugins requiring deep merging must implement a custom static `Config.merge` method that handles nested object recursion and array concatenation according to their specific domain requirements.