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.

TypeScripttypescript
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:

TypeScripttypescript
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;
}
FieldWhenUse it for
installationIdEvery eventThe primary key for this org's installation.
sharedSecretInstallVerifying tokens; never expires until uninstall.
pomavoApiBaseUrlInstallThe base URL to point a PomavoClient at.
integrations[].apiKeyInstallAuthenticating PomavoClient calls back to Pomavo.
manifestJsonInstall / UpdateThe 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:

TypeScripttypescript
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:

TypeScripttypescript
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:

TypeScripttypescript
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.

Next Steps