Middlewares

Secure, rate-limit, and monitor your bot with built-in middlewares. Create custom middleware for authentication, logging, and request enrichment.

Middlewares

Middlewares execute in sequence for every incoming message. They can enrich the context, block requests, or perform side effects like logging and analytics. The framework ships with three production-ready middlewares and a flexible API for custom ones.


Middleware Contract

type Middleware<TContextIn, TContextOut> = (
  ctx: TContextIn,
  next: () => Promise<void>,
) => Promise<void | Partial<TContextOut>>;
ActionHow
Pass controlCall await next()
Block requestDon't call next() — pipeline stops here
Enrich contextReturn an object — merged into context for downstream handlers
Side effectsDo work before and/or after next()
Error handlingWrap next() in try/catch

Built-in Middlewares

Auth Middleware

Controls access based on user IDs, channel IDs, custom logic, or roles.

import { authMiddleware, authPresets, roleMiddleware } from '@igniter-js/bot';

Basic Usage

// Allow only specific users
builder.addMiddleware(authMiddleware({
  allowedUsers: ['user_123', 'user_456'],
  unauthorizedMessage: '⛔ You are not authorized.',
}));

// Block specific users
builder.addMiddleware(authMiddleware({
  blockedUsers: ['spammer_789'],
}));

// Allow only in specific channels
builder.addMiddleware(authMiddleware({
  allowedChannels: ['channel_support', 'channel_vip'],
}));

Custom Authorization

builder.addMiddleware(authMiddleware({
  checkFn: async (ctx) => {
    const user = await database.getUser(ctx.message.author.id);
    return user?.plan === 'premium'; // Only premium users
  },
  unauthorizedMessage: (ctx) => `Upgrade to premium, ${ctx.message.author.name}!`,
  skip: (ctx) => ctx.message.content?.type === 'command' && ctx.message.content.command === 'help',
  onUnauthorized: async (ctx) => {
    await analytics.track('unauthorized_access', { userId: ctx.message.author.id });
  },
}));
OptionTypeDescription
allowedUsersstring[]Only these user IDs can access
allowedChannelsstring[]Only these channel IDs are active
blockedUsersstring[]These user IDs are blocked
blockedChannelsstring[]These channel IDs are blocked
checkFn(ctx) => boolean | Promise<boolean>Custom authorization logic
unauthorizedMessagestring | (ctx) => stringMessage sent on denial
skip(ctx) => boolean | Promise<boolean>Skip auth under certain conditions
onUnauthorized(ctx) => void | Promise<void>Custom handler for denied access

Role-Based Auth

builder.addMiddleware(roleMiddleware({
  getRoles: async (userId) => {
    const user = await db.users.findById(userId);
    return user?.roles ?? [];
  },
  requiredRoles: ['admin', 'moderator'],
  unauthorizedMessage: '🔒 Admins and moderators only.',
}));

Pre-built Presets

// Admin-only bot
builder.addMiddleware(authPresets.adminsOnly(['admin_001', 'admin_002']));

// Private chat only
builder.addMiddleware(authPresets.privateOnly());

// Group chat only
builder.addMiddleware(authPresets.groupsOnly());

// Whitelist
builder.addMiddleware(authPresets.whitelist(['user_a', 'user_b']));

// Blacklist
builder.addMiddleware(authPresets.blacklist(['spammer_x']));

Rate Limit Middleware

Prevents abuse by limiting the number of requests per user within a time window.

import { rateLimitMiddleware, rateLimitPresets, memoryRateLimitStore } from '@igniter-js/bot';

Basic Usage

builder.addMiddleware(rateLimitMiddleware({
  maxRequests: 10,
  windowMs: 60_000, // 1 minute
  message: '⚠️ Too many requests. Please wait a minute.',
}));
OptionTypeDefaultDescription
maxRequestsnumberRequiredMax requests in the window
windowMsnumberRequiredTime window in milliseconds
storeRateLimitStoreMemoryRateLimitStoreStorage backend
keyGenerator(ctx) => stringprovider:userIdCustom key for rate limiting
messagestring | (ctx, retryAfter) => stringDefault messageResponse on limit exceeded
skip(ctx) => boolean | Promise<boolean>Skip rate limiting
onLimitReached(ctx, retryAfter) => void | Promise<void>Custom handler

Custom Key Generator

Rate limit by a combination of provider, user, and command:

builder.addMiddleware(rateLimitMiddleware({
  maxRequests: 3,
  windowMs: 10_000, // 10 seconds
  keyGenerator: (ctx) => {
    const cmd = ctx.message.content?.type === 'command'
      ? ctx.message.content.command
      : 'message';
    return `${ctx.provider}:${ctx.message.author.id}:${cmd}`;
  },
  message: (ctx, retryAfter) =>
    `Command rate limit reached. Try again in ${retryAfter}s.`,
}));

Redis Store

For production, use a shared store like Redis:

import { RateLimitStore } from '@igniter-js/bot';

class RedisRateLimitStore implements RateLimitStore {
  constructor(private redis: Redis) {}

  async get(key: string): Promise<number> {
    return parseInt(await this.redis.get(key) || '0');
  }

