Skip to content

What a plugin is

TrakState ships with no fixed data model, and with no fixed list of features either. Much of what the product can do comes from plugins — the vendor’s own packages travel the same seam yours does.

A plugin is a signed ZIP archive with a manifest.json at its root. Inside it, up to three things:

Part What it is When you need it
Frontend A Module Federation remote the shell loads at runtime Only if your plugin has screens of its own
Backend Code on the server — one TypeScript module, or a process of your own Only if something has to happen server-side
Declarations Contributions: routes, commands, settings, page types, connectors Almost always

A connector plugin is backend only. It ships no frontend and no stylesheet: the settings form is generated from what the connector type declares. If you find yourself writing UI for a connector, something has gone wrong.

A plugin is built against this package and nothing else — never against the host’s source. What is exported there is what the host promises to keep working. What is not exported is not part of the contract, however convenient it looks from the outside.

import { parseManifest, type PluginManifest, type Plugin, type PluginHost } from '@prodlog/sdk';

The SDK carries four groups of things:

  • Manifest — schema, types, and the checking functions parseManifest / parseUploadedManifest.
  • Runtime — Plugin, PluginHost, CapabilityDefinition, InvocationContext, CapabilityConflictError.
  • Frontend — ShellContext, Surface, ScopedStore, PageInstance and the page-type contract.
  • Connectors and configuration — ConnectorType, connectorContribution, and the configuration document behind a tenant template.

This is not politeness. It is the load-bearing boundary of the system.

  • No database connection. No handle, no credentials. Every query goes through the mediated host surface and therefore runs under the call’s tenant context (row level security).
  • No reach into another plugin’s data. Tables are pinned to the plugin’s namespace, and the host checks that before loading.
  • No filesystem outside its own working directory.
  • No environment variables beyond a short allowlist (TZ, LANG, LC_ALL, NODE_ENV). DATABASE_URL and anything secret are structurally out of reach.
  • No tenant switching. The invocation context carries no field with which a plugin could name a different tenant at all.

If something you need is missing, that is a conversation about the contract — not a gap to route around.

Two rights axes, and they are not the same thing

Section titled “Two rights axes, and they are not the same thing”

Mixing these up is the most common mistake in a first package.

  • permissions says what your code may reach on the host APIs: data:read, data:write, plugin:manage, capability:invoke, audit:read, net:request, net:socket. An operator reads this list on the install screen.
  • rbacPermissions says what a person may do, inside your own namespace — for example acme.tcp:configure.

Both gates must hold. A missing data:write ends in permission_denied even when the user’s role allows everything — and the other way round.

The SDK version and the host version move in lockstep. Your manifest names the range you built against:

"sdk": "^0.9.0"

A host provides its own contract version plus every older one it still fulfils completely. Host 0.9.0 therefore provides 0.9.0, 0.8.0, 0.7.0 and 0.6.0; a package on ^0.6.0 keeps running there unchanged. That is a maintained table, not a rule about 0.x versions: 0.6.0 was itself a breaking release. Read what changed before you jump a minor.

If the range does not fit, the package is refused with sdk_incompatible and never loads.

From an empty directory to an installed package

Section titled “From an empty directory to an installed package”
  1. Write the manifest and check it with your own compiler using parseManifest.
  2. Choose a backend kind — in-process or sidecar.
  3. Build and pack — deterministically, or the digest moves on every build.
  4. Sign — Ed25519 over the archive bytes.
  5. Install — upload it, or take it from the operator’s catalogue.
  6. Enable it — installing is not enabling. A backend starts only when it is switched on.

Each of these steps has its own topic further down this chapter.

Four real packages live in the product workspace, and between them they cover every build shape:

Package What it is the pattern for
plugins/tcp Sidecar connector: Python backend, a held socket, a connector type as data, its own pack script
plugins/tenant-user Eleven capabilities, both rights axes, a system plugin with no tables of its own
plugins/board A page type, an in-process backend, own tables built from the manifest description

@prodlog/sdk is not on a public registry yet. npm install @prodlog/sdk does not work from outside; today a plugin is built inside the product workspace. This guide moves with that as soon as it changes.