Jiffoo Docs
Developer

Plugin Runtime Architecture

Deep dive into Jiffoo Mall's plugin runtime architecture and multi-instance system

Plugin Runtime Architecture

This document provides a comprehensive overview of Jiffoo Mall's plugin runtime architecture, including the multi-instance model, database schema, lifecycle management, and security boundaries.

Overview

Jiffoo Mall's plugin system uses a two-tier architecture:

  1. Plugin Package (PluginInstall) - The installed plugin code and metadata
  2. Plugin Instance (PluginInstallation) - Runtime configurations that can be created multiple times per plugin

This separation enables powerful multi-tenancy and configuration management capabilities.

Multi-Instance Architecture

Conceptual Model

┌─────────────────────────────────────────────────────────────────┐
│                        Plugin Runtime System                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Plugin Package (PluginInstall)                                 │
│  └─ slug: "stripe"                                              │
│  └─ source: vendors/jiffoo-extensions-official/extensions/plugins/stripe/ │
│  └─ metadata: name, version, author, permissions                │
│                                                                   │
│     ┌───────────────────────────────────────────────────┐      │
│     │  Plugin Instances (PluginInstallation)            │      │
│     ├───────────────────────────────────────────────────┤      │
│     │  Instance 1: "default"                            │      │
│     │    - id: c1a2b3c4-...                             │      │
│     │    - enabled: true                                │      │
│     │    - config: { apiKey: "pk_live_..." }           │      │
│     ├───────────────────────────────────────────────────┤      │
│     │  Instance 2: "stripe-us"                          │      │
│     │    - id: d5e6f7g8-...                             │      │
│     │    - enabled: true                                │      │
│     │    - config: { apiKey: "pk_live_us_..." }        │      │
│     ├───────────────────────────────────────────────────┤      │
│     │  Instance 3: "stripe-eu"                          │      │
│     │    - id: h9i0j1k2-...                             │      │
│     │    - enabled: false                               │      │
│     │    - config: { apiKey: "pk_live_eu_..." }        │      │
│     └───────────────────────────────────────────────────┘      │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Key Concepts

  • Plugin Package: One installation per plugin (identified by slug)
  • Plugin Instance: Multiple configurations per plugin (identified by instanceKey or installationId)
  • Default Instance: Every plugin has a reserved default instance (cannot be deleted)
  • Instance Isolation: Each instance has its own configuration and enabled state

Database Schema

PluginInstall Table (Package Level)

Stores the installed plugin code and metadata.

FieldTypeDescription
idCUIDPrimary key
slugString (unique)Plugin identifier (format: ^[a-z][a-z0-9-]{0,30}[a-z0-9]$)
nameStringPlugin display name
versionStringSemantic version (format: MAJOR.MINOR.PATCH)
authorString?Plugin author
authorUrlString?Author website URL
descriptionString?Plugin description
categoryString?Plugin category (payment, shipping, etc.)
runtimeTypeStringinternal-fastify or external-http
entryModuleString?Entry point for internal-fastify (e.g., server/index.js)
externalBaseUrlString?Base URL for external-http
sourceStringInstallation source (builtin, local-zip, official-market)
installPathString?Relative path to plugin directory
zipHashString?SHA-256 hash for idempotent installation
manifestJsonString?Full manifest.json content
permissionsString?JSON array of declared permissions
deletedAtDateTime?Soft delete timestamp
installedAtDateTimeInstallation timestamp
updatedAtDateTimeLast update timestamp

Constraints:

  • slug must be unique
  • Cannot delete or update soft-deleted plugins (deletedAt != null)
  • Built-in plugins cannot be uninstalled

PluginInstallation Table (Instance Level)

Stores runtime instances and their configurations.

FieldTypeDescription
idUUIDInstallation ID (globally unique, non-reusable)
pluginSlugStringForeign key to PluginInstall.slug
instanceKeyStringInstance identifier (format: ^[a-z0-9-]{1,32}$)
enabledBooleanWhether this instance is enabled
configJsonString?Instance-specific configuration (max 64KB)
grantedPermissionsString?JSON array of granted permissions
deletedAtDateTime?Soft delete timestamp
createdAtDateTimeCreation timestamp
updatedAtDateTimeLast update timestamp

Constraints:

  • (pluginSlug, instanceKey) must be unique
  • instanceKey === 'default' instance cannot be deleted
  • Soft-deleted instances cannot be recreated (must use different instanceKey)
  • Cascade delete when plugin package is deleted

