Runtime recipes

Beignet keeps runtime work behind explicit entrypoints. The web process serves HTTP. Cron routes trigger one bounded unit of work. Workers consume queues or repeat bounded commands. Provider-backed function hosts run provider-owned invocations. Tasks run when an operator, CI job, or release pipeline asks for them.

That separation is intentional: providers install ports and manage clients, but they should not start unbounded polling loops, queue consumers, or drain intervals during server boot. Put repeated work in the runtime that owns its scaling, shutdown, health checks, and credentials.

Runtime matrix

Choose the smallest process layout that matches the work your app actually does:

If your app usesRun thisBeignet surface
Contracts, auth, UI, OpenAPI, devtools, uploads, storage routes, and webhooksOne web processcreateApiRoute(getServer), createNextServer(...), or createFetchServer(...)
Best-effort cross-process eventsWeb processes with the Redis event bus provider registeredctx.ports.eventBus, redisEventBusProvider
Durable events or outbox-backed jobsWeb process plus a cron drain route or drain commandcreateOutboxDrainRoute(...), beignet outbox drain
BullMQ-backed jobsWeb process plus a worker processcreateBullMQJobWorker(...)
Inngest-backed jobsWeb process plus the Inngest route or function hostcreateInngestJobFunction(...), serve(...)
SchedulesWeb process plus a protected cron route, scheduled function, or command schedulercreateScheduleRoute(...), beignet schedule run <name>
Backfills, repairs, imports, exports, and release maintenanceOne-off commandbeignet task run <name>

Use a single web process when all work is request/response. Add a cron route when work needs an HTTP-triggered scheduler. Add a worker process when work is long-running, repeated, or queue-backed. Add a provider function host only when the provider owns invocation and retry semantics.

Web process

The web process answers traffic only. It owns API routes, auth callbacks, webhooks, OpenAPI, devtools when enabled, uploads, storage routes, and process-local health endpoints.

In Next.js, expose the central API from a catch-all route:

// app/api/[[...path]]/route.ts
import { createApiRoute } from "@beignet/next";
import { getServer } from "@/server";

export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
  createApiRoute(getServer);

For Bun, Deno, Cloudflare Workers, Node fetch servers, and other Web Fetch runtimes, use @beignet/web and serve the server.fetch handler from the host that owns the process.

Do not start queue consumers, outbox polling loops, schedule intervals, or long-running workers from the web process in serverless deployments. A container web process may make outbound provider calls while handling requests, but background repetition should still live in a worker, cron, or command runtime.

Liveness and readiness

Keep liveness and readiness separate:

Generated apps expose both endpoints. A readiness route can use createHealthRoute(...) from @beignet/next and provider-owned checkHealth() helpers:

// app/api/ready/route.ts
import { createHealthRoute } from "@beignet/next";
import { env } from "@/lib/env";
import { getServer } from "@/server";

export const { GET } = createHealthRoute(
  getServer,
  {
    checks: {
      database: (ports) => ports.db.checkHealth(),
      cache: (ports) => ports.redis.checkHealth(),
      search: (ports) => ports.meilisearch.checkHealth(),
      storage: (ports) => ports.s3Storage.checkHealth(),
    },
    timeoutMs: 2000,
  },
  env.NODE_ENV,
);

Only include checks for providers your app actually installs. Readiness probes should be cheap and non-mutating: select 1, Redis PING, queue metadata, search health, storage bucket access, or a provider health endpoint. Do not run migrations, outbox drains, schedule handlers, queue consumers, imports, or backfills from readiness routes.

First-party providers expose these useful readiness helpers:

Provider areaPort or escape hatchProbe
Drizzle databasectx.ports.dbcheckHealth()
Redis cachectx.ports.redischeckHealth()
Redis event busctx.ports.redisEventBuscheckHealth()
Redis locksctx.ports.redisLockscheckHealth()
BullMQ jobsctx.ports.bullMQJobscheckHealth({ timeoutMs })
Meilisearchctx.ports.meilisearchcheckHealth()
S3-compatible storagectx.ports.s3StoragecheckHealth()

For worker readiness, expose the same kind of bounded check from the worker host or run it before accepting work. A BullMQ worker, for example, should prove Redis and the app database are reachable before it starts claiming jobs.

Cron and schedules

Use cron routes when the deployment platform owns the schedule trigger. In Next.js apps, createScheduleRoute(...) keeps the route small: it authenticates with CRON_SECRET, runs the schedule through the server request pipeline, records instrumentation, and returns a status:

// app/api/cron/digests/daily-digest/route.ts
import { createScheduleRoute } from "@beignet/next";
import { env } from "@/lib/env";
import { getServer } from "@/server";
import { schedules } from "@/server/schedules";

export const runtime = "nodejs";

