Lifecycle

Last Updated: October 2026

This document describes every stage a plugin passes through — from initial upload to deletion — including the error resilience mechanisms that protect the engine.

Lifecycle Overview

                 ┌─────────────┐
                 │   .tgz file │
                 └──────┬──────┘
                        │
                        ▼
               ┌─────────────────┐
               │   1. INSTALL    │  Validate → Extract → npm install → DB record
               └────────┬────────┘
                        │ isActive: false
                        ▼
               ┌─────────────────┐
               │   2. ACTIVATE   │  Version gate → register() → DB update → WS broadcast
               └────────┬────────┘
                        │ isActive: true
            ┌───────────┼────────────┐
            ▼           ▼            ▼
    ┌──────────────┐  ┌──────────┐  ┌──────────────────┐
    │ 3. DEACTIVATE│  │4. UPGRADE│  │ 5. AUTO-DISABLE  │
    │  (manual)    │  │          │  │ (circuit breaker)│
    └──────┬───────┘  └────┬─────┘  └────────┬─────────┘
           │               │                 │
           │               │ re-activates    │ isActive: false
           ▼               ▼                 │ lastError set
    ┌──────────────┐  ┌──────────────┐       ▼
    │ isActive:    │  │ isActive:    │  ┌──────────────┐
    │ false        │  │ true (new v) │  │  6. DELETE   │
    └──────┬───────┘  └──────────────┘  │  (optional)  │
           │                            └──────────────┘
           ▼
    ┌──────────────┐
    │  6. DELETE   │
    │  (optional)  │
    └──────────────┘

1. Install

Endpoint: POST /api/v1/admin/plugins/upload (super-admin only, multipart form-data)

Steps

  1. The uploaded archive is validated before extraction:
    • Extension must be .tgz or .tar.gz
    • Archive entries are scanned before extraction: path-traversal patterns (../, absolute paths) and entries other than regular files and directories are rejected
    • At most 5,000 files, 20 directory levels and 50 MB uncompressed
  2. The archive is extracted to a temporary directory, and the extracted tree is scanned again. Symlinks, hard links and special files are rejected.
  3. Package validation:
    • package.json must exist and have name and version
    • package.json must contain a pokerEnginePlugin field with valid providerType and displayName
    • providerType must match ^[a-z][a-z0-9_-]*$
    • providerType must be unique — a plugin with the same type must not already be installed
  4. The files are copied to plugins/{providerType}/.
  5. npm install --omit=dev --ignore-scripts is run inside that directory (60-second timeout). A failure is logged and installation continues.
  6. A Plugin record is created in the database with isActive: false.

Result: Plugin is installed but not running. No code is imported. The admin UI lists the plugin as inactive.

Error States

ErrorResult
Path-traversal or unsafe entry detectedArchive rejected before extraction
Size, file-count or depth limit exceededTemporary files cleaned up, rejected
Invalid package.jsonTemporary files cleaned up, rejected
Duplicate providerTypeTemporary files cleaned up, rejected
npm install failsLogged as a warning; installation continues

2. Activate

Endpoint: POST /api/v1/admin/plugins/:id/activate (super-admin only)

Steps

  1. Version gate — if set, apiVersion must fall within 1.0.0–1.999.999. If minEngineVersion or maxEngineVersion are set, the running engine version is checked.
  2. The plugin’s server entry (dist/server/index.js) is dynamically imported with a cache-busting query parameter (?v={timestamp}).
  3. register(registry) is called with a 10-second timeout. If the timeout expires, activation rolls back.
  4. process.exit() is blocked while the module loads and while register() runs. The call is logged and ignored, and activation continues.
  5. A PaymentSystem row is created for each registered provider type that has none.
  6. The plugin’s own server:routes:* and server:ready callbacks are replayed with the arguments recorded at startup, so these hooks work for plugins activated at runtime. server:boot is not replayed. If a replayed callback throws, activation rolls back.
  7. The Plugin record is updated: isActive: true, lastError: null.
  8. A plugin:activated server action fires.
  9. A plugin:activated WebSocket event is broadcast to all connected clients with { providerType, bundleUrl }.
  10. Connected clients receive the WebSocket event and dynamically inject the frontend bundle (dist/client/index.js).
  11. If hasHealthCheck: true, healthCheck(registry) runs in the background. An unhealthy result is recorded but does not block activation.

Result: Plugin is running. Routes respond, hooks are registered, frontend components are rendered.

Rollback on Failure

