Inngest-backed JobDispatcherPort provider for Beignet applications.
The provider installs the app-facing ctx.ports.jobs dispatcher for durable
background jobs using Inngest and exposes
ctx.ports.inngest only as an escape hatch for raw Inngest access.
bun add @beignet/provider-inngest @beignet/core inngest
import { createNextServer } from "@beignet/next";
import { definePorts } from "@beignet/core/ports";
import { inngestProvider } from "@beignet/provider-inngest";
import { routes } from "@/server/routes";
// Set environment variables:
// INNGEST_APP_NAME=my-app (optional, defaults to "beignet-app")
// INNGEST_EVENT_KEY=your-event-key (optional, required for Inngest Cloud)
const appPorts = definePorts({});
export const server = await createNextServer({
ports: appPorts,
providers: [inngestProvider],
createContext: ({ ports }) => ({
ports,
}),
routes,
});
Once the provider is registered, your ports include:
jobs: the canonical JobDispatcherPort for app code.inngest: the raw Inngest escape hatch for advanced usage.Define jobs with @beignet/core/jobs, then dispatch them through
ctx.ports.jobs:
import { createJobHandlers } from "@beignet/core/jobs";
import { z } from "zod";
const jobs = createJobHandlers<AppCtx>();
export const SendInviteEmailJob = jobs.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.actor.id,
inviteeEmail: input.email,
});
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
inviteId: invite.id,
inviteeEmail: input.email,
});
return invite;
}
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 | "beignet-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.
jobs: JobDispatcherPortThe 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: InngestPortThe 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 Beignet jobs.
await ctx.ports.inngest.send({
name: "user.invited",
data: {
inviterId: ctx.actor.id,
inviteeEmail: input.email,
inviteId: createdInvite.id,
},
});
client: InngestAccess 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
}
);
When @beignet/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 an instrumentation target to createInngestJobFunction(...) to record
worker execution as started, completed, and failed events:
const sendInviteEmail = createInngestJobFunction({
client: inngest,
job: SendInviteEmailJob,
ctx: () => createBackgroundContext(),
instrumentation: appPorts.devtools,
});
To get proper type inference, include both provider-contributed ports in your app ports type:
import { definePorts } from "@beignet/core/ports";
import type { JobDispatcherPort } from "@beignet/core/ports";
import type { InngestPort } from "@beignet/provider-inngest";
// Your base ports, if any
const basePorts = definePorts({});
type AppPorts = typeof basePorts & {
jobs: JobDispatcherPort;
inngest: InngestPort;
};
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:
// features/users/listeners.ts
import { createEventHandlers } from "@beignet/core/events";
import { UserInvited } from "@/features/users/domain/events";
import { SendInviteEmailJob } from "@/features/users/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(...) from @beignet/core/jobs; in
production, install inngestProvider.
Use createInngestJobFunction(...) to turn a first-class Beignet 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 Beignet server, usually in a serverless function or route handler:
// app/api/inngest/route.ts (Next.js App Router example)
import { createInngestJobFunction } from "@beignet/provider-inngest";
import { serve } from "inngest/next";
import { inngest } from "@/infra/inngest";
import { SendInviteEmailJob } from "@/features/users/jobs";
import { createBackgroundContext } from "@/infra/background-context";
const sendInviteEmail = createInngestJobFunction({
client: inngest,
job: SendInviteEmailJob,
ctx: () => createBackgroundContext(),
});
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [sendInviteEmail],
});
createBackgroundContext() is application-owned. Put it near your
infrastructure wiring and return the ports, logger, auth assumptions, and
devtools context your background workers need.
The Inngest provider:
setup:
jobs portinngest escape hatch portThe provider will throw errors in these cases:
Make sure to handle these during application startup.
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:
MIT