Skip to main content

Overview

The Inngest provider extends your application ports with background job and event capabilities using Inngest. It enables reliable event-driven workflows and background processing for your Contract Kit applications.

Installation

npm install @contract-kit/provider-inngest inngest

Configuration

The Inngest provider reads configuration from environment variables:
VariableRequiredDescriptionDefault
INNGEST_APP_NAMENoFriendly application name shown in Inngest"contract-kit-app"
INNGEST_EVENT_KEYNoEvent key / signing key for Inngest Cloud-
INNGEST_EVENT_KEY is required when using Inngest Cloud for production deployments.

Example .env

INNGEST_APP_NAME=my-app
INNGEST_EVENT_KEY=your-event-key

Setup

Basic Setup

import { createServer } from "@contract-kit/server";
import { inngestProvider } from "@contract-kit/provider-inngest";

const app = createServer({
  ports: basePorts,
  providers: [inngestProvider],
  createContext: ({ ports }) => ({
    ports,
    // ... other context
  }),
  routes: [
    // ... your routes
  ],
});

Type Your Ports

To get proper type inference for the inngest port, extend your ports type:
import type { InngestPort } from "@contract-kit/provider-inngest";

// Your base ports
const basePorts = definePorts({
  db: dbAdapter,
});

// After using inngestProvider, your ports will have this shape:
type AppPorts = typeof basePorts & {
  inngest: InngestPort;
};

API Reference

The provider extends your ports with an inngest property:

send<TData>(args)

Send an event to Inngest.
await ctx.ports.inngest.send({
  name: "user.invited",
  data: {
    inviterId: ctx.userId!,
    inviteeEmail: input.email,
    inviteId: createdInvite.id,
  },
});
Parameters:
  • args.name: Event name
  • args.data: Event data
Returns: Promise<void>

client

Access the underlying Inngest client for advanced operations like defining functions.
// Define Inngest functions using the client directly
const myFunction = ctx.ports.inngest.client.createFunction(
  { id: "my-function" },
  { event: "user.invited" },
  async ({ event, step }) => {
    // Your function logic
  }
);
Type: Inngest

Usage Examples

Sending Events in Use Cases

async function inviteUser(ctx: AppCtx, input: InviteUserInput) {
  // Create the invite
  const invite = await ctx.ports.db.invites.create({
    inviterId: ctx.userId!,
    inviteeEmail: input.email,
  });

  // Send an event to trigger background job
  await ctx.ports.inngest.send({
    name: "user.invited",
    data: {
      inviterId: ctx.userId!,
      inviteeEmail: input.email,
      inviteId: invite.id,
    },
  });

  return invite;
}

Wiring Domain Events to Inngest Jobs

This provider does NOT automatically subscribe to domain events. This is intentional to keep the provider generic and reusable. To wire domain events to Inngest jobs, create a separate jobs provider in your application:
// app/providers/jobs.ts
import { createProvider } from "@contract-kit/ports";
import type { AppPorts } from "@/lib/ports";

export const jobsProvider = createProvider<AppPorts, never>({
  name: "jobs",

  async register({ ports }) {
    // Define your job-specific methods
    ports.extend("jobs", {
      enqueueInviteEmail: async (event) => {
        await ports.inngest.send({ 
          name: "user.invited", 
          data: event 
        });
      },
    });
  },

  async onAppStart({ ports }) {
    // Subscribe to domain events and enqueue jobs
    ports.eventBus.subscribe("user.invited", async (event) => {
      await ports.jobs.enqueueInviteEmail(event);
    });
  },
});
Then use it in your app:
const app = createServer({
  ports: basePorts,
  providers: [
    eventBusProvider,
    inngestProvider,
    jobsProvider, // Wire domain events to Inngest
  ],
  // ...
});
This pattern clearly separates:
  • Framework-level provider (inngestProvider) - Inngest integration
  • App-level provider (jobsProvider) - Business job logic

Defining Inngest Functions

Inngest functions (the handlers that process events) should be defined separately from your Contract Kit application, typically in a separate API route or serverless function:
// app/api/inngest/route.ts (Next.js App Router example)
import { serve } from "inngest/next";
import { inngest } from "@/lib/inngest"; // Your Inngest client

// Define your functions
const sendInviteEmail = inngest.createFunction(
  { id: "send-invite-email" },
  { event: "user.invited" },
  async ({ event, step }) => {
    await step.run("send-email", async () => {
      // Send email logic
    });
  }
);

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [sendInviteEmail],
});

Lifecycle

The Inngest provider:
  1. During register:
    • Creates Inngest client
    • Adds the inngest port
  2. No cleanup needed: Inngest client doesn’t require explicit shutdown

Error Handling

The provider will throw errors in these cases:
  • Missing required configuration (though all fields have defaults or are optional)
  • Invalid configuration values
Make sure to handle these during application startup.

Local Development

For local development, you can run the Inngest Dev Server:
npx inngest-cli@latest dev
This provides a local UI at http://localhost:8288 where you can:
  • View events
  • Trigger functions
  • Debug execution

Best Practices

Send only necessary data in events. Large payloads can cause performance issues.
Follow a consistent naming pattern like entity.action (e.g., user.invited, order.created).
Design your Inngest functions to handle duplicate events gracefully.
Break down complex jobs into steps using step.run() for better visibility and retry control.
Keep Inngest integration (framework-level) separate from your business job logic (app-level).

Next Steps