Building a plugin
For developers. A plugin adds a work area to the desk through its extension points; this page describes them, the manifest, the rules a plugin must follow, and the APIs of the plugins that come with the desk.
| Part | What a plugin can add |
|---|---|
| The app | Pages, its item in the sidebar and its page in Settings, a tab on Reports. |
| The case | Sections and actions on the case, actions on the inbox's selected cases, timeline entries. |
| The server | API routes, handlers on case events, events for webhooks, audit log entries. |
| Integrations | Permissions for tokens, and tools for AI assistants. |
Admins turn plugins on and off under Settings → Plugins (admin guide). The Software
Development plugin in plugins/dev/ and the Incident Management plugin in plugins/incidents/ are complete examples.
The page in order: Layout and the manifest; the server side (routes, access, extension points, events, jobs, permissions, MCP tools, tables, tests); the web side (extension points, the side panel, slots); then the plugin API and the API of each plugin that comes with the desk.
[!IMPORTANT] Plugins are compiled with the desk, not loaded at runtime: a desk runs exactly the plugins its image was built with.
To add your own:
- Put it in
plugins/<id>/. - List it in
plugins/plugins.config.ts. - Build the image.
Layout
plugins/
plugins.config.ts the plugins this desk is built with: ['dev', …]
dev/
package.json a workspace package (@desk/plugin-dev) with its dependencies
manifest.ts id, name, permissions …
shared.ts types and constants both sides use (optional)
server/index.ts default export: definePluginServer({ manifest, module })
server/… a NestJS module: controllers, services, MCP tools, tests (*.test.ts)
web/index.tsx default export: defineWebPlugin({ … })
web/… React pages and components, CSS
apps/server/prisma/schema/
plugin-dev.prisma the plugin's tables (plugin_dev_…)
The desk never imports a plugin's files by name. It reads the list in plugins.config.ts and loads
<id>/server/index and <id>/web/index.tsx by convention. Removing a plugin is deleting its line (and, if you like,
its folder): the desk still builds, and the plugin's tables and data stay in the database.
How it is built
The server build (nest build, swc) compiles apps/server/src and plugins/ (without the
web/ folders) into apps/server/dist, so a plugin's server code ends up in dist/plugins/<id>/server. Vite
bundles the web/ folders into the app (import.meta.glob), with the board-style pages split into their own chunks
when you load them lazily. The Dockerfile copies plugins/ into the build; nothing there names a plugin.
Imports
[!IMPORTANT] Plugins use the desk through two names, and nothing else of the desk's code.
| Import | File | What it has |
|---|---|---|
@desk/server/plugin-sdk | apps/server/src/plugin-sdk.ts | The extension points, access scope, Prisma, errors, validation, decorators, Markdown, the case service. |
@desk/web/plugin-sdk | apps/web/src/plugins/sdk.ts | The extension point types, the API client and hooks, the app's components (lib/ui.tsx), auth and toasts, and the list parts below. |
@desk/shared has the manifest types (packages/shared/src/plugins.ts).
For long lists the web SDK has the inbox's parts (lib/list.tsx), so a plugin's list works like the inbox's:
FilterPill,GlanceChips,ColHead;useDensitywithDensityToggle;useListKeys(thej/kkeyboard);PagerandusePager.
A list of tasks uses the shared frame:
ListToolbar:search, then the standardfilters, yourextraones, and theend(sort, clear, columns, density).- The standard pills
PersonPill,AccountPillandPeriodPill(Created, Updated) next toFilterPill. TaskTablewithuseColumns(list, columns)andColumnsMenu: describe every column (ListColumn:id,label,widthas a grid track,render, optionalsort,mainfor the title,default) and each person picks theirs, saved ascolumns.<list>in their preferences.
Package dependencies
List them in the plugin's package.json with exact versions. Packages the server side needs at
runtime must also be dependencies of apps/server (compiled plugin code runs from apps/server/dist and resolves
packages from there); web-only packages (like @dnd-kit/core for the board) need nothing more.
The plugin API
| Endpoint | Who | What |
|---|---|---|
GET /api/v1/plugins | Signed in (any token) | Every plugin: id, name, description, version, enabled, scopes, events, requires and requiredBy (plugin ids). |
GET /api/v1/admin/plugins | Admins, no tokens | As above, with turnedOn and turnedOnAt. |
POST /api/v1/admin/plugins/<id>/on, …/off | Admins, no tokens | Turn it on or off (stored as the installation setting plugin.<id>.enabled, audited), and the plugin as it is now. On also turns on what it needs; off leaves the plugins built on it turned on but off with it (turnedOn: true, enabled: false), until it is on again. |
In this section
- The manifest: Id, name, permissions, roles, events and what a plugin needs.
- Server side: The NestJS module and the plugin context: routes, events, MCP tools and extension points.
- Web side: Pages, slots, panels and the web plugin SDK.
- The Software Development plugin's API: Projects, tasks and sprints for other plugins.
- The service catalogue API: Services, health, windows and meetings for other plugins.
- The Problem Management plugin's API: Problems for other plugins.
- The Change Management plugin's API: Changes for other plugins.
- The Incident Management plugin's API: Incidents for other plugins.
- The Major Incident Management plugin's API: Major incidents for other plugins.
- The Status Page plugin's API: The status page for other plugins.
- The Access Management plugin's API: Access history and reviews for other plugins.