Developer documentation · Plugins

Build a GPlayer plugin.

A plugin is a ZIP archive with a plugin.json manifest and a set of Node.js modules, HTML templates, and assets. Once installed from the administration, a plugin can serve its own public and admin pages, react to application events with hooks, inject widgets into admin screens, override built-in pages, run a background worker, and expose a configuration form — all executed in sandboxed worker threads with strict resource limits.

01 / Structure

Plugin anatomy

Everything a plugin does is declared in the manifest and implemented in plain files. A typical layout:

my-plugin/
  plugin.json              # manifest — required, at the ZIP root
  background.mjs           # optional long-running worker
  state.json               # runtime state, preserved on upgrade via keep_files
  views/
    frontend/
      index.html           # public page at /p/my-plugin/
      report.mjs           # public page at /p/my-plugin/report/
    backend/
      tools.mjs            # admin page at /administrator/p/my-plugin/tools/
  hooks/
    video-save.mjs         # handler for the video.save hook
  widgets/
    status.html            # widget injected into an admin slot
  assets/
    styles.css             # served at /plugins/my-plugin/assets/styles.css
    icon.svg

Node.js modules

Pages, hooks, and widgets can be .mjs, .js, or .cjs modules. They run in an isolated worker thread and must export a handler function. PHP files are rejected with a 501 response.

HTML templates

Pages and widgets can also be plain .html files. Placeholders such as {{plugin_name}} or {{config.my_key}} are interpolated server-side and HTML-escaped.

Static assets

Files in assets/ or public/ are served read-only at /plugins/<folder>/… with a sandboxing CSP.

02 / Contract

The manifest — plugin.json

The manifest sits at the root of the ZIP and declares identity, capabilities, and configuration. JSON keys use snake_case.

FieldTypeRequiredDescription
namestringyesDisplay name, 1–50 characters, no control characters.
folderstringyesInstall directory name, 1–50 chars, alphanumeric plus . _ -. Must not collide with protected directories.
versionstringyesVersion string, 1–100 chars. Re-installing the exact same version is rejected; a different version upgrades in place.
prioritynumbernoGlobal ordering between plugins (default 0).
icon_uristringnoIcon shown in the plugin list (max 2048 chars).
keep_filesstring[]noRelative paths preserved during upgrades — use it for state files (max 1000 entries).
backgroundstringnoPath to a background worker module (.js/.mjs/.cjs only).
configobjectnoDefault configuration values, merged with admin-saved values on upgrade.
config_fieldsarraynoSchema of the admin configuration form (see Configuration).
overridesobjectnofrontend and backend maps of page name → template file to replace built-in pages.
hooksobjectnoHook name → array of { "file", "priority" } handlers.
widgetsobjectnoSlot name → array of { "file", "priority", "admin_only" } widgets.
use_clibooleannoReserved flag; widens the list of protected folder names when true.
{
  "name": "Hello Analytics",
  "folder": "hello-analytics",
  "version": "1.0.0",
  "icon_uri": "/plugins/hello-analytics/assets/icon.svg",
  "keep_files": ["state.json"],
  "background": "background.mjs",
  "config": { "endpoint": "https://analytics.example.com/collect", "enabled": true },
  "hooks": {
    "video.save": [{ "file": "hooks/video-save.mjs", "priority": 0 }]
  },
  "widgets": {
    "backend.dashboard.bottom": [
      { "file": "widgets/status.html", "priority": 0, "admin_only": true }
    ]
  },
  "config_fields": [
    { "name": "endpoint", "label": "Collect endpoint", "type": "url", "required": true,
      "description": "HTTPS URL that receives events." },
    { "name": "enabled", "label": "Enable tracking", "type": "checkbox", "required": false }
  ]
}

03 / Shipping

Packaging & installation

Zip the plugin folder contents (with plugin.json at the archive root) and upload it from /administrator/plugins/list/.

Archive rules

Max 100 MB uncompressed, max 10,000 entries. No ZIP64, multi-disk, or encrypted archives. Symbolic links, path traversal, and case-insensitive duplicate names are rejected; every entry's CRC32 is verified.

Install & upgrade

A new plugin installs disabled — enable it from the list. Uploading the same name with a new version upgrades in place: files in keep_files survive, saved configuration is merged with the new defaults, and the database record keeps its id.

Load balancers

Secondary nodes synchronize plugins automatically from the main server through plugins/sync/ — a copy of the ZIP is kept server-side for that purpose.

04 / Surface

Plugin pages & overrides

Plugins serve their own pages, and can also replace built-in ones.

Custom pages

/p/<folder>/<page>/ · /administrator/p/<folder>/<page>/

Files resolve from views/frontend/ (public) or views/backend/ (admin, authenticated) trying <page>.mjs, .js, .cjs, .html, .htm in that order. GET and POST are supported; admin POSTs require the csrf token provided in the page input.

Page overrides

"overrides": { "frontend": { "index": "views/frontend/home.html" } }

When an enabled plugin overrides a page name, the matching application URL serves the plugin template instead. Login, logout, register, password-reset, error pages, and player routes can never be overridden. The highest priority wins.

Node.js page handler

Export render, default, or run. The handler receives one input object and returns a string or a response object:

