Plugin Development
Practical guide for building new plugins on the current production plugin runtime
Plugin Development (Current Runtime)
This guide is for building new plugins on top of the current, already-implemented plugin runtime.
The platform runtime contract is stable in these areas:
- ZIP install and manifest validation
- runtime routing through
/api/v1/shop/plugins/{slug}/api/* - multi-instance routing via
installationIdorinstanceKey - lifecycle hook callbacks
- shared manifest contract between platform and SDK
Use this guide as the practical implementation reference.
1. Minimal Manifest Templates
All plugins use manifest.json with schemaVersion: 1.
External HTTP plugin (recommended)
{
"schemaVersion": 1,
"slug": "my-payment-plugin",
"name": "My Payment Plugin",
"version": "1.0.0",
"description": "A custom payment integration",
"runtimeType": "external-http",
"externalBaseUrl": "https://my-payment-plugin.example.com",
"permissions": ["orders:read", "orders:write"],
"category": "payment",
"capabilities": ["payment.process", "payment.refund"],
"lifecycle": {
"onInstall": true,
"onEnable": true,
"onDisable": true,
"onUninstall": true,
"onUpgrade": true
},
"configSchema": {
"apiKey": {
"type": "string",
"label": "API Key",
"required": true,
"secret": true
}
}
}Internal Fastify plugin (platform-maintained)
{
"schemaVersion": 1,
"slug": "my-internal-plugin",
"name": "My Internal Plugin",
"version": "1.0.0",
"description": "Internal plugin running in isolated Fastify runtime",
"runtimeType": "internal-fastify",
"entryModule": "dist/index.js",
"permissions": [],
"category": "integration",
"capabilities": ["api.read"]
}Required validation rules
slugmust match^[a-z][a-z0-9-]{0,30}[a-z0-9]$versionandsdkVersionmust be strict semver (MAJOR.MINOR.PATCH)runtimeType="external-http"requiresexternalBaseUrlruntimeType="internal-fastify"requiresentryModulethemeExtensions.embeds[].targetPositionmust behead-endorbody-end
2. Gateway Contract (Must Follow)
Your plugin is called through platform gateway, not directly by frontend clients.
- Gateway path:
/api/v1/shop/plugins/{slug}/api/* - Instance selection:
?installation=default(byinstanceKey)?installationId=<id>(by instance id)- default fallback:
installation=default
- Health probe path from platform:
/api/v1/shop/plugins/{slug}/health
Platform injects context headers before forwarding:
x-plugin-slugx-installation-idx-installation-keyx-platform-idx-user-id(empty string for anonymous, NOT "anonymous")x-user-role(guest/customer/admin)x-request-idx-caller(shop/admin/theme-app/api-internal/unknown)x-platform-signaturex-platform-timestampx-platform-versionx-platform-api-base-urlx-localex-plugin-config(Base64URL-encoded JSON of instance config)
Important:
- Do not trust incoming spoofed context headers from clients
- For external plugins, verify
x-platform-signature+x-platform-timestamp - Treat
installationIdas the primary tenant boundary in plugin logic
3. Lifecycle Implementation
Lifecycle declaration lives in manifest.json under lifecycle.
Supported hooks:
onInstallonEnableonDisableonUninstallonUpgrade
For external-http, expose lifecycle endpoint:
POST /__lifecycle/:hookName
Request body shape:
{
"installationId": "...",
"pluginSlug": "my-plugin",
"instanceKey": "default",
"config": {}
}Suggested behavior:
- Keep hooks idempotent
- Keep hooks short and retry-safe
- Never assume default instance only; always use
installationId
4. Config and Multi-Instance Development Rules
The platform creates a default instance (instanceKey=default) when plugin ZIP install succeeds.
- Default instance may start disabled if required config is missing
- Additional instances can be created with different
instanceKey - Every runtime request is resolved to one concrete
installationId
Your plugin code should:
- read config per request/instance (not global singleton config)
- isolate data by
installationId - include
installationIdin logs and error reports - treat
instanceKeyas human-readable label, not security boundary
5. Local Development and Packaging
Install SDK:
npm install jiffoo-plugin-sdkScaffold a new project:
npx jiffoo-plugin init my-pluginValidate/build/package:
npx jiffoo-plugin validate
npx jiffoo-plugin build --output dist
npx jiffoo-plugin pack --output my-plugin.zipUseful pack flags:
--no-build--no-validate--include-source
6. Minimal Implementation Example (external-http)
import express from 'express';
import {
createContextMiddleware,
createSignatureMiddleware,
defineLifecycleHooks,
definePlugin,
} from 'jiffoo-plugin-sdk';
const app = express();
app.use(express.json());
const plugin = definePlugin({
schemaVersion: 1,
slug: 'my-payment-plugin',
name: 'My Payment Plugin',
version: '1.0.0',
description: 'A custom payment integration',
runtimeType: 'external-http',
externalBaseUrl: process.env.PLUGIN_BASE_URL || 'http://127.0.0.1:4100',
permissions: [],
category: 'payment',
capabilities: ['payment.process'],
lifecycle: { onInstall: true, onEnable: true, onDisable: true },
});
app.get('/health', (_req, res) => {
res.json({ status: 'ok', slug: plugin.getManifest().slug, version: plugin.getManifest().version });
});
app.use('/api', createContextMiddleware());
app.use('/api', createSignatureMiddleware(process.env.PLUGIN_SECRET || 'dev-secret'));
app.post('/api/process-payment', async (req, res) => {
const ctx = (req as any).pluginContext;
res.json({
success: true,
installationId: ctx?.installationId,
requestId: ctx?.requestId,
});
});
const lifecycle = defineLifecycleHooks({
onInstall: async (ctx) => {
console.log('onInstall', ctx.installationId);
},
onEnable: async (ctx) => {
console.log('onEnable', ctx.installationId);
},
onDisable: async (ctx) => {
console.log('onDisable', ctx.installationId);
},
});
app.post('/__lifecycle/:hookName', lifecycle.httpHandler());
const port = Number(process.env.PORT || 4100);
app.listen(port, () => {
console.log(`Plugin listening on :${port}`);
});7. Debug and Acceptance Checklist
Before uploading ZIP to Admin:
manifest.jsonvalidates successfullyLICENSEexistshealthendpoint returns 200- lifecycle endpoint is reachable for declared hooks
- signature verification is enabled for gateway endpoints
- logs include
installationIdandrequestId
After installation in Admin:
- plugin package appears in installed list
- default instance is created
- plugin gateway call succeeds:
GET /api/v1/shop/plugins/{slug}/api/...?...
- disable instance returns gateway soft block (404)
- re-enable instance resumes successful routing
8. Internal Fastify Appendix (Platform-Maintained)
Use internal-fastify only when plugin code is trusted and packaged with a runtime entry that can be loaded by the API runtime.
Runtime expectations
manifest.json.runtimeTypemust beinternal-fastifymanifest.json.entryModulemust point to a built JS file inside the plugin package- the entry module must export a Fastify plugin function (default export or module export)
- the plugin receives instance config as registration options
Minimal internal plugin entry
// dist/index.js
async function myInternalPlugin(fastify, options) {
fastify.get('/api/ping', async () => {
return {
ok: true,
source: 'internal-fastify',
installationId: options.installationId || null,
};
});
fastify.get('/health', async () => {
return { status: 'ok' };
});
}
export default myInternalPlugin;Internal plugin packaging rules
- always upload built artifacts, not TypeScript source only
- keep
entryModulestable across versions when possible - keep startup fast; slow
register/readycan disable instance on warm failure - keep implementation stateless per request; do not cache mutable global instance config
9. Concept Map: Business Plugins vs Themes vs Runtime Types
This section resolves the most common confusion.
A. Extension kinds (installer-level category)
The installer supports these extension kinds:
plugintheme-shoptheme-admintheme-app-shoptheme-app-adminbundle
Think of this as the package family selected at install time (POST /api/v1/admin/extensions/:kind/install).
B. Business plugin runtime types (plugin-internal mode)
Inside the plugin extension kind, there are two runtime modes:
external-httpinternal-fastify
So:
pluginis an extension kindexternal-http/internal-fastifyare runtime types inside that kind
C. Themes are separate from business plugins
- Theme Pack (
theme-shop,theme-admin) customizes storefront/admin presentation using static resources - Theme App (
theme-app-shop,theme-app-admin) is an executable storefront/admin runtime - Neither Theme Pack nor Theme App is the same concept as business plugin runtime types
D. What is themeExtensions in plugin manifest?
themeExtensions does not turn a business plugin into a theme.
It means a business plugin can publish controlled UI extension points (app blocks / app embeds) that Theme runtime can render.
In short:
- Theme owns page rendering model
- Business plugin owns business capability/API
themeExtensionsis the bridge from business plugin into theme-rendered UI slots
10. Should We Split Docs by Plugin Type?
No. Keep one primary guide and split by sections.
- Most teams should follow
external-httpsections only - Platform maintainers can use the internal-fastify appendix
- Both paths share the same manifest contract, gateway path, lifecycle model, and multi-instance rules