Skip to docs content
Understanding Plugin Architecture
Open Source
Free Cloud
Pro
SDK current
Updated 2026-09-04

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:

  1. Init
  2. Migration
  3. SchemaRegister
  4. RESTApiRegister
  5. Execute

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 optional ui.js.
  • Git tag, config.yml plugin.version, and sdk.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.

Next: Exploring Plugins and Marketplace.