Docket Docs
Developer GuideArchitecture

Plugin Architecture

How adapters are discovered, validated, loaded, and used.

Plugin Architecture

Plugins in Docket are npm packages that implement a frozen interface. There is no special plugin SDK, no proprietary registry, and no build step required. If you can write a JavaScript class, you can write a Docket plugin.

The plugin contract

Every plugin is a single JavaScript module that exports one class:

┌─────────────────────────────────────────────────────────────────┐
│                        Plugin Package                           │
│                     (npm module on disk)                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   index.js                                                      │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │  class MyAdapter extends FrozenInterface {              │   │
│   │                                                         │   │
│   │    constructor(config) { ... }                          │   │
│   │    async initialize() { ... }                           │   │
│   │    async primaryMethod(...) { ... }                     │   │
│   │    async health() { ... }                               │   │
│   │                                                         │   │
│   │    static get metadata() {                              │   │
│   │      return {                                            │   │
│   │        name: 'my-adapter',                               │   │
│   │        version: '0.1.0',                                 │   │
│   │        category: 'llm',                                  │   │
│   │        capabilities: ['chat'],                           │   │
│   │        docketCompatibility: '>=0.1.0 <0.3.0'             │   │
│   │      };                                                  │   │
│   │    }                                                     │   │
│   │  }                                                      │   │
│   │                                                         │   │
│   │  module.exports = { MyAdapter };                        │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   package.json                                                  │
│   { "name": "docket-llm-myprovider", ... }                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Adapter registry resolution

When Docket needs an adapter, the registry resolves the package name to a file path:

  Config says:
  adapter: "@docket/llm-ollama"


┌──────────────────────┐
│  AdapterRegistry     │
│  .resolveAdapterPath │
└──────┬───────────────┘

       ├─ starts with "@docket/" ?
       │      └─▶ resolve to ./src/adapters/{category}/{name}/index

       ├─ starts with "." ?
       │      └─▶ resolve relative to process.cwd()

       └─ otherwise
              └─▶ require.resolve(packageName)  ← npm package


  require(resolvedPath)


  Find first class-like export


  new AdapterClass(config)


  instance.initialize()


  Verify metadata exists


  Return initialized adapter

Plugin lifecycle

   Developer                     npm registry                    User system
      │                              │                              │
      │  npm publish                 │                              │
      │─────────────────────────────▶│                              │
      │                              │                              │
      │                              │◄──── npm install plugin      │
      │                              │      (user runs this)        │
      │                              │                              │
      │                              │                              │
                                     ▼                              ▼
      │                              │                    ┌─────────────────┐
      │                              │                    │  PluginService  │
      │                              │                    │  .validate()    │
      │                              │                    └───────┬─────────┘
      │                              │                            │
      │                              │                    check metadata schema
      │                              │                    check category match
      │                              │                    check class exports
      │                              │                            │
      │                              │                            ▼
      │                              │                    ┌─────────────────┐
      │                              │                    │  valid ?        │
      │                              │                    └───────┬─────────┘
      │                              │                         yes │ no
      │                              │                            ▼
      │                              │                    ┌─────────────────┐
      │                              │                    │  reject with    │
      │                              │                    │  errors[]       │
      │                              │                    └─────────────────┘
      │                              │                            │
      │                              │                            ▼
      │                              │                    ┌─────────────────┐
      │                              │                    │  PluginService  │
      │                              │                    │  .register()    │
      │                              │                    └───────┬─────────┘
      │                              │                            │
      │                              │                    AdapterRegistry
      │                              │                    .loadAdapter()
      │                              │                            │
      │                              │                            ▼
      │                              │                    ┌─────────────────┐
      │                              │                    │  instance       │
      │                              │                    │  initialized    │
      │                              │                    └───────┬─────────┘
      │                              │                            │
      │                              │                            ▼
      │                              │                    ┌─────────────────┐
      │                              │                    │  stored in      │
      │                              │                    │  PluginService  │
      │                              │                    │  .registered    │
      │                              │                    └─────────────────┘
      │                              │                            │
      │                              │                            ▼
      │                              │                    Data Plane can now
      │                              │                    use the adapter

Runtime onboarding flow

You do not need to restart Docket to try a plugin. The control plane exposes validation and registration endpoints:

┌─────────────┐
│   Client    │
│  (admin UI  │
│   or CLI)   │
└──────┬──────┘

       │ POST /admin/plugins/validate
       │ { "packageName": "docket-llm-groq" }

┌─────────────────────────┐
│  Control Plane          │
│  PluginService.validate │
│  · require() package    │
│  · check metadata       │
│  · infer category       │
└───────┬─────────────────┘
        │ { valid: true, manifest, category }

        │ POST /admin/plugins
        │ { "packageName": "docket-llm-groq",
        │   "config": { "apiKey": "..." } }

┌─────────────────────────┐
│  Control Plane          │
│  PluginService.register │
│  · instantiate class    │
│  · call initialize()    │
│  · store in registry    │
└───────┬─────────────────┘
        │ { status: "registered", plugin }

        │ (adapter is now live)

┌─────────────────────────┐
│  Data Plane             │
│  reads registry snapshot │
│  and uses new adapter   │
└─────────────────────────┘

Adapter class anatomy

Here is the complete anatomy of a well-behaved adapter:

┌────────────────────────────────────────────────────────────┐
│                    Adapter Class                            │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  CONSTRUCTOR                                               │
│  ┌────────────────────────────────────────────────────┐    │
│  │  constructor(config) {                             │    │
│  │    super(config);   ← validateConfig() called here │    │
│  │    this.apiKey = config.apiKey;                    │    │
│  │    this.model = config.model || 'default';         │    │
│  │  }                                                 │    │
│  └────────────────────────────────────────────────────┘    │
│                                                            │
│  INITIALIZATION                                            │
│  ┌────────────────────────────────────────────────────┐    │
│  │  async initialize() {                              │    │
│  │    const health = await this.health();             │    │
│  │    if (!health.ok) throw new Error(...);           │    │
│  │  }                                                 │    │
│  └────────────────────────────────────────────────────┘    │
│                                                            │
│  PRIMARY METHODS (vary by interface)                       │
│  ┌────────────────────────────────────────────────────┐    │
│  │  LlmAdapter:     async chat(messages, options)     │    │
│  │  EmbedderAdapter: async embed(text)                │    │
│  │  StoreAdapter:   async createMemory(memory)        │    │
│  │  BlobAdapter:    async put(key, data, metadata)    │    │
│  │  QueueAdapter:   async enqueue(type, payload)      │    │
│  └────────────────────────────────────────────────────┘    │
│                                                            │
│  HEALTH CHECK                                              │
│  ┌────────────────────────────────────────────────────┐    │
│  │  async health() {                                  │    │
│  │    return { ok: true, latency: 42 };               │    │
│  │  }                                                 │    │
│  └────────────────────────────────────────────────────┘    │
│                                                            │
│  METADATA (static — inspected before instantiation)        │
│  ┌────────────────────────────────────────────────────┐    │
│  │  static get metadata() {                           │    │
│  │    return {                                         │    │
│  │      name: 'my-adapter',                            │    │
│  │      version: '0.1.0',                              │    │
│  │      category: 'llm',                               │    │
│  │      capabilities: ['chat', 'stream'],              │    │
│  │      docketCompatibility: '>=0.1.0 <0.3.0',         │    │
│  │      description: '...',                            │    │
│  │      author: '...',                                 │    │
│  │      repository: '...',                             │    │
│  │      license: 'MIT'                                 │    │
│  │    };                                               │    │
│  │  }                                                  │    │
│  └────────────────────────────────────────────────────┘    │
│                                                            │
└────────────────────────────────────────────────────────────┘

Security boundary

The control plane owns the adapter registry for a reason. If the data plane could load arbitrary code at runtime, a compromised query endpoint could inject malicious adapters.

┌─────────────────────────────────────────────────────────────┐
│                     SECURITY MODEL                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Control Plane          Data Plane         External User   │
│   ┌──────────┐          ┌──────────┐        ┌──────────┐   │
│   │ Registry │◄─────────│ Snapshot │◄───────│  Client  │   │
│   │ (RW)     │  push    │ (RO)     │  HTTP  │          │   │
│   └──────────┘          └──────────┘        └──────────┘   │
│        │                                                    │
│        │ can load new adapters                              │
│        ▼                                                    │
│   ┌─────────────────┐                                       │
│   │  npm packages   │                                       │
│   │  (any code)     │                                       │
│   └─────────────────┘                                       │
│                                                             │
│   Data plane NEVER calls require() on user-provided         │
│   package names. It only uses pre-registered instances.     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Dependency rules

The codebase enforces a strict dependency direction via ESLint:

  Adapters ──may import──▶ Core interfaces
  Adapters ──may import──▶ Core utils (id-generator, logger)
  Adapters ──MUST NOT───▶ Core services, server routes, or other adapters

  Core ──may import──▶ nothing outside core (interfaces are at the root)

  Server ──may import──▶ Core + Adapters (via registry)
  Server ──MUST NOT───▶ Adapter internals directly

This prevents spaghetti coupling. Adapters are truly swappable black boxes.

On this page