Restaq
Concepts

Typed Events

How event data is fully typed based on the plugins you register

When you register a plugin with restaq, the event catalog is automatically inferred and merged. Your handlers get full TypeScript autocomplete—no casting, no unknown types.

How it works

Each plugin carries an event map—a TypeScript type that describes what events it can emit and what their payloads look like. When you pass the plugins array to restaq, the function infers the union of all those maps:

import { restaq as createRestaq } from 'restaq';
import { stripe } from '@restaq/stripe';
import { github } from '@restaq/github';

export const restaq = createRestaq({
  plugins: [stripe(), github()],
});

// restaq.on() now autocompletes all Stripe and GitHub event names:
restaq.on('stripe.charge.succeeded', async (event, ctx) => {
  // event.data is typed as Stripe.Charge
  const charge = event.data.object;
  console.log(charge.amount); // ✓ TypeScript knows this field exists
});

restaq.on('github.pull_request.opened', async (event, ctx) => {
  // event.data is typed as the GitHub pull_request.opened payload
  console.log(event.data.pull_request.title); // ✓ autocomplete works
});

Stripe events

The stripe plugin exports a typed catalog (StripeEventMap) that is derived from your installed stripe package's event discriminated union. This means:

  • The types track the Stripe API version pinned in your package.json
  • event.data for a stripe.charge.succeeded event is typed as { object: Stripe.Charge }
  • Every Stripe webhook event (~250 types) is included

GitHub events

The github plugin exports GitHubEventMap from the @octokit/webhooks-types package. GitHub's event naming respects its delivery model:

  • Base events (no action): github.push, github.ping — these appear as bare event types
  • Action events: github.pull_request.opened, github.pull_request.closed, github.issues.labeled — these are keyed by action, never by the base event name alone

This means you'll never see a bare github.pull_request key in the type map—only the action-qualified variants that actually fire.

Unknown events

If an event type isn't registered in any plugin's catalog, TypeScript still allows it—it just falls back to data: Record<string, unknown>:

// If "custom.myevent" isn't in any registered plugin's catalog:
relay.on('custom.myevent', async (event, ctx) => {
  // event.data is Record<string, unknown> - you lose autocomplete
  const value = event.data.someField; // ✓ compiles, but type is unknown
});

This lets you write custom handlers for test events or unsupported providers without breaking the build.

Important: Compile-time only

Typed events are compile-time only. The type system gives you autocomplete and type safety, but payloads are not validated at runtime. After signature verification (which is always enforced), the webhook body is cast to match the type—but if the provider's API evolved or sent unexpected fields, TypeScript won't catch it.

For example, if you pinned an older Stripe API version on your webhook endpoint and it sends fields that differ from the types, the handler still runs—the mismatch isn't detected at runtime.

Writing a custom typed plugin

To create your own plugin with a typed event map, use definePlugin:

import { definePlugin } from '@restaq/plugin';

type MyEventMap = {
  'my.user.created': { userId: string; email: string };
  'my.order.placed': { orderId: string; amount: number };
};

const myPlugin = definePlugin<MyEventMap>({
  id: 'my',
  async verify(req) {
    // signature verification logic
    return true;
  },
  normalize(rawBody) {
    // convert raw body to NormalizedEvent
    return { id: '...', type: '...', data: {}, receivedAt: new Date().toISOString() };
  },
  sign(rawBody, secret) {
    // sign a payload for relay trigger
    return {};
  },
  buildTestPayload(eventType, data) {
    // build a test payload for relay trigger
    return { body: data };
  },
});

export const restaq = createRestaq({
  plugins: [myPlugin],
});

restaq.on('my.user.created', async (event, ctx) => {
  console.log(event.data.userId); // ✓ typed as string
  console.log(event.data.email); // ✓ typed as string
});

On this page