# Best Practices for Cordis Plugin Entry Points: Configuration, Lifecycle, and Grouping

> Master Cordis plugin entry points with best practices for configuration, lifecycle management (init update refresh), and grouping. Optimize your Cordis development.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: best-practices
- Published: 2026-08-25

---

**Cordis plugins are instantiated through lightweight **entries**—metadata wrappers defined in [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts) that manage configuration interpolation, hierarchical IDs, and lifecycle transitions via the `init`, `update`, and `refresh` methods.**

Cordis is a modular plugin framework where every plugin registers through the **entry system** located in the `@cordis/loader` package. Mastering best practices for Cordis plugin entry points ensures deterministic namespacing, clean configuration management, and predictable lifecycle behavior across your application.

## Understanding the Entry Class Architecture

The `Entry` class serves as the central abstraction for plugin registration. It handles module resolution, context isolation, and state management through a declarative options interface.

### EntryOptions Interface Structure

Every entry is defined by an `EntryOptions` object declared at lines 8-15 of [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts). This interface requires:

- **`id`** (required): Unique identifier within the plugin tree
- **`name`** (required): Module name to import (package or file path)
- **`config`** (optional): Configuration object supporting template interpolation
- **`group`** (optional): Boolean indicating whether the entry contains children
- **`disabled`** (optional): Boolean controlling runtime availability
- **`inject`** (optional): Dependency injection configuration

The `id` field acts as the canonical reference for lookups, while `name` specifies the actual module to load via the internal `tree.import(name, getOuterStack)` method.

### Hierarchical ID Namespacing

When an entry belongs to a group, Cordis automatically prefixes its ID with the parent entry's ID using the pattern `parent.id + '/' + id`. This deterministic namespacing is implemented in the `id` getter at lines 56-62 of [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts).

This hierarchy enables clean lookup chains and prevents naming collisions across large plugin trees. Child entries inherit their parent's namespace, simplifying configuration scoping and cross-plugin communication.

### The Disabled Flag Behavior

The `disabled` getter at lines 64-73 implements cascade logic: if any ancestor entry is disabled, all children are automatically disabled regardless of their individual flags. Groups themselves are always enabled; disabling a group entry effectively disables every descendant.

A disabled entry is never loaded, and Cordis automatically disposes the associated fibre when an entry transitions to disabled.

## Lifecycle Management Best Practices

Cordis manages plugin state through three distinct lifecycle methods: `init()`, `update()`, and `refresh()`. Understanding these transitions prevents memory leaks and ensures clean reconfiguration.

### The Init-Update-Refresh Flow

The async flow defined at lines 94-134 handles plugin instantiation and mutation:

- **`init()`**: Imports the module via `tree.import()`, creates the runtime fibre, registers the plugin with its interpolated configuration, and emits the `loader/entry-init` event
- **`update()`**: Mutates the entry's options, disposes the old fibre when necessary, emits `loader/partial-dispose`, and optionally re-initialises the plugin if configuration changes require a full reload
- **`refresh()`**: A convenience method that only re-initialises a non-loaded, enabled entry

Always use `entry.update(newOptions)` instead of mutating `entry.options` directly. The `update` method guarantees proper disposal sequencing and event emission.

### Event Hooks and Cleanup

The entry system emits two critical lifecycle events during construction and updates:

- **`loader/entry-init`**: Emitted at line 49 when the entry first initialises
- **`loader/partial-dispose`**: Emitted during `update()` when replacing an already-loaded entry

Plugins can listen to these events to perform custom cleanup or state migration during hot-reconfiguration scenarios.

## Configuration Patterns

Cordis supports declarative configuration through string interpolation and group-based scoping.

### Context-Aware Configuration Interpolation

When a plugin's default export is **not** an entry group, the entry's `config` object undergoes interpolation through the current context. This process, handled by `_resolveConfig` and the `interpolate` method at lines 79-82 of [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts), replaces patterns like `${env.VAR}` with actual environment values.

Store raw configuration templates in `options.config` and let Cordis resolve variables at load time. This keeps configuration portable across environments without hardcoding secrets.

### Plugin Groups and Deterministic Namespacing

Set `group: true` in a parent entry's options to create a namespace container. Child entries automatically receive hierarchical IDs (`parent.id/child.id`), simplifying tree navigation and configuration inheritance.

Groups provide logical organization for related plugins while maintaining strict isolation between different branches of the plugin tree.

## Runtime State Management

Effective entry point management requires atomic state changes and proper error handling.