If register() throws or times out, or a replayed lifecycle callback throws:

  • All hooks registered so far are removed
  • All routes registered so far are unmounted
  • All providers registered so far are unregistered
  • The Plugin record is updated: isActive: false, lastError: <error message>
  • No WebSocket event is broadcast

3. Deactivate

Endpoint: POST /api/v1/admin/plugins/:id/deactivate (super-admin only)

Steps

  1. If any PaymentSystem whose type equals the plugin’s providerType is active for deposits or withdrawals, deactivation is blocked with a 409 error. Disable the payment system first.
  2. A plugin:deactivating server action fires. This is the plugin’s last chance to clean up, because its own callbacks are removed next.
  3. All registered hooks (actions and filters) are removed from the server hook registry.
  4. All registered routes are unmounted from the plugin router dispatcher.
  5. All registered providers are unregistered from the provider registry.
  6. The Plugin record is updated: isActive: false.
  7. A plugin:deactivated server action fires. The deactivated plugin’s own callbacks no longer receive it.
  8. A plugin:deactivated WebSocket event is broadcast to all connected clients.
  9. Connected clients unregister all slot fills, pages, and client hooks registered by this providerType.

Result: Plugin is stopped. Routes return 404, hooks no longer fire, frontend components are removed — without a page reload.

4. Upgrade

Endpoint: POST /api/v1/admin/plugins/:id/upgrade (super-admin only, multipart form-data)

An atomic swap that replaces the plugin files while preserving settings and the database record.

Steps

  1. The new archive is validated identically to the install flow, except that the providerType must match the installed plugin’s.
  2. The files are copied to plugins/{providerType}-next.
  3. npm install --omit=dev --ignore-scripts is run for the new version. A failure is logged and the upgrade continues.
  4. If the plugin was active, its hooks, routes and providers are removed. The database record stays active.
  5. Atomic swap:
    • plugins/{providerType} → plugins/{providerType}-bak (old version backed up)
    • plugins/{providerType}-next → plugins/{providerType} (new version moved into place)
  6. The new version is imported and register() runs with the new code.
  7. The Plugin record is updated with the new version and manifest fields. isActive and settings are preserved.
  8. plugins/{providerType}-bak is deleted.
  9. A plugin:updated WebSocket event is broadcast to connected clients.

Upgrade does not fire the plugin:activated server action, replay server:routes:* or server:ready, or attach the new version’s healthCheck export. Restart the server after upgrading a plugin that relies on those.

Rollback on Failure

If the swap or the new version’s register() fails:

  • plugins/{providerType}-bak is moved back to plugins/{providerType} (old version restored)
  • If the plugin was active before the upgrade attempt, the old version’s register() runs again
  • The Plugin record retains the old version and isActive state

Result: Either the new version is running, or the old version is fully restored. The file system is never left in a partially-upgraded state.

5. Auto-Disable (Circuit Breaker)

The circuit breaker automatically disables a plugin that is generating too many errors.

Trigger Conditions

SourceError Count
Hook callback throws+1 per error
Health check returns healthy: false+10
Unhandled promise rejection attributed to plugin+10
Uncaught exception attributed to plugin+10

Threshold: 50 errors within a 5-minute window. The count resets once the window is more than 5 minutes old.

Attribution

Process-level errors (unhandledRejection, uncaughtException) are attributed to a plugin by inspecting the stack trace for the pattern plugins/{directory}/. If a loaded plugin’s providerType matches that directory name, the error is counted against it.

On Threshold Exceeded

  1. All plugin hooks are removed.
  2. All plugin routes are unmounted.
  3. All plugin providers are unregistered.
  4. Plugin record updated: isActive: false, lastError: 'Auto-disabled: exceeded 50 errors in 5 minutes'.
  5. plugin:auto-disabled server action fires. The disabled plugin’s own callbacks no longer receive it.
  6. plugin:auto-disabled WebSocket event broadcast to clients.

Result: Plugin is disabled. Manual re-activation via the admin panel is required. Investigate the lastError field and server logs before re-activating.

6. Delete

Endpoint: DELETE /api/v1/admin/plugins/:id (super-admin only)

The plugin must be inactive before it can be deleted.

Steps

  1. The plugin’s install directory is removed from the filesystem.
  2. The Plugin database record is deleted.
  3. All PluginSetting records for this providerType are cascade-deleted.

Result: Plugin is fully removed. Settings cannot be recovered. Re-uploading the same providerType will start with an empty settings store.

Startup Loading

