Build your first plugin
Two tracks share the same registries. First-party plugins live in this
repo under libs/plugins/* and load through the generated manifests;
marketplace plugins are standalone bundles published to the marketplace.
This walkthrough does marketplace end-to-end and notes the first-party
deltas.
1. Scaffold
# Marketplace: copy the standalone starter out of the repo
cp -r tools/plugin-loader/realm/template my-plugin && cd my-plugin
npm install
# First-party (in-repo) instead:
node tools/scripts/create-plugin.mjs my-plugin \
--label "My Plugin" --surfaces console,tenantApi
The marketplace starter gives you src/index.js (the entry contract),
manifest.json, a build config that compiles your react /
@aglyn/aglyn imports into host-ABI lookups, and a README that becomes
your marketplace listing docs. The first-party scaffolder generates the
whole Nx library, wires plugins.config.json, and prints the two manual
follow-ups (catalog entry + release flag).
2. Write the entry
import { createElement } from 'react'
import { registerConsoleExtension } from '@aglyn/aglyn'
export function register(host) {
registerConsoleExtension({
pluginId: 'my-plugin', // must match manifest.json id
displayName: 'My plugin',
widgets: [{
slot: 'hostActivity',
widgetId: 'my-plugin-hello',
Component: () => createElement('div', null, 'Hello!'),
}],
})
}
Every registry in the API reference
is available on host.aglyn; the zones catalog
lists where widgets can render.
3. Develop against a live workspace
npm run watch # rebuilds dist/plugin.bundle.mjs on save
npx serve dist --cors # any localhost static server works
Start the console/tenant dev server with the dev-loop variable and refresh after each rebuild:
NEXT_PUBLIC_PLUGIN_DEV_BUNDLES="my-plugin=http://localhost:3000/plugin.bundle.mjs" \
npx nx serve console
The bundle loads unverified (localhost only; the whole path is
compiled out of production builds). The browser console logs
dev realm bundle loaded (UNVERIFIED): my-plugin. If a change doesn't
stick, clear the Next dev cache (rm -rf apps/console/.next/cache) —
inlined env values live in it.
4. Verify
node tools/scripts/verify-plugin-bundle.mjs dist/plugin.bundle.mjs manifest.json
The same checks the publish API enforces, run over the bundle's parse tree:
entry exports, no leftover static imports, no eval/Function/computed
access on a global, no import() it cannot resolve, size — and every
network call diffed against your manifest's capabilities.network, which
is why the manifest goes in too. It lists every area it checked, so you can
tell a pass from a check that never ran, and prints the bundle's sha256 —
the content pin every install verifies.
5. Publish
Publish from Marketplace → Publish (choose A plugin (upload a bundle)) or the
API — the
bundle plus manifest.json fields and your listing content (README,
logo, screenshots, links, license, categories — see the
publisher handbook). New listings
enter the review queue as submitted; staff list (or verify ✅) them
before they appear in browse. Publishing requires a marketplace profile, a
Pro plan, and payouts onboarding for paid listings; there's a daily
publish cap.
6. Install, enable, load
A workspace admin installs from the marketplace: the install pins your
exact {version, sha256}, enables the plugin on the org switchboard, and
it loads on the next visit — sandboxed by default, or into the app realm
once staff have signed the version (trust: 'realm'). Upgrades are
explicit re-pins; your published artifacts are immutable, so nobody's
pinned install can change underneath them.
7. Uninstall
Marketplace → Installed → Uninstall, or the Uninstall button on the listing's own detail page (Uninstall org-wide for an org-scope install), removes the pin and the switchboard entry. Data your plugin created stays untouched — reinstalls pick up where they left off. Design for that: namespace your documents and tolerate finding old ones.
Troubleshooting
- Nothing loads: check the browser console for the loader's reason —
sha mismatch (stale artifact URL), missing/invalid signature (not
realm-signed yet — expected for sandbox-tier), host ABI mismatch
(rebuild against the current template), or the plugin isn't in
org.enabledPlugins. - Blank widgets / hook errors: your bundle carries its own React.
Realm bundles must import
reactonly through the template's build config (host-ABI lookups) — run the verifier; leftover static imports fail it. - Publish 422: the response lists exactly which static check failed.
- Works in dev, not after publish: the dev loop skips verification; the published path doesn't. Run the verifier locally first.