@contract-kit/provider-inngest
v1.0.0
Published
Inngest provider for contract-kit - adds inngest port for background jobs and events
Maintainers
Readme
@contract-kit/provider-inngest
Inngest provider for contract-kit that installs an Inngest-backed
JobDispatcherPort for durable background jobs using
Inngest.
Installation
npm install @contract-kit/provider-inngest @contract-kit/events inngest
# or
bun add @contract-kit/provider-inngest @contract-kit/events inngestTypeScript requirements
This package requires TypeScript 5.0 or higher for proper type inference.
Usage
Basic setup
import { createServer } from "@contract-kit/server";
import { inngestProvider } from "@contract-kit/provider-inngest";
// Set environment variables:
// INNGEST_APP_NAME=my-app (optional, defaults to "contract-kit-app")
// INNGEST_EVENT_KEY=your-event-key (optional, required for Inngest Cloud)
const app = await createServer({
ports: basePorts,
providers: [inngestProvider],
createContext: ({ ports }) => ({
ports,
// ... other context
}),
routes: [
// ... your routes
],
});Dispatching jobs in use cases
Once the provider is registered, your ports include:
jobs: the canonicalJobDispatcherPortfor app code.inngest: the raw Inngest escape hatch for advanced usage.
Define jobs with @contract-kit/events, then dispatch them through
ctx.ports.jobs:
import { createEventHandlers } from "@contract-kit/events";
import { z } from "zod";
const events = createEventHandlers<AppCtx>();
export const SendInviteEmailJob = events.defineJob("mail.invite.send", {
payload: z.object({
inviteId: z.string(),
inviteeEmail: z.string().email(),
}),
retry: {
attempts: 3,
},
async handle({ payload, ctx }) {
await ctx.ports.mailer.send({
to: payload.inviteeEmail,
subject: "You were invited",
text: `Invite id: ${payload.inviteId}`,
});
},
});
async function inviteUser(ctx: AppCtx, input: InviteUserInput) {
const invite = await ctx.ports.db.invites.create({
inviterId: ctx.userId!,
inviteeEmail: input.email,
});
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
inviteId: invite.id,
inviteeEmail: input.email,
});
return invite;
}Configuration
The Inngest provider reads configuration from environment variables with the INNGEST_ prefix:
| Variable | Required | Description | Default |
|----------|----------|-------------|---------|
| INNGEST_APP_NAME | No | Friendly application name shown in Inngest | "contract-kit-app" |
| INNGEST_EVENT_KEY | No | Event key / signing key for Inngest Cloud | - |
Note: INNGEST_EVENT_KEY is required when using Inngest Cloud for production deployments.
Ports
jobs: JobDispatcherPort
The jobs port is the recommended application API. It validates and parses the
job payload with the job definition before sending an Inngest event using the
job name.
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
inviteId: invite.id,
inviteeEmail: input.email,
});inngest: InngestPort
The inngest port is an escape hatch for direct Inngest usage.
send<TData>(args: { name: string; data: TData }): Promise<void>
Send a raw event to Inngest. Prefer ctx.ports.jobs.dispatch(...) for
first-class Contract Kit jobs.
await ctx.ports.inngest.send({
name: "user.invited",
data: {
inviterId: ctx.userId!,
inviteeEmail: input.email,
inviteId: createdInvite.id,
},
});client: Inngest
Access the underlying Inngest client for advanced operations.
// 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
}
);Devtools
When @contract-kit/devtools is registered before this provider, calls to
ctx.ports.jobs.dispatch(...) and ctx.ports.inngest.send(...) are recorded
automatically under the jobs watcher. Successful enqueues are recorded as
scheduled; failed enqueues are recorded as failed with schedule-phase error
details.
Pass a devtools port to createInngestJobFunction(...) to record worker
execution as started, completed, and failed events:
const sendInviteEmail = createInngestJobFunction({
client: inngest,
job: SendInviteEmailJob,
ctx: ({ step }) => createJobContext({ step }),
devtools: appPorts.devtools,
});TypeScript support
To get proper type inference, include both provider-contributed ports in your app ports type:
import { definePorts } from "@contract-kit/ports";
import type { JobDispatcherPort } from "@contract-kit/ports";
import type { InngestPort } from "@contract-kit/provider-inngest";
// Your base ports, if any
const basePorts = definePorts({});
type AppPorts = typeof basePorts & {
jobs: JobDispatcherPort;
inngest: InngestPort;
};Wiring domain events → Inngest jobs
This provider does NOT automatically subscribe to domain events. That is intentional: events are facts that happened, while jobs are explicit work to do.
To wire a domain event to a durable job, register a listener in your application and dispatch the job from the listener:
// domain/listeners.ts
import { createEventHandlers } from "@contract-kit/events";
import { UserInvited } from "@/domain/events";
import { SendInviteEmailJob } from "@/domain/jobs";
import type { AppCtx } from "@/app-context";
const events = createEventHandlers<AppCtx>();
export const sendInviteEmail = events.defineListener(UserInvited, {
name: "mail.send-invite-email",
async handle({ payload, ctx }) {
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
inviteId: payload.inviteId,
inviteeEmail: payload.inviteeEmail,
});
},
});Register that listener against your event bus during infrastructure startup.
In tests, use createInlineJobDispatcher(...); in production, install
inngestProvider.
Inngest functions
Use createInngestJobFunction(...) to turn a first-class Contract Kit job into
an Inngest function. The helper subscribes to job.name, validates incoming
event data with parseJobPayload, and then calls job.handle(...).
If the job defines retry.attempts, the helper maps it to Inngest's
function-level retries option. Inngest supports integer values from 0 to
20; values outside that range throw during function creation.
Define functions separately from your Contract Kit server, usually in a serverless function or route handler:
// app/api/inngest/route.ts (Next.js App Router example)
import { createInngestJobFunction } from "@contract-kit/provider-inngest";
import { serve } from "inngest/next";
import { inngest } from "@/infra/inngest";
import { SendInviteEmailJob } from "@/domain/jobs";
import { createJobContext } from "@/app-context";
const sendInviteEmail = createInngestJobFunction({
client: inngest,
job: SendInviteEmailJob,
ctx: ({ step }) => createJobContext({ step }),
});
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [sendInviteEmail],
});Lifecycle
The Inngest provider:
- During
setup:- Creates Inngest client
- Returns the
jobsport - Returns the
inngestescape hatch port
- 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 devThis provides a local UI at http://localhost:8288 where you can:
- View events
- Trigger functions
- Debug execution
License
MIT