Relationship Diagram

PluginInstall (1) ───< (N) PluginInstallation
    slug                    pluginSlug (FK)
                           instanceKey

Example:
stripe ────< stripe + default
       └───< stripe + stripe-us
       └───< stripe + stripe-eu

Plugin Lifecycle

Installation Flow

┌─────────────────────────────────────────────────────────────────┐
│                     Plugin Installation                          │
└─────────────────────────────────────────────────────────────────┘


                    ┌─────────────────────┐
                    │  Upload ZIP file    │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Calculate SHA-256   │
                    │  Check for duplicate │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Extract to temp    │
                    │  directory          │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Validate manifest   │
                    │  - schemaVersion: 1  │
                    │  - slug format       │
                    │  - semver format     │
                    │  - runtimeType       │
                    │  - permissions array │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Security checks     │
                    │  - Zip slip          │
                    │  - File type filter  │
                    │  - Size limits       │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Move to extensions/ │
                    │  plugins/{slug}/     │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Create PluginInstall│
                    │  record in database  │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Create "default"    │
                    │  PluginInstallation  │
                    │  (enabled: true)     │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Plugin Ready        │
                    └─────────────────────┘

Instance Lifecycle

┌─────────────────────────────────────────────────────────────────┐
│                     Instance Lifecycle                           │
└─────────────────────────────────────────────────────────────────┘


                    ┌─────────────────────┐
                    │  Create Instance     │
                    │  POST /instances     │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Validate input      │
                    │  - instanceKey       │
                    │  - config size/depth │
                    │  - plugin exists     │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Store in database   │
                    │  - Generate UUID     │
                    │  - Set enabled=true  │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Instance Active     │
                    └─────────────────────┘

                    ┌─────────┴─────────┐
                    ▼                   ▼
        ┌──────────────────┐  ┌──────────────────┐
        │  Enable/Disable  │  │  Update Config   │
        │  (toggle flag)   │  │  (replace JSON)  │
        └──────────────────┘  └──────────────────┘
                    │                   │
                    └─────────┬─────────┘

                    ┌─────────────────────┐
                    │  Soft Delete         │
                    │  (set deletedAt)     │
                    │  - Disable instance  │
                    │  - Cannot recreate   │
                    └─────────────────────┘

Uninstallation Flow

┌─────────────────────────────────────────────────────────────────┐
│                    Plugin Uninstallation                         │
└─────────────────────────────────────────────────────────────────┘


                    ┌─────────────────────┐
                    │  Request uninstall   │
                    │  DELETE /plugins/    │
                    │  {slug}              │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Verify not builtin  │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Transaction:        │
                    │  1. Set PluginInstall│
                    │     .deletedAt       │
                    │  2. Disable all      │
                    │     instances        │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Clear cache         │
                    │  - plugins:installed │
                    │  - plugins:config:*  │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  Files preserved     │
                    │  (safety measure)    │
                    └─────────────────────┘

Configuration Validation Rules

Slug Validation

Format: ^[a-z][a-z0-9-]{0,30}[a-z0-9]$

Rules:

  • Length: 2-32 characters
  • Start: Must begin with a lowercase letter
  • Characters: Lowercase letters, numbers, hyphens only
  • End: Must end with letter or number (not hyphen)

Valid Examples:

  • stripe
  • payment-gateway
  • my-plugin-v2

Invalid Examples:

  • Stripe (uppercase)
  • -stripe (starts with hyphen)
  • stripe- (ends with hyphen)
  • a (too short)

Instance Key Validation

Format: ^[a-z0-9-]{1,32}$

Rules:

  • Length: 1-32 characters
  • Characters: Lowercase letters, numbers, hyphens only
  • Reserved: default cannot be used for custom instances

Valid Examples:

  • default (reserved)
  • production
  • stripe-us-west
  • test-1

Invalid Examples:

  • Production (uppercase)
  • stripe_us (underscore)
  • my-very-long-instance-key-name-exceeds-limit (too long)

Version Validation

Format: Strict semantic versioning (MAJOR.MINOR.PATCH)

Rules:

  • Must follow ^\d+\.\d+\.\d+$ pattern
  • No pre-release tags
  • No build metadata

Valid Examples:

  • 1.0.0
  • 2.3.15
  • 10.0.1

