npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beignet/provider-jobs-inngest

v0.0.44

Published

Inngest-backed jobs provider for Beignet

Readme

@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.

Install

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

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

Setup

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

Usage

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

Unique jobs

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.

Configuration

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.

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.

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

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 Beignet jobs.

await ctx.ports.inngest.send({
  name: "user.invited",
  data: {
    inviterId: ctx.actor.id,
    inviteeEmail: input.email,
    inviteId: createdInvite.id,
  },
});

client: Inngest

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

Devtools

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.

TypeScript support

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

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:

// 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.

Inngest functions

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.

Lifecycle

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

Error handling

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.

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

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.

Deployment notes

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.

License

MIT