Example: GitHub Plugin

A complete walkthrough of the first-party GitHub plugin: an Express server wiring lifecycle, OAuth setup, token-verified APIs, widgets, and webhooks.

A Real, End-to-End Plugin

The first-party GitHub plugin is a full implementation of everything in this section. It links commits, branches, and pull requests to tickets, renders them as widgets on the ticket detail page, and stays in sync through GitHub webhooks. Reading its structure is the fastest way to see how the pieces fit together.

The Server

The plugin is a single Express app that mounts a router per concern and serves its built widget bundles as static assets:

TypeScripttypescript
import express from 'express';
import cors from 'cors';
import lifecycleRouter from './routes/lifecycle.js';
import setupRouter from './routes/setup.js';
import apiRouter from './routes/api.js';
import widgetsRouter from './routes/widgets.js';
import { webhookRouter } from './routes/webhook.js';
 
const app = express();
app.use(cors());
 
// Webhooks need the RAW body for signature verification,
// so register them BEFORE express.json().
app.use('/github/webhook', webhookRouter);
app.use(express.json());
 
// Serve the built widget assets.
app.use('/static', express.static('dist/widgets'));
 
app.use(lifecycleRouter);  // POST /lifecycle
app.use(setupRouter);      // GET  /setup, OAuth callback
app.use(apiRouter);        // GET  /api/prs, /api/commits, ...
app.use(widgetsRouter);    // GET  /widgets/prs, /widgets/commits
 
app.listen(process.env.PORT ?? 5301);

Order matters: mount any webhook route that verifies a signature before express.json(), otherwise the body parser consumes the raw payload you need to compute the HMAC.

The Route Modules

Each concern lives in its own router under routes/, all mounted by server.ts above. Browse the plugin file by file:

TypeScripttypescript
// Persists the installation and integration API key on install; cleans up on uninstall.
import { Router } from 'express';
import { createLifecycleHandler } from '@pomavo/sdk/server';
 
const router = Router();
 
router.post('/lifecycle', createLifecycleHandler({
  async onInstall({ installationId, sharedSecret, pomavoApiBaseUrl, integrations }) {
    await db.installations.upsert({ installationId, sharedSecret, pomavoApiBaseUrl });
    const first = integrations?.[0];
    if (first) await db.apiKeys.upsert({ installationId, apiKey: first.apiKey });
  },
  async onUninstall({ installationId }) {
    await db.installations.delete({ installationId });
  },
}));
 
export default router;

Lifecycle

The lifecycle router persists the installation and the integration API key, and cleans up on uninstall - exactly the pattern from Lifecycle & Auth. See the lifecycle.ts tab above for the full handler.

OAuth Setup

Some configuration can't be a simple parameter. The GitHub plugin's setup.url opens a page that runs an OAuth handshake, then stores the resulting GitHub token against the installation:

text
GET  /setup            → renders the "Connect GitHub" page
GET  /github/callback  → exchanges the OAuth code, stores the token
POST /github/sync      → backfills repos/commits for the installation

The host links admins to /setup after install so they can finish connecting their GitHub account.

Token-Verified API

The plugin's data endpoints are protected by the token verifier, so each request is tied to a real org, user, and ticket. The verifier resolves the installation's shared secret, then validates the host token before your handler runs - see the api.ts tab above.

Widgets

Each widget is a React app built to a static bundle and served from /widgets/.... It uses the PomavoBridge to get context, follow the theme, and auto-resize - see Plugin UI for the full PrWidget source. The flow is always the same:

Mount & signal ready

The widget creates a PomavoBridge, subscribes to theme changes, and calls signalReady().

Get context

requestContext() returns the token and the current ticketSequenceNumber.

Fetch from /api

The widget calls /api/prs?ticket=... with Authorization: Bearer <token>, which the token-verified router authenticates.

Render & resize

It renders the pull requests and calls enableAutoResize() so the iframe fits the list.

Webhooks

To stay current, the plugin subscribes to GitHub webhooks. The webhook router verifies GitHub's signature against the raw body, then updates its own store and, where relevant, posts back to Pomavo with the PomavoClient, for example by adding a comment when a PR merges.

Putting It Together

text
Install        → onInstall stores installationId + sharedSecret + apiKey
Configure      → /setup runs OAuth, stores the GitHub token
View a ticket  → host embeds /widgets/prs in a sandboxed iframe
Widget loads   → bridge.requestContext() → token + ticket
Widget fetches → GET /api/prs (Bearer token) → verified → returns PRs
GitHub event   → webhook verifies signature → updates store → PomavoClient comments back
Uninstall      → onUninstall deletes the installation and revokes access

The GitHub plugin lives at Pomavo.GitHubPlugin in the monorepo. Use it as a reference implementation when scaffolding your own plugin.

Next Steps