On server start, loadPlugins() loads plugins from three sources in order:

  1. PAYMENT_PLUGINS environment variable — Comma-separated package names. These are loaded from node_modules (not the plugins/ directory) and are useful for bundling a payment provider with the deployment. The package name is used as the providerType, and version gates are not applied. Skipped if a matching Plugin record is inactive.
  2. plugins/ directory — Auto-discovered directories containing a package.json with pokerEnginePlugin.providerType and an existing entry file. Skipped if the Plugin record is inactive. On success, a Plugin record is created or updated with isActive: true.
  3. Database records — Plugin rows where isActive: true that were not already loaded by the earlier steps.

A plugin already loaded by an earlier step is not loaded again.

If a plugin fails to load at startup:

  • Its partial registrations are rolled back
  • If it was loaded from a database record, isActive is set to false and lastError is set to Startup load failed: <reason>
  • The engine continues starting — a failed plugin does not prevent the server from starting

Safe Mode

Set SAFE_MODE=true in the environment to skip all plugin loading at startup:

SAFE_MODE=true node dist/index.js

Useful when a plugin is causing the server to fail to start. Once the server is running, the problematic plugin can be deactivated or deleted via the admin panel, and SAFE_MODE removed. GET /api/v1/admin/plugins returns safeMode so the admin panel can show this state.

WebSocket Events

EventDirectionPayloadWhen
plugin:activatedServer → Client{ providerType: string, bundleUrl?: string }After activation
plugin:deactivatedServer → Client{ providerType: string }After deactivation
plugin:updatedServer → Client{ providerType: string, bundleUrl?: string }After upgrade
plugin:auto-disabledServer → Client{ providerType: string, reason: string }Circuit breaker triggered

The bundleUrl field is set when hasFrontend: true and contains the URL of the client bundle to inject (e.g., /plugins/{providerType}/dist/client/index.js).

Clients subscribe via usePluginSync(), which is mounted in the root App component.

Database Models

Plugin
  id               UUID (PK)
  name             string (unique)         — from pokerEnginePlugin.displayName
  packageName      string                  — from package.json "name"
  version          string                  — from package.json "version"
  providerType     string (unique)         — from pokerEnginePlugin.providerType
  description      string?
  installPath      string                  — absolute path to the server entry file
  configFields     JSON                    — from pokerEnginePlugin.configFields
  hasFrontend      boolean
  hasHealthCheck   boolean
  frontendBundlePath string?
  isActive         boolean                 — false after install, true after activate
  lastError        string?                 — set on failure or circuit breaker trigger
  lastHealthCheck  DateTime?
  healthStatus     string?                 — "healthy" | "unhealthy"
  installedAt      DateTime
  updatedAt        DateTime
  installedBy      string                  — userId of the admin who installed, or "system" for auto-discovered plugins

PluginSetting
  id               UUID (PK)
  providerType     string                  — FK to Plugin.providerType (cascade delete)
  key              string
  value            string                  — AES-encrypted for password fields
  updatedAt        DateTime
  UNIQUE(providerType, key)

Error Reference

ErrorAdmin Panel MessageResolution
Archive contains path traversal, links or special filesUpload rejectedRebuild archive with regular files and safe paths
Extracted size > 50 MBUpload rejectedReduce bundle size; exclude node_modules
More than 5,000 files or 20 directory levelsUpload rejectedReduce the package contents
Invalid package.jsonUpload rejectedAdd valid pokerEnginePlugin field
Duplicate providerTypeUpload rejectedDelete existing plugin first, or upgrade
npm install failedNone (logged as a warning)Check that dependencies resolve correctly
apiVersion unsupportedActivation blockedUpdate plugin to use apiVersion: "1.0.0"
Engine version mismatchActivation blockedUpdate minEngineVersion/maxEngineVersion
register() timeoutActivation rolled backReduce work done in register(); defer to actions
register() threwActivation rolled backCheck lastError field and server logs
Replayed server:routes:* or server:ready callback threwActivation rolled backCheck lastError field and server logs
Active payment system existsDeactivation blockedDisable the payment system before deactivating
Plugin is activeDelete blockedDeactivate first
Circuit breaker triggeredAuto-disabledCheck lastError, fix plugin, re-activate
ResourceLink
Plugin Development Guidedeveloper/plugins
Server Hooks Referencedeveloper/plugins-hooks-server
Admin: Pluginsadmin/plugins
Plugin manager sourcesrc/core/plugins/plugin-manager.service.ts
Circuit breaker sourcesrc/core/plugins/circuit-breaker.ts
Plugin loader sourcesrc/core/plugins/plugin-loader.ts
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.