Beignet API reference
    Preparing search index...

    Module @beignet/provider-jobs-inngest

    @beignet/provider-jobs-inngest

    Caution

    Beignet is experimental alpha software. The 0.0.x package line is for early evaluation, and APIs may change between releases while the framework settles.

    Inngest-backed jobs provider for Beignet applications.

    The provider installs the app-facing ctx.ports.jobs dispatcher for durable background work using Inngest, a durable execution platform for background jobs, scheduled functions, and workflows. In Beignet, this package adapts Inngest to the JobDispatcherPort; it is not the Beignet domain event bus.

    The provider also exposes ctx.ports.inngest as an escape hatch for raw Inngest access.

    createInngestJobsProvider(...) returns the stable InngestJobsProvider type. InngestConfig describes its validated config; the Zod schema remains internal.

    bun add @beignet/provider-jobs-inngest @beignet/core inngest@^4.12.1
    

    Inngest v4 and Node.js 20 or newer are required.

    import { createNextServer, createNextServerLoader } from "@beignet/next";
    import { definePorts } from "@beignet/core/ports";
    import { createInngestJobsProvider } from "@beignet/provider-jobs-inngest";
    import { inngest } from "@/infra/inngest";
    import { routes } from "@/server/routes";

    // Set environment variables:
    // INNGEST_APP_NAME=my-app (optional, defaults to "beignet-app")
    // INNGEST_DEV=1 (local dev server only)
    // INNGEST_EVENT_KEY=your-event-key (production cloud dispatch)
    // INNGEST_SIGNING_KEY=your-signing-key (production endpoint verification)

    const initialPorts = definePorts({});

    export const getServer = createNextServerLoader(() =>
    createNextServer({
    ports: initialPorts,
    providers: [createInngestJobsProvider({ client: inngest })],
    context: ({ 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 { createJobs, retry } from "@beignet/core/jobs";
    import { z } from "zod";

    const { defineJob } = createJobs<AppContext>();

    export const SendInviteEmailJob = defineJob("mail.invite.send", {
    payload: z.object({
    inviteId: z.string(),
    inviteeEmail: z.string().email(),
    }),
    retry: retry.exponential({
    attempts: 3,
    }),
    timeout: "30s",
    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: AppContext, 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;
    }

    Inngest dispatchers are ordinary Beignet job dispatchers, so apps can wrap them with createUniqueJobDispatcher(...) from @beignet/core/jobs when a job's unique declaration should suppress duplicate dispatches through ctx.ports.locks:

    import { createUniqueJobDispatcher } from "@beignet/core/jobs";
    import type { LocksPort } from "@beignet/core/locks";
    import {
    createInngestJobDispatcher,
    type InngestPort,
    } from "@beignet/provider-jobs-inngest";

    export function createJobsPort(args: {
    inngest: InngestPort;
    locks: LocksPort;
    }) {
    const inngestJobs = createInngestJobDispatcher(args.inngest.client);

    return createUniqueJobDispatcher({
    jobs: inngestJobs,
    locks: args.locks,
    });
    }

    The uniqueness lease prevents duplicate Inngest event sends for the configured TTL. Inngest still owns execution retries, so handlers must remain idempotent for provider retries and process failures.

    The Beignet provider and the Inngest SDK use environment variables with the INNGEST_ prefix:

    Variable Required Description Default
    INNGEST_APP_NAME No Stable application ID shown in Inngest "beignet-app"
    INNGEST_DEV Local only Set to 1 when using the local dev server -
    INNGEST_EVENT_KEY Production Authenticates events sent to Inngest Cloud -
    INNGEST_SIGNING_KEY Production Verifies requests to the function endpoint -
    INNGEST_SIGNING_KEY_FALLBACK No Previous signing key during rotation -

    Do not deploy with INNGEST_DEV=1. Run beignet preflight in the production environment to require both cloud credentials and reject local-dev mode.

    beignet doctor --strict checks that installed Inngest jobs providers are registered in server/providers.ts, that server/inngest.ts covers generated job registries, and that Next.js apps expose app/api/inngest/route.ts.

    Prefer one app-owned client shared by dispatch and execution:

    import { createInngestJobsProvider } from "@beignet/provider-jobs-inngest";
    import { inngest } from "@/infra/inngest";

    export const providers = [
    createInngestJobsProvider({ client: inngest }),
    ];

    Calling createInngestJobsProvider() with no options remains available for env-backed dispatch-only setups. Passing appName and eventKey configures that provider-owned client directly.

    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.

    When tracing is active, the event data uses Beignet's versioned job transport envelope. createInngestJobFunction(...) accepts both that envelope and legacy raw payloads, unwraps before validation and context resolution, and continues the producer trace. Unknown or malformed trace metadata is ignored without blocking execution. Raw ctx.ports.inngest.send(...) events are unchanged.

    await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
    inviteId: invite.id,
    inviteeEmail: input.email,
    });

    The inngest port is an escape hatch for direct Inngest usage.

    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,
    },
    });

    Access the underlying Inngest client for advanced Inngest operations.

    // Define Inngest functions using the client directly
    const myFunction = ctx.ports.inngest.client.createFunction(
    { id: "my-function", triggers: { 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 a static or lazy instrumentation target to createInngestJobFunction(...) to record worker execution as started, completed, and failed events:

    import { createServiceActor } from "@beignet/core/ports";
    import { createInngestJobFunction } from "@beignet/provider-jobs-inngest";
    import { SendInviteEmailJob } from "@/features/users/jobs";
    import { inngest } from "@/infra/inngest";
    import { getServer } from "@/server";

    const sendInviteEmail = createInngestJobFunction({
    client: inngest,
    job: SendInviteEmailJob,
    ctx: async () =>
    (await getServer()).createServiceContext({
    actor: createServiceActor("beignet-inngest"),
    }),
    instrumentation: async () => (await getServer()).ports,
    });

    When that target exposes ports.tracing, the function starts the job span before evaluating the lazy ctx factory. Service contexts created by the factory therefore inherit the real active job span. Register the app-owned OpenTelemetry SDK in the Inngest function process before initializing the server.

    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-jobs-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 { createListeners } from "@beignet/core/events";
    import { UserInvited } from "@/features/users/domain/events";
    import { SendInviteEmailJob } from "@/features/users/jobs";
    import type { AppContext } from "@/app-context";

    const { defineListener } = createListeners<AppContext>();

    export const sendInviteEmail = defineListener("mail.send-invite-email", {
    event: UserInvited,
    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 createInngestJobsProvider({ client: inngest }) and expose the same client through the function endpoint.

    Use createInngestJobFunction(...) to turn one first-class Beignet job into an Inngest function, or createInngestJobFunctions(...) for a central registry. The helpers subscribe to job.name, validate incoming event data with parseJobPayload, and then call job.handle(...).

    If the job defines a retry policy, the helper maps Beignet's total retry.attempts value to Inngest's function-level retries option. Inngest supports retry values from 0 to 20, so Beignet accepts total attempt counts from 1 to 21 for Inngest-backed jobs. A job without an explicit attempt count is configured with retries: 0, overriding Inngest's provider default so it remains a single-attempt Beignet job.

    Inngest-backed jobs support Beignet's retry attempt count only. Custom Beignet backoff fields, jitter, and retryIf classification are outbox-worker semantics; createInngestJobFunction(...) fails fast if a job policy includes retry behavior that the adapter cannot honor.

    When a job declares timeout, createInngestJobFunction(...) maps it to Inngest's timeouts.finish setting. Inngest function timeouts use whole-second precision, so Beignet fails fast for millisecond-precision timeouts that cannot be represented exactly.

    createInngestJobFunction(...) also accepts hooks. Function hooks wrap job-local hooks around every handler attempt, run inside the Beignet timeout, and fail through Inngest's function error path like handler errors. When the Inngest SDK reports attempt metadata, Beignet forwards it to hooks using the same one-based attempt convention as other job runners.

    The Beignet terms still apply: attempts is the maximum total attempts, including the first try, while Inngest's retries setting is the number of retries after the first try. Inngest owns backoff and terminal failure handling for direct Inngest jobs. Use the Beignet outbox when the app needs Beignet-owned backoff, retry classification, or dead-letter state.

    Pass errorReporter to report the native Inngest onFailure lifecycle after all provider retries are exhausted. Individual attempts remain visible through instrumentation and are not reported as incidents. The reporter may be a port or a lazy resolver when server startup contributes it:

    const sendInviteEmail = createInngestJobFunction({
    client: inngest,
    job: SendInviteEmailJob,
    ctx: () => createBackgroundContext(),
    errorReporter: async () => (await getServer()).ports.errorReporter,
    });

    Terminal reports include only the job name and Inngest function/run/event identifiers, never the event payload.

    This is the provider convention for external job systems: map the Beignet semantics the provider can actually honor, document the mapping, and fail fast for retry fields that would otherwise be silently ignored.

    Keep registration separate from the route so beignet make job can maintain one explicit execution registry:

    // server/inngest.ts
    import { createServiceActor } from "@beignet/core/ports";
    import { createInngestJobFunctions } from "@beignet/provider-jobs-inngest";
    import { inngest } from "@/infra/inngest";
    import { userJobs } from "@/features/users/jobs";
    import { getServer } from "@/server";

    export const inngestJobs = [...userJobs] as const;
    export const inngestFunctions = createInngestJobFunctions({
    client: inngest,
    jobs: inngestJobs,
    ctx: async () => {
    const server = await getServer();
    return server.createServiceContext({
    actor: createServiceActor("beignet-inngest"),
    });
    },
    instrumentation: async () => (await getServer()).ports,
    errorReporter: async () => (await getServer()).ports.errorReporter,
    });
    // app/api/inngest/route.ts (Next.js App Router)
    import { serve } from "inngest/next";
    import { inngest } from "@/infra/inngest";
    import { inngestFunctions } from "@/server/inngest";

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

    The lazy resolvers avoid booting the Beignet server when Next.js imports the route during a production build. They resolve only inside an active function execution.

    The Inngest jobs provider:

    1. During setup:
      • Uses the injected Inngest client, or creates one for env-backed setup
      • Returns the jobs port
      • Returns the inngest escape hatch port
    2. No cleanup needed: Inngest client doesn't require explicit shutdown

    The provider will throw errors in these cases:

    • Invalid configuration values
    • Unsupported Beignet retry options on an Inngest-backed function, such as custom backoff fields, jitter, or retryIf
    • Millisecond-precision Beignet timeouts on an Inngest-backed function

    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:

    • View events
    • Trigger functions
    • Debug execution

    Use createInlineJobDispatcher(...) or a recording dispatcher in use-case tests when you only need to assert dispatch intent. Use createInngestJobFunction(...) tests for adapter behavior that depends on Inngest event payload shape.

    Expose Inngest functions through the runtime adapter your host expects, such as inngest/next for a Next.js route. This package targets Inngest v4 and Node.js 20 or newer. Configure the endpoint URL in Inngest Cloud and deploy both INNGEST_EVENT_KEY and INNGEST_SIGNING_KEY. Use the Beignet outbox instead of direct Inngest dispatch when enqueueing must commit atomically with a database transaction or when the app needs Beignet-owned dead-letter state.

    In non-Next apps, create the app server through that runtime's normal lifecycle and pass its service-context factory, ports, and error reporter to createInngestJobFunctions(...). The CLI-generated server/inngest.ts exposes createAppInngestFunctions(...) for this binding instead of assuming a Next-specific getServer() loader.

    MIT

    CreateInngestJobFunctionOptions
    CreateInngestJobFunctionsOptions
    CreateInngestJobsProviderClientOptions
    CreateInngestJobsProviderConfigOptions
    InngestConfig
    InngestJobDispatcherOptions
    InngestJobFunctionContextArgs
    InngestJobsProviderPorts
    InngestPort
    CreateInngestJobsProviderOptions
    InngestFunctionRetryAttempts
    InngestJobFunctionContext
    InngestJobFunctionErrorReporter
    InngestJobFunctionInstrumentation
    InngestJobsProvider
    createInngestJobDispatcher
    createInngestJobFunction
    createInngestJobFunctions
    createInngestJobsProvider