export const { GET, POST } = createScheduleRoute({
  server: getServer,
  schedules,
  schedule: "digests.send-daily",
  secret: env.CRON_SECRET,
  source: "vercel-cron",
});

Use beignet schedule run when the host can run a command instead of calling HTTP, such as a release job, container worker, CI job, or scheduler with command support:

beignet schedule run digests.send-daily --scheduled-at 2026-01-01T09:00:00.000Z

Schedules are trigger definitions. When missed or failed work needs durable retry and dead-letter behavior, keep the schedule handler small and enqueue a job or write an outbox message.

Outbox drains

Use createOutboxDrainRoute(...) for deployments where a platform cron invokes HTTP. The route should drain one bounded batch and exit:

// app/api/cron/outbox/drain/route.ts
import { createOutboxDrainRoute } from "@beignet/next";
import { env } from "@/lib/env";
import { getServer } from "@/server";
import { outboxRegistry } from "@/server/outbox";

export const runtime = "nodejs";

export const { GET, POST } = createOutboxDrainRoute({
  server: getServer,
  registry: outboxRegistry,
  secret: env.CRON_SECRET,
  batchSize: 100,
});

Use the CLI when the host can run a bounded command:

beignet outbox drain --batch-size 100

Both paths use the same server/outbox.ts registry. The HTTP route creates context through the server request pipeline; the CLI path uses createOutboxDrainContext(...) from server/outbox.ts to build an app service context. A long-running worker may repeat bounded drain passes, but the loop belongs to the worker host, not provider lifecycle hooks.

Workers

Worker processes own long-running or repeated background work. They can run queue consumers, repeated outbox drains, or command schedulers, and they should still call Beignet primitives internally so app context, ports, logging, audit, and devtools instrumentation remain consistent.

For BullMQ, consume an explicit list of Beignet job definitions with createBullMQJobWorker(...):

// server/workers/jobs.ts
import { createBullMQJobWorker } from "@beignet/provider-jobs-bullmq";
import { SendWelcomeEmailJob } from "@/features/users/jobs";
import { createBackgroundContext } from "@/infra/background-context";

export const jobsWorker = createBullMQJobWorker({
  queueName: process.env.BULLMQ_QUEUE_NAME ?? "beignet-jobs",
  redisUrl: process.env.BULLMQ_REDIS_URL ?? "redis://localhost:6379/0",
  prefix: process.env.BULLMQ_PREFIX ?? "beignet",
  jobs: [SendWelcomeEmailJob],
  ctx: () => createBackgroundContext(),
  workerOptions: {
    concurrency: 5,
  },
  errorReporter: ({ ctx }) => ctx.ports.errorReporter,
});

Give workers the same provider environment they need to build app ports, plus worker-specific settings such as queue Redis URLs, concurrency, and shutdown timeouts. Handle process shutdown by closing workers so the provider stops claiming new work and gives active work a chance to finish.

Prefer the outbox when work must commit atomically with database writes. Prefer provider-backed jobs when the provider should own queueing, scheduling, invocation, and retry behavior.

Provider-backed functions

Inngest owns queueing, invocation, and retry behavior for direct Inngest-backed jobs. The Beignet provider installs ctx.ports.jobs for dispatch and exposes helpers that map Beignet job definitions to Inngest functions:

// app/api/inngest/route.ts
import { createInngestJobFunction } from "@beignet/provider-jobs-inngest";
import { serve } from "inngest/next";
import { SendWelcomeEmailJob } from "@/features/users/jobs";
import { createBackgroundContext } from "@/infra/background-context";
import { inngest } from "@/infra/inngest";

const sendWelcomeEmail = createInngestJobFunction({
  client: inngest,
  job: SendWelcomeEmailJob,
  ctx: () => createBackgroundContext(),
});

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

Treat provider-backed functions as worker entrypoints, not web request handlers. They need the same service-context decisions as other background work: actor, tenant, app ports, logging, audit, and failure reporting.

Best-effort events

The Redis event bus provider gives cross-process Pub/Sub delivery while subscribers are online. Every process that should receive an event must register its listeners in that process. If a web process publishes an event and no worker is subscribed, Redis does not replay the missed message later.

Use Redis Pub/Sub for best-effort notifications, cache invalidation, or in-process fan-out where missed messages are acceptable. Use the outbox or a job provider for workflow-critical side effects that need persistence, retry, acknowledgement, or dead-letter state.

One-off tasks

Use app tasks for backfills, repairs, imports, exports, and release maintenance:

beignet task run posts.backfill-search --input '{"dryRun":true}'

Tasks should call use cases and ports rather than reaching into infra directly. Prefer CLI entrypoints over exposed HTTP routes for work that operators or CI trigger by hand. Pass --tenant when the app's createTaskContext(...) resolves operational tenant scope separately from the task input payload.

Runtime checklist

Before shipping a runtime topology, confirm: