Server Hooks

Last Updated: October 2026

This page documents every server-side action and filter hook that the poker engine fires. Plugins register for hooks via registry.addAction() and registry.addFilter().

Overview

┌──────────────────────────────────────────────────────────────┐
│                     Hook Types                               │
│                                                              │
│  Action    registry.addAction(hook, callback, priority?)     │
│            - Fire-and-forget; callback return value ignored  │
│            - Callback may be async                           │
│                                                              │
│  Filter    registry.addFilter(hook, callback, priority?)     │
│            - Callback MUST return the (possibly modified)    │
│              first argument                                  │
│            - Filters chain in priority order; synchronous    │
└──────────────────────────────────────────────────────────────┘

Lower priority numbers run first (default: 10).

Startup hooks — hooks marked ⚠ fire during server initialisation. When a plugin is activated at runtime, the engine replays its server:routes:* and server:ready callbacks with the recorded arguments, so they still run. server:boot is not replayed; only plugins loaded at startup receive it. Replay does not happen on upgrade: restart the server after upgrading a plugin that relies on these hooks.

Server Lifecycle

server:boot ⚠

Type: Action When: After plugins load and before the Express application is created. Not replayed for plugins activated at runtime. Arguments: none Source: src/index.ts

Use this to perform setup that must happen before any routes or middleware are registered.

registry.addAction('server:boot', () => {
  registry.logger.info('Early boot hook called');
});

server:ready ⚠

Type: Action When: After server.listen() completes and the WebSocket server is running. Arguments: server: HttpServer Source: src/index.ts (dispatched through src/core/plugins/plugin-lifecycle.service.ts)

The server argument is the raw Node.js http.Server instance. Use it to attach additional WebSocket listeners or to record the listening address.

registry.addAction('server:ready', (server) => {
  const addr = server.address();
  registry.logger.info(`Server ready on port ${addr.port}`);
});

server:shutdown

Type: Action When: When a SIGTERM or SIGINT signal is received. The engine enters graceful shutdown after all server:shutdown callbacks return. Any plugin loaded at that moment receives it, including plugins activated at runtime. Arguments: none Source: src/index.ts

Use this to flush queues, close external connections, or write final state before the process exits.

registry.addAction('server:shutdown', async () => {
  await myExternalClient.disconnect();
  registry.logger.info('Plugin shut down cleanly');
});

Route Registration

These hooks fire once per route module during startup, and are replayed for plugins activated at runtime. Use them to append additional Express routes or middleware to an existing router.

server:routes:auth ⚠

Type: Action When: After the auth router is mounted at /api/v1/auth. Arguments: router: Router Source: src/app.ts

registry.addAction('server:routes:auth', (router) => {
  router.post('/custom-login', async (req, res) => {
    // Custom authentication endpoint appended to /api/v1/auth/
    res.json({ ok: true });
  });
});

server:routes:cashier ⚠

Type: Action When: After the cashier router is mounted at /api/v1/cashier. Arguments: router: Router Source: src/app.ts

registry.addAction('server:routes:cashier', (router) => {
  router.get('/my-provider/rates', async (req, res) => {
    res.json({ rate: 1.0 });
  });
});

server:routes:admin ⚠

Type: Action When: At the end of the admin sub-router composition. Arguments: router: Router, a dedicated router under /api/v1/admin with authenticate and requireAdmin already applied. Source: src/api/routes/admin/plugin-admin-router.ts

registry.addAction('server:routes:admin', (router) => {
  router.get('/my-plugin/report', async (req, res) => {
    res.json({ data: [] });
  });
});

server:routes:api ⚠

Type: Action When: After all route modules are mounted on the Express app. Arguments: router: Router, a dedicated router mounted at the root of the app. Source: src/app.ts

This is the catch-all route hook. Use it when you need routes outside /api/v1/plugins/{providerType} and outside a specific route module. No auth is applied.

registry.addAction('server:routes:api', (router) => {
  router.use('/custom-prefix', myRouter);
});

Plugin Lifecycle

These hooks fire as the engine activates, deactivates, or force-disables plugins.

plugin:activated

Type: Action When: After a plugin is activated through the admin panel and its database record is updated. Does not fire for plugins loaded at startup or on upgrade. Arguments: providerType: string Source: src/core/plugins/plugin-manager.service.ts

registry.addAction('plugin:activated', (providerType) => {
  if (providerType === 'my-dependency') {
    // A plugin your plugin depends on just became active
    initDependentFeature();
  }
});

plugin:deactivating

Type: Action When: During deactivation through the admin panel, after the active-payment-system check and before the plugin’s hooks and routes are removed. Arguments: providerType: string Source: src/core/plugins/plugin-manager.service.ts

This is the hook to use for a plugin’s own cleanup, such as removing an external webhook. Guard on providerType, because it fires for every plugin.

registry.addAction('plugin:deactivating', async (providerType) => {
  if (providerType !== registry.providerType) return;
  await deleteExternalWebhook();
});

plugin:deactivated

Type: Action When: Immediately after a plugin has been deactivated and its hooks/routes cleaned up. The deactivated plugin’s own callbacks no longer receive it. Arguments: providerType: string Source: src/core/plugins/plugin-manager.service.ts

plugin:auto-disabled

