Plugin and Service Model
Zelavis currently benefits from using two names for two different layers:
servicefor the internal runtime-mounting unitpluginfor the public extension concept developers and operators interact with
Current recommendation
Section titled “Current recommendation”Use this language split consistently:
- Services are the internal transport/runtime primitive.
- Plugins are the external product concept.
That means the dashboard, docs, and developer-facing product language can talk about plugins without forcing the runtime internals to stop using the service contract that already exists.
Are plugins services under the hood?
Section titled “Are plugins services under the hood?”Sometimes yes, but not always in the same way.
- Auth provider plugins extend the auth API surface through plugin registration.
- Ecommerce provider plugins extend the ecommerce API surface through plugin registration.
- Installable dashboard/runtime features may eventually expose one runtime-mounted service as part of how the plugin is activated.
That lower-level provider layer benefits from explicit extension points.
Current example:
zelavis-ecommerceis the top-level marketplace/runtime plugin- payment providers such as Stripe or PayPal are child plugins targeting the ecommerce
paymentsextension point
So the safe model is:
A plugin may register or compose one or more internal services, but the plugin itself is the user-facing extension unit.
Why this split is good
Section titled “Why this split is good”- The runtime already has a clear internal service contract.
- Plugin is the more intuitive outside-world term for installable capabilities.
- Operators think in terms of installing, enabling, and using plugins, not mounting service graphs.
- This avoids leaking internal transport language into the UX.
Dashboard rule
Section titled “Dashboard rule”The dashboard should reflect that split:
Marketplaceis a top-level discovery/install area.- Installed plugins do not get first-slide root items.
- Each installed plugin gets exactly one root entry under
Workspace. - Each plugin may own unlimited nested sidebar slides inside its own workspace area.
- Plugin-owned dashboard navigation should be declared through a plain menu object such as
menu: { ... }, not by reaching into sidebar internals directly. - A plugin menu item may declare
page: { id, title, render }when that menu item owns dashboard content. - Core services may declare a service-only menu
surfacesuch asroot,core,workspace, orsettings. - Plugin menus must not declare a
surface; Zelavis always mounts them underWorkspace.
This keeps the first slide stable and prevents dashboard sprawl.
Plugin pages are served as full HTML documents and mounted by the dashboard inside the zelavis-plugin-frame iframe web component. That lets plugin authors use plain HTML, React, Vue, web components, or any other browser-side approach without coupling plugin settings or workspaces to the internal dashboard React tree. Zelavis serializes those page definitions to dashboard-safe URLs such as /zelavis/api/v1/dashboard/plugin-pages/:plugin/:page.
TypeScript direction
Section titled “TypeScript direction”A good current TypeScript direction is:
- keep
defineService(...)for the internal runtime contract - expose one shared public plugin builder:
definePlugin(...) - version that contract explicitly with
ZELAVIS_PLUGIN_V1 - let plugin definitions carry declarative dashboard metadata such as
menu: { ... }so plugins are not locked to one dashboard implementation detail - let child plugins declare
extends: { plugin, extensionPoint }instead of inventing package-local plugin builders
That builder should be about developer ergonomics and metadata, not about replacing the internal service contract.
Suggested plugin shape
Section titled “Suggested plugin shape”The current DX direction should lean declarative:
import { definePlugin, ZELAVIS_PLUGIN_V1 } from "zelavis";
definePlugin({ name: "zelavis-ecommerce", contractVersion: ZELAVIS_PLUGIN_V1, extensionPoints: [ { name: "payments", policy: "reviewed", allowedPlugins: ["stripe", "paypal"], }, ], menu: { title: "Ecommerce", path: "/commerce", page: { id: "dashboard", title: "Commerce", render() { return { html: "<!doctype html><html><body>Commerce</body></html>", }; }, }, items: [ { title: "Orders", path: "/commerce/orders", page: { id: "orders", title: "Orders", render() { return "<!doctype html><html><body>Orders</body></html>"; }, }, }, { title: "More", items: [ { title: "Customers", path: "/commerce/customers", }, ], }, ], }, setup(plugin) { // register services, routes, providers, and plugin capabilities },});Child plugins use the same builder but target a parent extension point:
import { definePlugin } from "zelavis/plugin";import type { EcommerceApi } from "@zelavis/ecommerce";
definePlugin<EcommerceApi>({ name: "stripe", extends: { plugin: "zelavis-ecommerce", extensionPoint: "payments", }, setup(api) { api.payments.registerProvider("stripe", provider); },});Installed child plugins are collected for their parent. They do not activate as independent top-level workspace plugins.
Parent plugins own their extension-point policy:
openaccepts any installed child plugin targeting that extension pointreviewedaccepts only child plugins listed by the parentprivateis the default and also accepts only listed child plugins
For ecommerce payments, the current policy is reviewed, with Stripe and PayPal allowed by the official ecommerce package. A future XYZ Payments child plugin would need the parent plugin to add it to that list before activation.
Important point:
- the
menuobject is plugin-owned metadata menu.pageis the content contract for plugin-owned dashboard pages- Zelavis decides how to render that metadata in the current dashboard shell
- if the dashboard changes later, the plugin contract can stay stable while Zelavis adapts the rendering layer
Registry direction
Section titled “Registry direction”Plugins should also be representable through one neutral registry shape:
createPluginRegistry([ { plugin: ecommercePlugin, status: "installed", source: "official", },]);That gives Marketplace, installed plugin navigation, and future runtime activation a shared model instead of separate ad hoc lists.
ESM loading direction
Section titled “ESM loading direction”Zelavis should keep plugin loading 100% inside standard JavaScript and ESM:
- plugin modules should be loaded through dynamic
import() - plugin modules may export a plugin definition directly, as
plugin, or asdefault - loading and registry updates should stay runtime-neutral and avoid Node-only APIs
That means a future runtime can do things like:
const plugin = await loadPlugin("zelavis-ecommerce");const registry = await loadPluginRegistry([ { specifier: "zelavis-ecommerce", status: "installed", source: "official" },]);Important boundary:
- load/install can be modeled with ESM imports and registry state
- remove/uninstall should mean removing the plugin from Zelavis registry/config state
- JavaScript does not offer a standard way to unload an already-imported ESM module from memory, so Zelavis should not pretend otherwise
Install State And Activation
Section titled “Install State And Activation”The runtime model should also stay explicit:
- plugin catalog metadata lives in Marketplace catalog entries
- plugin runtime metadata lives in plugin registry entries
- plugin install state lives in a registry store
- plugin activation order is an explicit
ordernumber - plugin setup runs in registry order and can register additional services through the setup context
- plugin activation is adapter-owned: Node-style adapters can recompose the local runtime graph, while serverless adapters can attach a worker/function boundary or another live host capability
That means install state and activation are related, but not the same thing:
- a plugin can exist in the catalog as
available - a plugin becomes active only when its registry state is
installed - setup should run only for installed plugins once the host has activated the plugin module
The current runtime direction now reflects that split with:
createPluginRegistry(...)definePluginCatalogEntry(...)definePluginCatalog(...)applyPluginRegistryState(...)activatePluginRegistry(...)- runtime plugin registry stores for memory, database, key/value, and file storage
- parent-owned extension point policies for child plugins
- plugin setup context carrying only standard data such as root path, API paths, platform summary, and collected services
- a plugin activation controller with declared host capabilities:
strategy:runtime-graph,worker-boundary,function-boundary, orcustomsupportsRuntimeInstallsupportsUploadedSpecifierssupportsIsolatedExecution
The dashboard exposes these capabilities so users can tell whether a host can truly apply uploaded or marketplace plugins at runtime. Core does not assume a filesystem, a deploy API, or a provider-specific worker model.
For Node or Bun adapters, the likely production shape is a local plugin cache such as .zelavis/plugins/<plugin>/<version>/index.js, plus a registry entry that points to that ESM entry point. Zelavis can then dynamic-import the specifier and recompose the runtime graph without restarting the process.
For serverless adapters, the correct shape is provider-specific. A Cloudflare adapter cannot mutate the already-running Worker in place. The plausible runtime-install model is a worker boundary: upload plugin code as a user Worker, route through a dispatch namespace or service binding, and pass Zelavis context through standard request/binding contracts. That keeps Zelavis core portable while letting the Cloudflare adapter implement the Cloudflare-specific deployment and isolation mechanics.
That platform summary should stay intentionally small:
presets: platform preset names such asnodeorcloudflareresources: booleans for resource availability such as key/value or file storagemetadata: plain serializable platform hints
That gives plugins useful context without leaking host-specific APIs into the plugin contract.
Current design preference
Section titled “Current design preference”For product language:
- prefer Plugin over Service
For runtime internals:
- keep Service
For possible alternatives:
- Apps feels more end-user/productized, but less precise for provider-style extensions
- Extensions is good, but broader and slightly less concrete than Plugin
- Plugins is the clearest choice for the current Zelavis direction