### Atomic Updates via entry.update()

Never mutate entry properties directly. Calling `entry.update()` ensures:

1. Proper fibre disposal and cleanup
2. Emission of `loader/partial-dispose` events
3. Conditional re-initialisation based on changed properties
4. Thread-safe state transitions

### Feature Toggling with the Disabled Flag

Leverage the `disabled` boolean for runtime feature flags without removing entries from the tree. When you disable an entry, Cordis automatically disposes the fibre and skips loading. Re-enabling triggers a fresh initialisation without requiring bootstrap script changes.

This pattern is particularly effective for A/B testing and conditional feature deployment.

## Practical Implementation Examples

### Defining a Simple Plugin Entry

Create a plugin that exports a default function accepting a Cordis `Context`:

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

export default (ctx: Context) => {
  ctx.on('ready', () => console.log('my-plugin is ready!'))
}

```

Register the plugin through the loader API exposed in [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts):

```typescript
await ctx.loader.entry('my-plugin', {
  id: 'my-plugin',
  name: 'my-plugin',
  config: { greeting: '${env.GREETING}' },
  disabled: false,
})

```

The `name` field matches the package identifier, while `config` uses interpolation for environment-specific values.

### Creating a Plugin Group

Groups organize related plugins under a common namespace:

```typescript
// Parent group entry
await ctx.loader.entry('utils', {
  id: 'utils',
  name: 'utils',
  group: true,
})

// Child entry automatically receives ID 'utils/logger'
await ctx.loader.entry('utils/logger', {
  id: 'logger',
  name: '@my/logger',
  config: { level: 'debug' },
})

```

The child entry's effective ID becomes `utils/logger`, and its configuration is scoped under the same tree branch.

### Disabling a Plugin at Runtime

Toggle plugin availability without tree modification:

```typescript
const entry = await ctx.loader.entry('feature-x', { 
  id: 'feature-x', 
  name: 'feature-x' 
})

// Dispose fibre and stop plugin
await entry.update({ disabled: true })

// Re-enable later
await entry.update({ disabled: false })

```

### Updating Configuration Without Full Reload

Modify configuration while preserving runtime state when possible:

```typescript
await entry.update({
  config: { greeting: 'Hello, world!' }
})

```

Cordis calls `fiber.update` with the new configuration if the entry is already loaded, avoiding unnecessary disposal cycles.

## Summary

- **Always provide stable, unique IDs**: Use deterministic naming schemes and avoid changing IDs after creation to prevent lookup chain breaks.
- **Use groups for organization**: Set `group: true` to enable hierarchical namespacing and simplify configuration inheritance.
- **Leverage atomic updates**: Call `entry.update()` for all state changes to ensure proper lifecycle event emission and fibre management.
- **Implement feature toggles**: Use the `disabled` flag instead of removing entries to enable runtime plugin control.
- **Never import manually**: Rely on `tree.import()` through the entry system to ensure proper error handling and stack tracing via the `getOuterStack` parameter.
- **Interpolate configuration**: Store templates in `config` fields and let Cordis resolve environment variables through context-aware interpolation.

## Frequently Asked Questions

### What is the difference between an entry's `id` and `name` fields?

The `id` field serves as the unique identifier within the plugin tree and supports hierarchical namespacing when entries belong to groups. The `name` field specifies the module to import—either a package name resolvable by Node.js or a file path. While `id` must be unique within the tree, `name` can be shared across multiple entries loading the same module with different configurations.

### How does Cordis handle configuration environment variables?

Cordis interpolates configuration values through the entry's context when the plugin's default export is not an entry group. The `_resolveConfig` method processes templates like `${env.VARIABLE}` at load time, replacing them with actual environment values. This occurs at lines 79-82 of [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts), keeping secrets out of configuration files while maintaining portability.

### What happens when I disable a group entry?

When you set `disabled: true` on a group entry (or any of its ancestors), Cordis immediately disables every descendant entry regardless of their individual flags. The `disabled` getter at lines 64-73 implements this cascade logic. Disposing a group entry also disposes all child fibres, making this an effective nuclear option for shutting down feature sets.

### Can I update a plugin's configuration without restarting the entire application?

Yes. Use `entry.update({ config: newConfig })` to trigger a partial update. If the entry is already loaded, Cordis emits `loader/partial-dispose` and attempts to call `fiber.update` with the new configuration. This allows hot-reconfiguration for stateful plugins that support incremental updates without full re-initialisation.