Building a plugin
A Mushi Mushi plugin is an HTTPS endpoint that receives HMAC-signed webhook events and does something useful with them. @mushi-mushi/plugin-sdk gives you a handler that checks the signature, drops duplicate deliveries and routes each event to your code, plus typed event shapes.
npm install @mushi-mushi/plugin-sdk honohono (or express) is an optional peer dependency; install the one your
server uses.
Minimal receiver
import { Hono } from 'hono'
import { createPluginHandler } from '@mushi-mushi/plugin-sdk'
import { honoHandler } from '@mushi-mushi/plugin-sdk/hono'
const handler = createPluginHandler({
secret: process.env.MUSHI_PLUGIN_SECRET!,
on: {
'report.classified': async (e) => {
await handleTriaged(e.data)
},
'fix.applied': async (e) => {
await handleFixApplied(e.data)
},
},
})
const app = new Hono()
app.post('/mushi/webhook', honoHandler(handler))
export default apphonoHandler comes from the /hono subpath, and expressMiddleware from
@mushi-mushi/plugin-sdk/express. For any other framework, call
handler({ rawBody, headers }) yourself and send back the status and body
it returns. Events with no handler (and no '*' handler) are answered 200
and ignored.
Answer within 8 seconds. Mushi Mushi treats a slower answer as a timeout and retries it, even though the SDK handler’s own time limit is 25 seconds. Any non-2xx answer is retried too: up to 5 attempts in all, about 30 s, 2 min, 10 min and 1 h apart. Queue long work instead of awaiting it inline. Retries reuse the same deliveryId, so the handler’s duplicate check skips work that already succeeded (the default store is in memory; pass a dedupStore to share it across instances).
Typed event shapes
import type {
MushiEventEnvelope,
MushiEventName,
MushiReportCreatedEvent,
MushiReportClassifiedEvent,
MushiReportStatusChangedEvent,
MushiFixEvent,
MushiJudgeScoreEvent,
} from '@mushi-mushi/plugin-sdk'parseEvent(raw) and isKnownEvent(name) are there for runtime checks.
Some SDK types promise fields the server doesn’t send: fix events other than
fix.requested have no fix.status, report.status_changed has actor
rather than actorUserId, and the judge fields differ. See
Webhook events for the payloads as sent.
Signature verification
createPluginHandler checks the Standard Webhooks headers when they are present: webhook-signature: v1,<base64> is HMAC-SHA256 over ${webhook-id}.${webhook-timestamp}.${rawBody}, with the timestamp in seconds. Otherwise it checks the legacy X-Mushi-Signature: t=<unix-ms>,v1=<hex> header, HMAC-SHA256 over ${t}.${rawBody}. Both reject a timestamp more than 5 minutes off.
To check by hand, use verifyStandardWebhooksSignature({ rawBody, webhookId, webhookTimestamp, webhookSignature, secret }) or verifySignature({ rawBody, header, secret }). Always pass the exact raw body you received, not re-serialized JSON.
Publishing to the marketplace
- Open a PR with a migration in
packages/server/supabase/migrations/that inserts aplugin_registryrow. Required columns:slug,name,short_description,publisher, andmanifest(a JSON object withsubscribes, the default event list, andconfig). Optional:long_description,homepage_url,source_url,required_scopes,category. - Add a doc page at
apps/docs/content/plugins/<slug>.mdx. - Once the migration is applied to the database, the listing appears under Marketplace for every project.
Local testing
The SDK ships a mushi-plugin command that posts a signed sample event to
your local server:
export MUSHI_PLUGIN_SECRET=dev-secret
export MUSHI_PLUGIN_URL=http://localhost:3000/mushi/webhook # default: http://localhost:3000/webhook
npx mushi-plugin simulate report.classifiedTo test against Mushi Mushi itself, expose your endpoint over HTTPS (for example ngrok http 3000), install the plugin with that URL, then press Test on it under Marketplace → Installed. The test sends a test.delivery event with data: { test: true }, and the result shows on the Deliveries tab.