  async increment(key: string, windowMs: number): Promise<number> {
    const count = await this.redis.incr(key);
    await this.redis.pexpire(key, windowMs);
    return count;
  }

  async reset(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

builder.addMiddleware(rateLimitMiddleware({
  maxRequests: 20,
  windowMs: 60_000,
  store: new RedisRateLimitStore(redisClient),
}));

Pre-built Presets

// Strict: 5 req/min
builder.addMiddleware(rateLimitPresets.strict());

// Moderate: 10 req/min
builder.addMiddleware(rateLimitPresets.moderate());

// Lenient: 20 req/min
builder.addMiddleware(rateLimitPresets.lenient());

// Per-command: 3 req/10s per command
builder.addMiddleware(rateLimitPresets.perCommand());

Logging Middleware

Structured logging for messages, commands, errors, and performance metrics.

import { loggingMiddleware, loggingPresets, commandLoggingMiddleware } from '@igniter-js/bot';

Basic Usage

builder.addMiddleware(loggingMiddleware({
  logMessages: true,
  logCommands: true,
  logErrors: true,
  logMetrics: true,
  includeUserInfo: true,
  includeContent: false, // Security: don't log message content
}));
OptionTypeDefaultDescription
loggerBotLoggerconsoleLogger instance (Pino, Winston, etc.)
logMessagesbooleantrueLog incoming messages
logCommandsbooleantrueLog command executions
logErrorsbooleantrueLog errors
logMetricsbooleantrueLog execution time
includeUserInfobooleantrueInclude user id/username
includeContentbooleanfalseInclude message content (security risk)
formatter(ctx, event, data) => stringDefault formatCustom log format
skip(ctx) => booleanSkip logging conditions

With Structured Logger

import pino from 'pino';

const logger = pino({ level: 'info' });

builder.addMiddleware(loggingMiddleware({
  logger,
  logMessages: true,
  logMetrics: true,
  includeUserInfo: true,
  includeContent: false,
}));

The middleware calls logger.info(), logger.debug(), logger.warn(), and logger.error() — compatible with any logger implementing the BotLogger interface.

Pre-built Presets

// Minimal: only errors
builder.addMiddleware(loggingPresets.minimal());

// Standard: messages, commands, errors
builder.addMiddleware(loggingPresets.standard());

// Verbose: everything including metrics and content
builder.addMiddleware(loggingPresets.verbose());

// Production: standard logging without PII
builder.addMiddleware(loggingPresets.production());

// Debug: full JSON output for troubleshooting
builder.addMiddleware(loggingPresets.debug());

Command-Specific Logging

For detailed per-command logs with parameters:

builder.addMiddleware(commandLoggingMiddleware({
  logger: pino(),
  includeParams: true,
}));

// Output:
// [COMMAND] /search by @username
// [PARAMS] ["typescript books"]
// [SUCCESS] /search completed in 45ms

Custom Middleware

Context Enrichment

Add properties to the context for downstream handlers:

builder.addMiddleware(async (ctx, next) => {
  const user = await db.users.findById(ctx.message.author.id);
  await next();
  return { user }; // ctx.user now available in all subsequent handlers
});

Timing & Performance

builder.addMiddleware(async (ctx, next) => {
  const start = performance.now();
  try {
    await next();
  } finally {
    const ms = performance.now() - start;
    metrics.histogram('bot.request.duration', ms, {
      provider: ctx.provider,
      command: ctx.message.content?.type === 'command' ? ctx.message.content.command : 'message',
    });
  }
});

Feature Flags

builder.addMiddleware(async (ctx, next) => {
  const flags = await featureFlags.getForUser(ctx.message.author.id);

  if (!flags.newSearchEnabled && ctx.message.content?.type === 'command' && ctx.message.content.command === 'search') {
    await ctx.reply('The new search feature is not available for your account yet.');
    return; // Block pipeline
  }

  await next();
});

Error Boundary

builder.addMiddleware(async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    await errorTracking.captureException(error, {
      userId: ctx.message.author.id,
      provider: ctx.provider,
      command: ctx.message.content?.type === 'command' ? ctx.message.content.command : undefined,
    });
    await ctx.reply('⚠️ An unexpected error occurred. Our team has been notified.');
    // Don't re-throw — gracefully handled
  }
});

Middleware Ordering Matters

The order you add middlewares determines the execution order:

1. Logging middleware      → Logs before/after everything
2. Rate limit middleware   → Check limits early
3. Auth middleware         → Block unauthorized users
4. Custom enrichment       → Load user/profile data
5. Command handler         → Execute the command
builder
  .addMiddleware(loggingPresets.production())     // 1st: log everything
  .addMiddleware(rateLimitPresets.moderate())      // 2nd: check rate limits
  .addMiddleware(authPresets.whitelist(['...']))   // 3rd: auth check
  .addMiddleware(async (ctx, next) => {            // 4th: enrich context
    const user = await loadUser(ctx.message.author.id);
    await next();
    return { user };
  });

Place rate limiting before auth so that rate limits apply to unauthorized users too — otherwise attackers can bypass rate limits by triggering auth failures.


Next Steps