// views/backend/tools.mjs
export function render ({ context, config, plugin }) {
  if (context.method === 'POST') {
    return { status: 303, body: '', headers: { location: `${context.baseUrl}${context.adminDirectory}/p/${plugin.folder}/tools/` } }
  }
  return {
    status: 200,
    contentType: 'text/html; charset=utf-8',       // or application/json, text/plain
    body: `<h1>${plugin.name}</h1><p>Endpoint: ${config.endpoint}</p>`,
    headers: { 'cache-control': 'private' }        // only cache-control and location are honored
  }
}

The input object contains context (method, path, page, query, body, user, baseUrl, adminDirectory, csrf), config (saved plugin configuration), and plugin (id, name, folder, version).

HTML template pages

<!-- views/frontend/index.html -->
<link rel="stylesheet" href="{{plugin_asset_base}}styles.css">
<h1>{{plugin_name}} v{{plugin_version}}</h1>
<p>Endpoint: {{config.endpoint}}</p>
<form method="post" action="{{base_url}}p/{{plugin_folder}}/save/">
  <input type="hidden" name="csrf" value="{{csrf}}">
</form>

Available placeholders: {{csrf}}, {{base_url}}, {{admin_directory}}, {{plugin_name}}, {{plugin_folder}}, {{plugin_version}}, {{plugin_asset_base}}, and {{config.<key>}}. All values are HTML-escaped.

05 / Events

Hooks

Hooks let a plugin react to application events. Handlers export handle, default, or run; the returned object is shallow-merged into the hook data and passed to the next handler (ordered by priority, then plugin rank). A failing handler is isolated and never blocks the request.

HookFired whenData
video.saveA video is created or updated in the adminThe saved video record
video.edit.loadThe video edit page loadsvideo_id, video_data
// hooks/video-save.mjs
export default async function ({ data, config, context, plugin }) {
  await fetch(config.endpoint, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ event: 'video.save', video: data, by: context.user?.id })
  })
  return { tracked: true }   // merged into the hook data
}

Each execution runs in its own worker thread with a 2-second timeout and a 2 MB output limit.

06 / Injection

Widgets

Widgets inject HTML into named slots of the admin screens. A widget is either an HTML template (interpolated like pages) or a Node.js module exporting render/default/run that returns a string or { html }. Set "admin_only": true to restrict a widget to administrator sessions.

// widgets/status.mjs
export function render ({ data, config }) {
  return { html: `<div class="plugin-widget">Slot ${data.slot} — endpoint ${config.endpoint}</div>` }
}

Available admin slots

07 / Files

Static assets

Files placed in the plugin's assets/ or public/ directories are served at /plugins/<folder>/assets/… and /plugins/<folder>/public/… with cache-control: public, max-age=300 and a sandboxing CSP. Supported types: css js mjs json svg png jpg jpeg gif webp ico woff woff2 txt html. Symlinks and path traversal are blocked.

08 / Settings

Configuration

Declare config_fields in the manifest and GPlayer renders a validated configuration form at /administrator/plugins/config/?id=…. Saved values are stored in the database and handed to every page, hook, widget, and template as config.

Field typeValidation
text / textareaString, max 100 KB, required check.
urlHTTP or HTTPS only, credentials in the URL rejected.
numberFinite number, optional minimum / maximum bounds.
passwordStored as string; an empty submission keeps the previous secret.
checkboxStored as boolean.
selectValue must match one of the declared options ({ "value", "label" }).

Each field takes name, label, type, required, an optional description, and for selects an options array. Config keys not declared as fields are preserved untouched when the admin saves the form.

09 / Daemon

Background worker

Point background at a module exporting default or run. The worker starts when the plugin is enabled, restarts automatically when its file content changes, and stops on disable or uninstall.

// background.mjs
import { readFile, writeFile } from 'node:fs/promises'

export default async function run ({ pluginDirectory }) {
  const statePath = `${pluginDirectory}/state.json`   // listed in keep_files
  let state = { ticks: 0 }
  try { state = JSON.parse(await readFile(statePath, 'utf8')) } catch {}
  for (;;) {
    await new Promise((resolve) => setTimeout(resolve, 60_000))
    state.ticks += 1
    await writeFile(statePath, JSON.stringify(state))
  }
}

Workers run with capped resources (32 MB old-generation heap, 16 MB young generation, 4 MB stack). Persist state under the plugin directory and list those files in keep_files so upgrades don't erase them.

10 / Guardrails

Security model & limits

Plugin code never runs in the main process.

Sandboxed execution

Every page, hook, and widget invocation runs in a fresh worker thread: 2 s timeout, 2 MB output, 32/16 MB heap, 4 MB stack. One failure never affects other plugins or the request.

Request protection

Admin plugin pages require an authenticated session; POSTs require same-origin plus a per-plugin CSRF token. Public plugin pages still enforce same-origin on POST.

Content security policy

Plugin pages get a strict CSP (default-src 'none', self-hosted scripts/styles only); assets are served with sandbox and same-origin resource policy. Ship your CSS/JS as plugin assets rather than inline.

11 / Recap

Minimal working plugin

Three files are enough for a public page with styling:

# plugin.json
{
  "name": "Hello World",
  "folder": "hello-world",
  "version": "1.0.0"
}

# views/frontend/index.html
<link rel="stylesheet" href="{{plugin_asset_base}}styles.css">
<h1>Hello from {{plugin_name}}!</h1>

# assets/styles.css
h1 { font-family: sans-serif; }
  1. Zip the three files with plugin.json at the archive root.
  2. Upload the ZIP on /administrator/plugins/list/.
  3. Enable the plugin from the list.
  4. Open /p/hello-world/ — your page is live.

← Back to the application guide