Going to production

Beignet gives production apps guardrails, but it does not make deployment or security automatic. Treat this page as the pre-launch checklist: run the preflight checks, validate configuration, verify host settings, wire bounded runtime entrypoints, and confirm the security posture of every exposed surface.

Preflight checks

Run these in CI before shipping:

bun run lint
bun run test
bun run typecheck
bun beignet lint
bun beignet doctor --strict

bun run lint runs Biome's code lint. Run bun run format locally before opening a change. beignet lint enforces dependency direction so sensitive infrastructure code does not leak into domain, use case, route, or component layers. doctor --strict is meant for CI: it catches production drift that is easy to miss during manual edits, such as exposed devtools routes, missing cron auth, upload definitions without explicit size limits, provider environment variables that are not configured, and credentialed wildcard CORS.

Confirm the CLI can inspect the route map:

bun beignet routes

If your app exposes OpenAPI, make sure the OpenAPI route is generated from the same route list used by the server, typically with server.contracts or contractsFromRoutes(routes). Run doctor after changing contracts or route registration.

Environment variables and secrets

Keep deploy-time configuration in lib/env.ts with @beignet/core/config and validate it at startup. Avoid reading ad hoc environment variables inside route handlers, use cases, or infra adapters. Declare server-only and client-safe variables separately:

import { createEnv } from "@beignet/core/config";
import { z } from "zod";

export const env = createEnv({
  server: {
    NODE_ENV: z.enum(["development", "test", "production"]),
    CRON_SECRET: z.string().min(32),
    BETTER_AUTH_SECRET: z.string().min(32),
    STORAGE_S3_BUCKET: z.string().min(1),
  },
  clientPrefix: "NEXT_PUBLIC_",
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: process.env,
});

Rules:

Provider credentials should be owned by the deployment environment and read through app config. Scope database and S3-compatible credentials to the app, environment, and narrowest bucket or key prefix possible, and never log, audit, or record credentials in devtools. doctor --strict checks common first-party provider environment variables when the corresponding package is installed.

Host settings

Before launch, verify the host configuration:

CORS, CSRF, and client IPs

For credentialed browser requests:

Any control keyed by client IP is only as trustworthy as the header it reads. Clients can send arbitrary x-forwarded-for values; only the entry appended by your platform's trusted reverse proxy is reliable. See Rate limiting for the trusted-proxy details, ipSource options, and key strategies.

Providers

Production providers should be installed in server/providers.ts when your app has that file, or directly in the central server setup. Provider startup and teardown belong to the application lifecycle, not route handlers.

Check these before shipping:

For durable workflows, verify each provider's failure semantics before relying on it in production: outbox adapters should preserve attempts, retry timing, leases, and dead-letter state, and job providers should document which retry behavior they own. In-memory providers are for tests, local development, or single-process apps; they are not a substitute for queues, workers, or durable outbox drains.

Runtime entrypoints

Beignet apps should run background and operational work from explicit bounded entrypoints. That keeps serverless deployments safe and gives long-running worker deployments the same app-owned context, ports, actor, tenant, logging, audit, and devtools wiring as HTTP requests.

WorkRuntime entrypointBeignet surface
API requestsThe app's catch-all route or fetch handlerserver.api, createNextServer(...), or createFetchHandler(...)
Cron-triggered schedulesPlatform cron route, scheduled function, or worker-hosted taskfeatures/<feature>/schedules/, server/schedules.ts, beignet schedule run <name>
Durable event and job deliveryCron route, worker process, queue consumer, or scheduleserver/outbox.ts, createOutboxDrainRoute(...), beignet outbox drain
One-off maintenanceLocal shell, CI job, release job, or admin workerfeatures/<feature>/tasks/, server/tasks.ts, beignet task run <name>
Provider-backed jobsProvider route or worker hostJob provider helpers such as createInngestJobFunction(...)

Do not start polling loops, queue consumers, or interval drains from provider setup or start hooks in serverless apps. Provider hooks should install ports and prepare clients. The host should decide when to invoke the work.

Every HTTP-triggered operational entrypoint can mutate production data without a browser user, so protect each one. createScheduleRoute(...) and createOutboxDrainRoute(...) authenticate with CRON_SECRET; for custom cron routes, compare Authorization: Bearer <secret> before doing work.

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, creates the app context, runs the schedule, records devtools events, and returns a status. See Schedules for the route code and failure semantics, or generate a schedule with its cron route in one step with beignet make schedule <feature>/<name> --cron "0 9 * * *" --route.

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 posts.log-daily-summary --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 Next.js deployments where a platform cron invokes HTTP. See Outbox for the drain route code, retry timing, and dead-letter handling. Use the CLI when the host can run a bounded command:

beignet outbox drain --batch-size 100

Both paths should load the same server/outbox.ts registry and context. A serverless invocation should drain one bounded batch and exit. A long-running worker may call the same command or app function repeatedly, but the loop belongs to the worker host, not provider lifecycle hooks.

Workers

Provider-backed workers should adapt Beignet definitions instead of bypassing them. For example, an Inngest route can map a Beignet job definition to an Inngest function while using an app-owned background context. Worker deployments should document which Beignet definitions they may execute, how they create actor and tenant context, and which retry semantics the provider owns.

Prefer the outbox when work must be committed atomically with database writes. Prefer provider-backed jobs when the provider should own queueing, scheduling, or retries.

Beignet does not expose a generic jobs worker command yet. Use beignet outbox drain for Beignet-owned durable rows and provider entrypoints such as Inngest functions for provider-owned queues. Add a framework job worker only when an adapter has a clear queue protocol for claiming, retrying, and shutting down work safely.

One-off tasks

Use app tasks for backfills, repair jobs, import/export work, 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, and prefer CLI entrypoints over exposed HTTP routes for work that operators or CI trigger by hand.

Devtools

Devtools are for local development by default. Production route handlers return 404 unless explicitly enabled. If you enable devtools in staging or an internal environment, add an authorize callback to the devtools route, keep event retention short, and redact sensitive fields before recording custom events. Beignet's doctor warns when a devtools route is explicitly enabled without an authorization callback. See Devtools for the route options and production-enable semantics.

Uploads and storage

Every upload definition should set explicit file constraints: allowed content types, maxSizeBytes, and visibility. Require authorize(...) on definitions that write user-owned data, keep object keys tenant- or owner-scoped, default to private visibility, and keep direct-upload expiration windows short. Beignet's doctor warns when feature-owned upload definitions omit maxSizeBytes. See Uploads and Storage for definition options and object ownership.

Logging, audit, and redaction

Logs, audit entries, and devtools events should help operators debug without leaking secrets or sensitive domain data. Use the shared redactValue and redactHeaders helpers from @beignet/core/ports for structured metadata, and store actor, tenant, request, and resource IDs instead of raw request or response bodies. Read Privacy lifecycle before launch to define retention, deletion, and "what not to log" rules, and Audit and activity logging for durable activity records.

Client base URLs

Server-side code should usually call internal functions directly. When it must use the HTTP client, pass an absolute baseUrl to createClient(...). Browser clients can use same-origin relative requests behind the deployment platform's routing layer. Keep client construction in client/ so base URL and auth behavior have one home.