# How to Implement Custom Middleware in OmniRoute: A Complete Guide

> Implement custom middleware in OmniRoute by registering JavaScript hooks with registerHook(). Execute your code in a sandboxed Node VM before routing.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-17

---

**To implement custom middleware in OmniRoute, register a JavaScript hook via the `registerHook()` function in [`src/lib/middleware/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/middleware/registry.ts), which executes your code in a sandboxed Node VM with a 5-second timeout before the routing phase.**

OmniRoute provides an extensible **pre-request hook pipeline** that allows you to intercept, mutate, or block requests before they reach provider selection and execution. This guide explains how to leverage the hook registry, types, and persistence layers to build production-ready custom middleware for the `diegosouzapw/OmniRoute` repository.

## Understanding the Middleware Architecture

OmniRoute's middleware system centers on three interconnected components that manage the lifecycle of custom hooks.

### Core Components

The **hook registry** ([`src/lib/middleware/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/middleware/registry.ts)) acts as a singleton that stores definitions, compiles functions, tracks execution logs, and manages health statistics. It handles registration order and ensures hooks run sequentially by priority.

Strong typing is enforced through **hook types** ([`src/lib/middleware/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/middleware/types.ts)), which define contracts for `HookConfig`, `PreRequestHookContext`, and `HookResult` objects. These guarantees ensure every hook receives a safe sandbox while allowing controlled mutations to specific request properties.

For durability, a **persistence layer** ([`src/lib/db/middleware.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/middleware.ts)) provides CRUD operations against the `middleware_hooks` SQLite table, enabling hooks to survive restarts and be managed via the admin UI or API.

### Execution Flow

When a request hits an API route like `/api/v1/chat/completions`, OmniRoute executes the following steps:

- **Shared middleware** runs first (e.g., `requireJsonContentType` or `bodySizeGuard`).
- **Context creation**: `createHookContext()` builds a `PreRequestHookContext` containing the parsed body, headers, model name, optional combo ID, and logger.
- **Registry execution**: `runHooks(context, comboId?)` loads all enabled hooks matching the request scope (global or combo-specific), sorts them by `priority`, and executes each in a Node VM sandbox with a hard `HOOK_EXECUTION_TIMEOUT_MS` of 5000 milliseconds.
- **Result processing**: Each hook returns a `HookResult` that may mutate `body`, `headers`, `model`, or `combo`, provide a `response` to short-circuit the pipeline, or set `skipRemaining` to halt further processing.
- **Post-hook routing**: The potentially modified request proceeds to combo routing and execution.

### Safety Guarantees

Hooks run inside a `vm.Script` with a deliberately restricted global object constructed by `createHookSandbox()` (lines 76-108 of the registry). This sandbox explicitly removes `process`, `require`, `fetch`, `Buffer`, and timers to prevent unauthorized system access. An async `Promise.race` guard ensures runaway code aborts after 5 seconds.

## Implementing Your First Custom Hook

You can implement custom middleware in OmniRoute by writing JavaScript code that receives a mutable `context` object and returns a `HookResult`.

### Basic Header Injection

To inject custom headers into every request, create a hook that modifies `context.headers`:

```typescript
// myHeaderHook.ts
export const code = `
  // Add a custom header that downstream handlers can read.
  context.headers['x-my-custom'] = 'omni-route';
  // No short-circuit, just mutate the request.
`;

```

Register the hook at startup or via the admin API:

```typescript
import { registerHook } from '@/lib/middleware/registry';
import { HookPriority } from '@/lib/middleware/types';

registerHook({
  name: 'addMyHeader',
  description: 'Inject X-My-Custom header into every request',
  priority: HookPriority.LOW,
  scope: { type: 'global' },
  enabled: true,
  code,
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
  runCount: 0,
});

```

The registry automatically compiles the code and queues it for execution after higher-priority hooks.

### Blocking Requests Conditionally

To short-circuit the pipeline and return an error response, provide a `response` object in your `HookResult`:

```typescript
// blockModelHook.ts
export const code = `
  const blockedModels = ['gpt-4-evil', 'claude-unstable'];
  if (blockedModels.includes(context.model)) {
    return {
      response: {
        status: 403,
        body: { error: { message: 'Model blocked by policy', type: 'invalid_request_error' } },
      },
    };
  }
`;

```

```typescript
import { registerHook } from '@/lib/middleware/registry';
import { HookPriority } from '@/lib/middleware/types';

registerHook({
  name: 'blockUnsafeModels',
  description: 'Reject requests that ask for disallowed models',
  priority: HookPriority.CRITICAL,
  scope: { type: 'global' },
  enabled: true,
  code,
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
  runCount: 0,
});

```

