Lifecycle & Auth
Handle install, uninstall, and update events on your plugin backend, and verify the signed tokens the host sends to your API.
The Lifecycle Endpoint
When an organization installs, updates, or removes your plugin, Pomavo sends a POST to the lifecycle.url declared in your manifest. The SDK gives you createLifecycleHandler so you don't have to parse the event envelope yourself.
import express from 'express';
import { createLifecycleHandler } from '@pomavo/sdk/server';
const app = express();
app.use(express.json());
app.post('/lifecycle', createLifecycleHandler({
async onInstall({ installationId, sharedSecret, pomavoApiBaseUrl, integrations }) {
// Persist everything you need to serve this installation later.
await db.installations.insert({ installationId, sharedSecret, pomavoApiBaseUrl });
// Store any integration API keys granted at install time.
for (const integration of integrations ?? []) {
await db.integrations.insert({
installationId,
integrationId: integration.integrationId,
name: integration.name,
apiKey: integration.apiKey,
});
}
},
async onUpdate({ installationId, manifestJson }) {
await db.installations.update({ installationId }, { manifestJson });
},
async onUninstall({ installationId }) {
await db.installations.delete({ installationId });
},
}));The sharedSecret is the key to everything: store it securely, scoped to the installationId. You use it to verify every token the host later sends to your API. Treat it like a password.
Event Payloads
The handler dispatches to your callbacks with these typed payloads:
interface InstallPayload {
pluginId?: string;
installationId: string;
sharedSecret: string;
pomavoApiBaseUrl: string;
manifestJson?: Record<string, any>;
integrations?: Array<{
integrationId: number;
name: string;
apiKey: string;
}>;
}
interface UpdatePayload {
pluginId?: string;
installationId: string;
manifestJson: Record<string, any>;
}
interface UninstallPayload {
pluginId?: string;
installationId: string;
}| Field | When | Use it for |
|---|---|---|
installationId | Every event | The primary key for this org's installation. |
sharedSecret | Install | Verifying tokens; never expires until uninstall. |
pomavoApiBaseUrl | Install | The base URL to point a PomavoClient at. |
integrations[].apiKey | Install | Authenticating PomavoClient calls back to Pomavo. |
manifestJson | Install / Update | The exact manifest the org installed. |
Verifying Host Tokens
Every time the host renders one of your widgets, it mints a short-lived plugin token - an HS256 JWT signed with that installation's sharedSecret. Your widget forwards this token to your API as a Bearer credential, and your backend verifies it before trusting any of its claims.
Express Middleware
The simplest approach is the createTokenVerifier middleware, which validates the token and attaches the decoded claims to req.pluginContext:
import { createTokenVerifier } from '@pomavo/sdk/server';
// sharedSecret comes from the installation record you stored at install time.
const sharedSecret = await getSharedSecretForRequest(req);
app.use('/api', createTokenVerifier(sharedSecret));
app.get('/api/prs', (req, res) => {
const { org, user, ticketSequenceNumber } = req.pluginContext;
res.json({ org: org?.name, viewer: user?.name, ticket: ticketSequenceNumber });
});Manual Verification
If you aren't using Express, verify the token directly:
import { verifyPluginToken } from '@pomavo/sdk/server';
const token = req.headers.authorization?.slice(7); // strip "Bearer "
const ctx = verifyPluginToken(token, sharedSecret);verifyPluginToken checks the HMAC-SHA256 signature, enforces the exp claim, and decodes the embedded context. It throws if the signature is invalid or the token has expired.
The Token Payload
After verification you get a PluginTokenPayload:
interface PluginTokenPayload {
installationId?: string;
orgId?: string;
ticketSequenceNumber?: string;
org?: {
id: string;
name?: string;
slug?: string;
shortName?: string;
logo?: string;
};
user?: {
id: string;
name?: string;
email?: string;
username?: string;
image?: string;
};
[key: string]: any;
}This tells you which organization and which user is viewing the widget, and which ticket they're looking at - all cryptographically attested by the shared secret, so you never trust client-supplied identifiers.
Because the token is signed with a per-installation secret, a token minted for one organization can never be replayed against another. Always look up the sharedSecret by the installation the request targets, then verify.