Understanding Plugin Architecture
Editions: Open Source · Free Cloud · Pro
There is one install class: system plugin. A project never “installs” a plugin. Super-admins install a signed zip on the Engine host. Project admins activate that already-installed plugin and set per-project env_vars. Do not bring back type: project.
Operator/consumer docs (install vs activate, trust, catalog links) live in Plugins. This section is the author track.
Two layers
SignedZip → catalog-v1 → EngineHost → HashiCorpSubprocess → DeclaredCapabilities
ProjectActivation → DeclaredCapabilities → plg_* / ext_* GraphQL
→ Plugin REST
→ Console routes / fields / settings
| Layer | Who | What |
|---|---|---|
| Host install | Super-admin | Binary + config.yml (+ optional ui.js) under PLUGIN_PATH/<id>/. Catalog, checksum, handshake, health. |
| Project activation | Project admin | Enable the plugin on one project. Secrets and config live on that project. |
Any of {project.graphql, project.rest, console.routes, console.settings, content.fields} is activation-gated. system.graphql, system.rest, and system.events run host-wide without activation.
HashiCorp subprocess
Engine launches the plugin as a go-plugin child. Handshake v1 is fixed:
| Field | Value |
|---|---|
| Protocol | 1 |
| Magic cookie key | APITO_PLUGIN |
| Magic cookie value | apito_plugin_magic_cookie_v1 |
The host exposes five RPCs only. Do not add methods for install vs activate:
InitMigrationSchemaRegisterRESTApiRegisterExecute
Activation is project metadata checked at schema-build and execute time. It is not a sixth RPC.
Identity and files
- Plugin IDs use the
hc-prefix (hc-cloudinary-plugin). - Language today is Go (HashiCorp). JS hello-world exists as an SDK sample.
- On disk:
PLUGIN_PATH/<id>/contains the binary (binary_path),config.yml, and optionalui.js. - Git tag,
config.ymlplugin.version, andsdk.Init(..., version, ...)must match.
Public naming
| Surface | Prefix / path | When |
|---|---|---|
| Project GraphQL | plg_* |
project.graphql + activation |
| System GraphQL | ext_* |
system.graphql (host-wide) |
| Project REST | /<pluginId><path> |
project.rest + activation. Console SDK calls /plugin/<pluginId><path> |
| System REST | unprefixed host routes | system.rest |
SchemaRegister merges live RPC names with handwritten contributions.api in config.yml. The YAML overlay documents queries, mutations, and REST; the Go SDK registers the actual resolvers.
Console UI trust
Console loads signed official ui.js from Engine:
GET /system/plugin/:id/ui.js plus SHA-256 (bundle_sha256 from GET /system/plugin/manifest).
Load only when all are true: official, signed, project activated, checksum matches. Third-party plugins get host-rendered settings forms. No arbitrary third-party JavaScript in Console.
Companions: Plugin capabilities · Plugin security.
config.yml skeleton
plugin:
id: "hc-example-plugin" # must match directory + zip name
language: "go"
title: "Example"
description: "What this plugin does"
capabilities: # allowlist — see table below
- project.graphql
- project.rest
- console.routes
- console.settings
- content.fields
enable: true
version: "0.0.1" # == git tag without v == sdk.Init version
author: "You"
repository_url: "https://github.com/you/hc-example-plugin"
binary_path: "hc-example-plugin"
handshake_config:
protocol_version: 1
magic_cookie_key: "APITO_PLUGIN"
magic_cookie_value: "apito_plugin_magic_cookie_v1"
ui_config:
entry_path: ui.js
official: true # false → no compiled Console JS
signed: true
publisher: You
contributions:
api:
scope: project
queries:
- name: exampleList
description: List example records
mutations:
- name: exampleMutate
description: Mutate an example record
rest:
- method: GET
path: /items
description: List items
ui:
ui_available: true
routes:
- path: /
title: Example
navigation:
- after: storage # content, database, model, users, storage,
label: Example # graphql, rest, auth, logic, plugins, settings
path: /console/plugin/hc-example-plugin
settings:
- path: /console/settings/plugins/hc-example-plugin
label: Example
fields:
- id: example_asset
label: Example Asset
icon: image
storage_type: media # core storage_type + plugin_id — never a new FIELD_TYPE_ENUM
form_component: ExamplePicker
display_component: ExamplePreview
content_form: true
env_vars:
- key: "EXAMPLE_API_KEY"
value: ""
Production example: Writing Your First Plugin walks hc-cloudinary-plugin.
Capability catalog — how much you can do
| Capability | Scope | Activation | What authors get |
|---|---|---|---|
system.graphql |
Host-wide | No | System GraphQL fields as ext_* |
system.rest |
Host-wide | No | Unprefixed REST on the Engine |
project.graphql |
Per project | Yes | Project GraphQL as plg_* |
project.rest |
Per project | Yes | REST at /<pluginId><path> |
console.routes |
Per project | Yes | Plugin pages + sidebar from contributions.ui |
console.settings |
Per project | Yes | Project Settings plugin config + env_vars |
content.fields |
Per project | Yes | Model field types (storage_type + plugin_id, never a new FIELD_TYPE_ENUM) |
system.events |
Host-wide | No | Event sink (Discord-style notifications) |
That is the ceiling. There is no extra RPC, no new GraphQL root, and no unsigned Console JS path for third parties.