Invalid Examples:

  • 1.0 (missing patch)
  • 1.0.0-beta (pre-release tag)
  • v1.0.0 (prefix not allowed)

Instance Config Validation

Size Limits:

  • Maximum JSON size: 64KB (65,536 bytes)
  • Maximum nesting depth: 10 layers

Format Rules:

  • Must be a JSON object (not array or primitive)
  • null or undefined is allowed (means no config)

Example Valid Config:

{
  "apiKey": "pk_live_...",
  "webhookSecret": "whsec_...",
  "settings": {
    "currency": "USD",
    "captureMethod": "automatic",
    "metadata": {
      "source": "jiffoo-mall"
    }
  }
}

Validation Logic:

// 1. Check if object (not array)
if (typeof config !== 'object' || Array.isArray(config)) {
  throw Error('Config must be an object');
}

// 2. Check size (64KB limit)
const jsonStr = JSON.stringify(config);
const sizeBytes = Buffer.byteLength(jsonStr, 'utf-8');
if (sizeBytes > 65536) {
  throw Error('Config exceeds 64KB');
}

// 3. Check depth (10 layers max)
function getDepth(obj, currentDepth = 1) {
  if (typeof obj !== 'object' || obj === null) {
    return currentDepth;
  }
  const depths = Object.values(obj).map(val =>
    getDepth(val, currentDepth + 1)
  );
  return depths.length > 0 ? Math.max(...depths) : currentDepth;
}

if (getDepth(config) > 10) {
  throw Error('Config nesting exceeds 10 layers');
}

Security Boundaries

Installation Security

┌─────────────────────────────────────────────────────────────────┐
│                      Security Layers                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Layer 1: ZIP Security                                           │
│  ├─ Zip Slip Protection (path traversal prevention)             │
│  ├─ File Type Whitelist                                         │
│  │  └─ Allowed: .js, .json, .woff, .woff2, .css, .html         │
│  │  └─ Blocked: .exe, .sh, .bat, .dll, .so, etc.               │
│  ├─ Size Limits                                                 │
│  │  └─ Max font file: 2MB each                                 │
│  │  └─ Max total fonts: 20MB                                   │
│  │  └─ Max font count: 50 files                                │
│  └─ Idempotent Install (SHA-256 hash check)                    │
│                                                                   │
│  Layer 2: Manifest Validation                                   │
│  ├─ Schema version check (must be 1)                           │
│  ├─ Required fields validation                                 │
│  ├─ Format validation (slug, version, permissions)             │
│  └─ Runtime type validation                                    │
│                                                                   │
│  Layer 3: Database Integrity                                    │
│  ├─ Unique constraints (slug, pluginSlug+instanceKey)          │
│  ├─ Foreign key constraints (cascade delete)                   │
│  ├─ Soft delete (prevents accidental data loss)                │
│  └─ Non-reusable IDs (installationId uniqueness)               │
│                                                                   │
│  Layer 4: Runtime Isolation                                     │
│  ├─ Permission boundaries (granted vs declared)                │
│  ├─ Instance-level access control (enabled flag)               │
│  ├─ Config sandboxing (64KB + 10 layer limits)                 │
│  └─ Gateway routing isolation                                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Permission Model

Declared Permissions (in manifest.json):

{
  "permissions": [
    "database:read",
    "database:write",
    "api:external"
  ]
}

Granted Permissions (per instance):

{
  "grantedPermissions": [
    "database:read",
    "database:write"
  ]
}

Rules:

  1. Plugins declare needed permissions in manifest
  2. Instances can be granted a subset of declared permissions
  3. Gateway enforces granted permissions at runtime
  4. Permissions cannot exceed declared set

Soft Delete Model

Purpose: Prevent accidental data loss and maintain audit trail

Implementation:

  • Plugin Package: Setting deletedAt hides plugin from UI/gateway
  • Plugin Instance: Setting deletedAt prevents recreation
  • Files: Preserved on disk for safety (manual cleanup required)

Uninstallation Rules:

// 1. Cannot uninstall built-in plugins
if (plugin.source === 'builtin') {
  throw Error('Cannot uninstall built-in plugins');
}

// 2. Set deletedAt on package (transaction)
await prisma.$transaction([
  prisma.pluginInstall.update({
    where: { slug },
    data: { deletedAt: new Date() }
  }),
  // 3. Disable all instances
  prisma.pluginInstallation.updateMany({
    where: { pluginSlug: slug, deletedAt: null },
    data: { enabled: false }
  })
]);