Type: Action When: When the circuit breaker triggers — the plugin reached 50 errors within a 5-minute window. The disabled plugin’s own callbacks no longer receive it. Arguments: providerType: string Source: src/core/plugins/circuit-breaker.ts

registry.addAction('plugin:auto-disabled', (providerType) => {
  registry.logger.error(`Plugin ${providerType} was auto-disabled`);
  // Notify an external alerting system
});

Settings

plugin:settings:saved

Type: Action When: After a plugin’s settings have been validated, encrypted, and written to the database. Fires for every plugin’s save. Arguments: providerType: string, settings: Record<string, string> Source: src/api/routes/admin/plugins.routes.ts

settings contains the plain-text values written by this save, including passwords. A password left unchanged is not written, so it is omitted; read it with registry.settings.get().

Always guard against other plugins’ settings by checking providerType:

registry.addAction('plugin:settings:saved', async (providerType, settings) => {
  if (providerType !== registry.providerType) return;
  reinitialiseApiClient(settings.apiKey, settings.apiSecret ?? await registry.settings.get('apiSecret'));
});

plugin:settings:validate

Type: Filter When: Before settings are written to the database, after built-in validation. Initial value: { valid: true, errors: string[] }, where errors holds any built-in validation errors Additional arguments: settings: Record<string, string>, providerType: string Expected return: The (possibly modified) { valid, errors } object Source: src/api/routes/admin/plugins.routes.ts

If errors is not empty when all filter callbacks have run, the settings save is rejected and the errors are returned to the admin UI. Setting valid to false without adding an error does not reject the save.

Only the callbacks registered by the plugin being saved run, so you do not need a providerType guard. A password left unchanged arrives as ********.

registry.addFilter('plugin:settings:validate', (result, settings) => {
  if (!settings.apiKey) {
    result.errors.push('API key is required');
    return result;
  }

  if (!settings.apiKey.startsWith('sk_live_') && settings.environment === 'production') {
    result.errors.push('A live API key is required for the production environment');
  }

  return result;
});

Discovery Filters

auth:providers

Type: Filter When: On GET /api/v1/auth/providers. Initial value: string[], starting as ['local'] Source: src/api/routes/auth.routes.ts

cashier:deposit:methods / cashier:withdraw:methods

Type: Filter When: On GET /api/v1/cashier/payment-methods. Initial value: Array of active PaymentSystemConfig objects for the direction. See Extension Points for the fields the route reads. Source: src/api/routes/cashier.routes.ts

Game Lifecycle Hooks

Game Hooks

Stability: Experimental — These hooks are observation-only. Callbacks must not throw or block.

Game hooks fire after internal game events. They run asynchronously and do not block the game loop.

registry.addAction('game:hand:completed', async (data: unknown) => {
  const { tableId, handNumber, winners } = data as {
    tableId: string;
    handNumber: number;
    winners: Array<{ playerId: string; amount: number }>;
  };
  // Log, update statistics, trigger achievements, etc.
  registry.logger.info(`Hand #${handNumber} completed on table ${tableId}`);
});
HookArgumentsWhen
game:hand:started{ tableId: string, handNumber: number, dealerSeat: number }After a new hand begins
game:hand:completed{ tableId: string, handNumber: number, winners: Array<{ playerId: string; amount: number }> }After a hand completes
game:player:seated{ tableId: string, userId: string, seatIndex: number }After a player joins a table
game:player:left{ tableId: string, userId: string, reason: 'voluntary' | 'forced' }After a player leaves a table

Plugin-to-Plugin Communication (Server)

Plugins can fire their own namespaced hooks using registry.doAction(). Other plugins can listen with registry.addAction().

Hooks are automatically namespaced to plugin:{providerType}:* — a plugin cannot fire hooks outside its own namespace.

// In plugin A (providerType: 'steem'):
registry.doAction('plugin:steem:deposit-confirmed', { userId, amount });

// In plugin B (any providerType):
registry.addAction('plugin:steem:deposit-confirmed', async (data: unknown) => {
  // React to steem deposits
});

Hook Summary

HookTypeStartupArguments
server:bootAction⚠—
server:readyAction⚠server: HttpServer
server:shutdownAction—
server:routes:authAction⚠router: Router
server:routes:cashierAction⚠router: Router
server:routes:adminAction⚠router: Router
server:routes:apiAction⚠router: Router
plugin:activatedActionproviderType: string
plugin:deactivatingActionproviderType: string
plugin:deactivatedActionproviderType: string
plugin:auto-disabledActionproviderType: string
plugin:settings:savedActionproviderType: string, settings: Record<string, string>
plugin:settings:validateFiltervalue: { valid, errors }, settings, providerType
auth:providersFiltervalue: string[]
cashier:deposit:methodsFiltervalue: method array
cashier:withdraw:methodsFiltervalue: method array
game:hand:startedAction{ tableId, handNumber, dealerSeat }
game:hand:completedAction{ tableId, handNumber, winners: Array<{ playerId, amount }> }
game:player:seatedAction{ tableId, userId, seatIndex }
game:player:leftAction{ tableId, userId, reason: 'voluntary' | 'forced' }
ResourceLink
Plugin Development Guidedeveloper/plugins
Client Hooks Referencedeveloper/plugins-hooks-client
Plugin Lifecycle Referencedeveloper/plugins-lifecycle
Hook registry sourcesrc/core/hooks/hook-registry.ts
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.