# How to Define and Register Plugins in Cordis: Complete Guide with Examples

> Learn how to define and register plugins in Cordis using ctx.plugin(). Explore supported plugin shapes and lifecycle management with examples.

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

---

**Cordis supports three plugin shapes—functions, objects with an `apply` method, and classes—and registers them via `ctx.plugin()`, which returns a promise-like `Fiber` that manages the plugin lifecycle and automatic disposal.**

Cordis is a lightweight, extensible plugin system that powers the Cordiverse ecosystem. To define and register plugins in Cordis, you interact with the `Context` API and the `RegistryService` to instantiate plugins as fibers with dependency injection and automatic cleanup. This guide covers the three valid plugin definitions, the registration flow, and the lifecycle management implemented in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts).

## Cordis Plugin Shapes and Metadata

Cordis recognizes three distinct plugin definitions, all processed by `RegistryService` in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts):

- **Function plugins**: `(ctx: Context, config: T) => any` – Invoked directly with the current context.
- **Object plugins**: `{ apply: (ctx, config) => any, … }` – The `apply` method is invoked upon registration.
- **Class plugins**: `class Foo { constructor(ctx, config) { … } }` – Instantiated with `new Foo(ctx, config)`.

All three forms share common **runtime metadata** properties:
- `name?: string` – Optional identifier for the plugin.
- `Config?: StandardSchemaV1` – Optional configuration schema for validation.
- `inject?: Inject` – Declares dependencies that Cordis auto-injects before execution.

## Defining Plugins in Cordis

### Functional Plugin Definition

The simplest approach defines a plugin as a function receiving `Context` and configuration:

```typescript
// my-plugin.ts
import { Context } from '@cordisjs/core';

export default function LoggerPlugin(ctx: Context, config: { level: string }) {
  ctx.logger.info(`Logger level set to ${config.level}`);
}

```

When registered, Cordis calls this function directly with the provided context and validated configuration.

### Object Plugin with Configuration Schema

For complex plugins requiring validation, export an object with `apply` and a `Config` schema:

```typescript
// timer-plugin.ts
import { Schema } from '@standard-schema/spec';
import { Context } from '@cordisjs/core';

export const TimerPlugin = {
  name: 'timer',
  Config: Schema.object({ 
    interval: Schema.number().default(1000) 
  }),
  apply(ctx: Context, config: { interval: number }) {
    const timer = setInterval(() => ctx.logger.info('tick'), config.interval);
    return () => clearInterval(timer); // cleanup function
  },
};

```

The `apply` method executes upon registration, and the returned function handles disposal.

### Class Plugin with Dependency Injection

Class plugins support decorators for automatic service injection:

```typescript
// service-plugin.ts
import { Context, Inject } from '@cordisjs/core';

export class ServicePlugin {
  static init = Symbol('init'); // service lifecycle marker

  constructor(public ctx: Context) {}

  @Inject('logger')
  init(logger) {
    logger.info('ServicePlugin started');
    return () => logger.info('ServicePlugin stopped');
  }
}

```

The `@Inject` decorator declares dependencies that Cordis resolves before calling the `init` method.

## Registering Plugins in Cordis

### The Registration API

The `Context` interface exposes two primary methods defined in [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts):

```typescript
declare module './context' {
  export interface Context {
    /** Register a plugin */
    plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
    /** Register a dependency‑only plugin */
    inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
  }
}

```

Calling `ctx.plugin()` performs three operations:
1. **Validation**: Throws if the argument is not a function or an object with `apply`.
2. **Runtime creation**: Instantiates the plugin once per `Context` via `RegistryService.plugin()`.
3. **Fiber spawning**: Returns a `Fiber` that resolves when the plugin’s setup phase completes.

### The RegistryService Resolution Flow

The core registration logic lives in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts):

1. **Resolution**: `RegistryService.resolve()` detects whether the plugin is a plain function or an object with an `apply` method.
2. **Registration**: `RegistryService.plugin()` creates a **runtime entry** (`Plugin.Runtime`) and a `Fiber` representing the plugin’s lifecycle.
3. **Injection**: The system resolves declared dependencies before invoking the plugin logic.

### Dependency-Only Registration

Use `ctx.inject()` to register logic that only consumes dependencies without defining a reusable plugin:

```typescript
await ctx.inject(['logger'], (ctx) => {
  ctx.logger.info('Injected logger ready');
});

```

This is a convenience wrapper that creates a lightweight fiber without the full plugin instantiation overhead.

## Managing Plugin Lifecycles with Fibers

Every registered plugin creates a `Fiber` instance defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts). Fibers implement `PromiseLike<Fiber>`, allowing you to `await ctx.plugin(...)` to ensure initialization completes before proceeding:

```typescript
import LoggerPlugin from './my-plugin';

await ctx.plugin(LoggerPlugin, { level: 'debug' });
// Execution continues only after LoggerPlugin setup finishes

```

The `Fiber` class manages:
- **Configuration storage**: Holds the validated plugin configuration.
- **Disposable tracking**: Maintains a list of cleanup functions (event listeners, timers, services).
- **Automatic disposal**: Calling `dispose()` removes all resources and cascades cleanup to child plugins.

Nested plugins create hierarchical fiber trees; disposing a parent fiber automatically cleans up all children.

## Complete Cordis Plugin Examples

### Basic Registration

```typescript
import LoggerPlugin from './my-plugin';
await ctx.plugin(LoggerPlugin, { level: 'debug' });

```

### Object Plugin with Schema

```typescript
await ctx.plugin(TimerPlugin, { interval: 2000 });

```

### Class Plugin Registration

```typescript
await ctx.plugin(ServicePlugin);

```

### Nested Plugins

Plugins can register other plugins, each receiving its own `Fiber` with cascaded cleanup:

```typescript
await ctx.plugin(async (ctx) => {
  ctx.logger.info('parent plugin');
  await ctx.plugin((ctx) => ctx.logger.info('child plugin'));
});

```

## Summary

- Cordis accepts **three plugin shapes**: functions, objects with `apply`, and classes.
- **Registration** occurs via `ctx.plugin()`, which validates the plugin and returns a promise-like `Fiber`.
- `RegistryService` in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) handles resolution, instantiation, and dependency injection.
- **Fibers** manage the async lifecycle; awaiting `ctx.plugin()` ensures setup completes before execution continues.
- **Cleanup** is automatic through fiber disposal, which removes event listeners, services, and child plugins.

## Frequently Asked Questions

### What are the valid plugin shapes in Cordis?

Cordis supports function plugins `(ctx, config) => any`, object plugins with an `apply` method `{ apply: (ctx, config) => any }`, and class plugins with a constructor `constructor(ctx, config)`. All three are processed by `RegistryService.resolve()` in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts).

### How does `ctx.plugin()` handle asynchronous initialization?

`ctx.plugin()` returns a `Fiber` that implements `PromiseLike<Fiber>`, as defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts). Awaiting this fiber pauses execution until the plugin's setup phase completes, ensuring dependencies are ready before subsequent code runs.

### What happens when a plugin is disposed?

When a fiber's `dispose()` method is called, Cordis executes all registered cleanup functions, removes event listeners, and disposes of any services created by the plugin. Child plugins registered within the disposed plugin are automatically cleaned up in cascade fashion.

### Can I register a plugin without an `apply` method?

Yes. Use `ctx.inject(deps, callback)` to register dependency-only logic that executes after Cordis resolves the specified dependencies. This creates a lightweight fiber without requiring a full plugin definition with an `apply` function or configuration schema.