// 4. Files remain at extensions/plugins/{slug}/
// (manual cleanup or re-install possible)

Gateway Routing

Request Flow

┌─────────────────────────────────────────────────────────────────┐
│                      Gateway Request Flow                        │
└─────────────────────────────────────────────────────────────────┘


            /api/extensions/plugin/{slug}/api/{path}


                    ┌─────────────────────┐
                    │  1. Parse slug       │
                    │     from URL         │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  2. Get instance     │
                    │     (default or ID)  │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  3. Check enabled    │
                    │     flag             │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  4. Verify plugin    │
                    │     exists on disk   │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  5. Inject headers   │
                    │     - X-Tenant-ID    │
                    │     - X-Installation-│
                    │       ID             │
                    │     - X-Platform-    │
                    │       Signature      │
                    └─────────────────────┘


                    ┌─────────────────────┐
                    │  6. Route to plugin  │
                    │     - internal: load │
                    │       module         │
                    │     - external: HTTP │
                    │       proxy          │
                    └─────────────────────┘

Instance Resolution

The gateway resolves instances in the following order:

  1. Explicit ID: If request includes X-Installation-ID header
  2. Default Instance: Falls back to instanceKey="default"
  3. Not Found: Returns 404 if no instance found or enabled

API Reference

Plugin Management

// Get plugin package
PluginManagementService.getPluginPackage(slug: string): Promise<PluginInstall | null>

// Get all installed packages
PluginManagementService.getAllPluginPackages(): Promise<PluginInstall[]>

// Uninstall plugin (soft delete)
PluginManagementService.uninstallPlugin(slug: string): Promise<void>

Instance Management

// Get instance by ID
PluginManagementService.getInstanceById(id: string): Promise<PluginInstallation | null>

// Get instance by key
PluginManagementService.getInstanceByKey(slug: string, key: string): Promise<PluginInstallation | null>

// Get default instance
PluginManagementService.getDefaultInstance(slug: string): Promise<PluginInstallation | null>

// Get all instances for plugin
PluginManagementService.getPluginInstances(slug: string): Promise<PluginInstallation[]>

// Create new instance
PluginManagementService.createInstance(
  slug: string,
  instanceKey: string,
  options?: {
    enabled?: boolean;
    config?: Record<string, unknown>;
    grantedPermissions?: string[];
  }
): Promise<PluginInstallation>

// Update instance
PluginManagementService.updateInstance(
  installationId: string,
  updates: {
    enabled?: boolean;
    config?: Record<string, unknown>;
    grantedPermissions?: string[];
  }
): Promise<PluginInstallation>

// Delete instance (soft delete, cannot delete "default")
PluginManagementService.deleteInstance(installationId: string): Promise<PluginInstallation>

Runtime Checks

// Check if instance is enabled
PluginManagementService.isPluginEnabled(
  slug: string,
  instanceKeyOrId: string
): Promise<boolean>

// Get instance configuration
PluginManagementService.getInstanceConfig(
  slug: string,
  instanceKeyOrId: string
): Promise<Record<string, unknown> | null>

Best Practices

For Plugin Developers

  1. Always validate config schema in your plugin code
  2. Handle missing config gracefully (use defaults)
  3. Never hardcode credentials (use instance config)
  4. Implement health checks (monitor instance status)
  5. Log instance-specific errors (include installationId)

For Platform Administrators

  1. Use meaningful instance keys (e.g., stripe-production, stripe-test)
  2. Disable unused instances instead of deleting (preserves audit trail)
  3. Test config changes in disabled instances before enabling
  4. Monitor plugin disk usage (soft-deleted files remain on disk)
  5. Review granted permissions regularly (principle of least privilege)

Multi-Instance Use Cases

Regional Configuration:

stripe + default       → US payments
stripe + stripe-eu     → EU payments (SCA compliance)
stripe + stripe-apac   → APAC payments (local methods)

Environment Isolation:

emailer + default      → Production SMTP
emailer + staging      → Staging SMTP
emailer + development  → Development (console logging)

A/B Testing:

analytics + default    → GA4 (primary)
analytics + mixpanel   → Mixpanel (secondary)
analytics + test       → Debug analytics (disabled)

On this page