When a hook returns a `response`, `runHooks()` immediately stops processing and returns that response to the client.

## Registration Methods

OmniRoute supports multiple patterns for registering custom middleware, depending on your deployment and persistence requirements.

### Runtime Registration

For testing or dynamic administration, call `registerHook()` directly with a complete `HookConfig` object. This adds the hook to the in-memory registry without database persistence.

### Database Persistence

To survive restarts, persist hooks in the `middleware_hooks` table using SQL or the DB API (`createHook`, `updateHook`):

```sql
INSERT INTO middleware_hooks (
  name, description, priority, scope_type, enabled, code,
  created_at, updated_at, run_count, last_error
) VALUES (
  'logRequestBody',
  'Log first 200 chars of the request body for debugging',
  200,
  'global',
  1,
  'if (context.body && typeof context.body === "object") {
       const snippet = JSON.stringify(context.body).slice(0,200);
       context.log.info("bodyLog", `Body snippet: ${snippet}`);
     }',
  datetime('now'),
  datetime('now'),
  0,
  NULL
);

```

After insertion, OmniRoute's `loadHooksFromConfig()` function automatically compiles and installs the hook on the next startup or reload cycle.

### Combo-Scoped Hooks

Restrict middleware to specific provider combinations using combo-scoped hooks:

```typescript
registerHook({
  name: 'comboRateLimit',
  description: 'Apply extra rate-limit to the "expensiveCombo"',
  priority: HookPriority.HIGH,
  scope: { type: 'combo', comboId: 'expensiveCombo' },
  enabled: true,
  code: `
    // Simple in-memory counter for illustration
    const key = \`combo-\${context.combo}-\${context.headers['x-request-id']}\`;
    const count = Number(globalThis[key] ?? 0) + 1;
    globalThis[key] = count;
    if (count > 5) {
      return { 
        response: { 
          status: 429, 
          body: { error: { message: 'Combo rate limit exceeded' } } 
        } 
      };
    }
  `,
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
  runCount: 0,
});

```

Only requests resolving to `expensiveCombo` execute this hook; other traffic bypasses it entirely.

## Built-in Reference Implementations

Study these built-in examples in `src/shared/middleware/` to understand production patterns:

- **[`requireJsonContentType.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/requireJsonContentType.ts)**: Rejects non-JSON POSTs with a 415 response
- **[`correlationId.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/correlationId.ts)**: Creates async-local request IDs for distributed tracing
- **[`bodySizeGuard.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/bodySizeGuard.ts)**: Enforces per-route request size limits with 413 responses

These run before the hook pipeline in route handlers (e.g., [`src/app/api/v1/chat/completions/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/chat/completions/route.ts)), demonstrating how shared middleware and custom hooks cooperate.

## Summary

- **Custom middleware in OmniRoute** is implemented via sandboxed JavaScript hooks registered through [`src/lib/middleware/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/middleware/registry.ts).
- Hooks execute in a **Node VM sandbox** with a 5-second timeout and restricted globals (no `process`, `require`, or `fetch`).
- Use **`registerHook()`** for runtime registration or the **`middleware_hooks`** SQLite table for persistence across restarts.
- Hooks can **mutate** `body`, `headers`, `model`, and `combo`, or **short-circuit** the pipeline by returning a `response` object.
- Scope hooks **globally** or to specific **combo IDs** to control execution breadth.

## Frequently Asked Questions

### What is the execution timeout for custom middleware in OmniRoute?

OmniRoute enforces a hard timeout of **5000 milliseconds** (`HOOK_EXECUTION_TIMEOUT_MS`) for all custom hooks. The registry uses `Promise.race` to abort execution if code exceeds this limit, preventing runaway scripts from blocking the request pipeline.

### How do I stop processing remaining hooks and return immediately?

Return a `HookResult` containing a `response` object. When `runHooks()` detects a `response` property, it immediately halts further hook execution and returns your specified status code and body to the client. You can also set `skipRemaining: true` to stop processing without sending a response.

### Can I access external APIs or the filesystem from within a hook?

No. The **sandbox** constructed by `createHookSandbox()` in [`src/lib/middleware/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/middleware/registry.ts) explicitly removes `fetch`, `require`, `process`, `Buffer`, and all timer functions. Hooks operate in a restricted environment that can only read and mutate the provided `context` object.

### Where are custom hooks stored and how do they survive restarts?

Hooks persisted via the DB API or SQL are stored in the **`middleware_hooks`** SQLite table managed by [`src/lib/db/middleware.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/middleware.ts). On startup, `loadHooksFromConfig()` reads this table and automatically registers all enabled hooks, ensuring your custom middleware survives application restarts.