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 the validation loop in CI before shipping:

bun beignet check

It runs beignet lint, beignet doctor --strict, and the app's lint, typecheck, and test scripts in one pass, and exits non-zero when any step fails. 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 the CI bar: 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. For GitHub-annotation output, run beignet lint --format github and beignet doctor --strict --format github individually.

In the deploy pipeline itself — where production configuration is present — run the runtime gate:

bun beignet preflight

It validates the actual environment: required provider env vars, secrets still matching .env.example placeholders, the app env schema, doctor's production hardening findings, and logging/error-reporting wiring. Add --connect to boot the server and run every port's checkHealth() so a bad credential fails the deploy instead of the first request, or run it as part of the loop with beignet check --preflight.

Confirm the CLI can inspect the route map:

bun beignet routes

If your app exposes OpenAPI, make sure the OpenAPI route imports the static contractsFromRoutes(routes) export from server/routes.ts. Run doctor after changing contracts or route registration.

Production hardening checklist

When doctor reports production-readiness diagnostics, use this checklist before launch:

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:

Liveness and readiness

Keep liveness and readiness separate:

Generated apps expose both endpoints. The readiness route uses createHealthRoute(...) from @beignet/next and provider-owned checks such as ctx.ports.db.checkHealth():

// 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(),
    },
    timeoutMs: 2000,
  },
  env.NODE_ENV,
);

Readiness checks should be cheap, non-mutating probes such as select 1, Redis PING, queue health checks, or a provider health endpoint. Do not run migrations, drains, workers, or polling loops from readiness routes. doctor only checks for the presence of app-owned readiness wiring; it does not make network calls. First-party Drizzle, Redis cache, Redis event bus, Redis locks, BullMQ jobs, Meilisearch, and S3-compatible storage providers expose explicit checkHealth() helpers for this purpose.

Security headers, CORS, CSRF, and client IPs

Install the browser response-header baseline in server/index.ts:

import { createSecurityHeadersHooks } from "@beignet/core/server";

const hooks = [
  createSecurityHeadersHooks<AppContext>({
    contentSecurityPolicy: "default-src 'self'; frame-ancestors 'none'",
    strictTransportSecurity:
      env.NODE_ENV === "production"
        ? { maxAgeSec: 31_536_000, includeSubDomains: true }
        : false,
  }),
];

The hook adds common response headers and preserves route-owned headers. Keep CSP and HSTS explicit because asset hosts, embedded frames, redirects, and HTTPS rollout are app-owned deployment decisions. doctor warns when the central server does not install createSecurityHeadersHooks(...).

For credentialed browser requests:

import { createCsrfHooks } from "@beignet/core/server";

const csrf = createCsrfHooks<AppContext>({
  allowMissingOrigin: false,
  trustedProxy: {},
  trustedOrigins: ["https://app.example.com"],
  token: {
    cookieName: "csrf",
    headerName: "x-csrf-token",
  },
  skip: ({ contract }) => contract.name.startsWith("webhooks."),
});

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. Configure an explicit trustedProxy.clientIp for IP-scoped limits; otherwise Beignet requires a custom earlyKey or explicit ipSource: "none" opt-out. The same trustedProxy policy lets createCsrfHooks(...) compare browser origins against external forwarded host and protocol values. See Rate limiting for the trusted-proxy details 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.

Use beignet providers add <preset> for supported provider recipes such as OpenFeature flags, mail, Meilisearch search, Sentry error reporting, Upstash rate limiting, Redis cache, Redis event bus, Redis locks, and S3-compatible storage. The command writes dependency, provider, port, env, and setup-note changes that beignet providers audit and beignet doctor --strict can inspect.

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. Mail providers intentionally fail fast because sends are not idempotent; retry mail through jobs or outbox rows that own idempotency. Redis Pub/Sub event delivery is best-effort and cross-process, but it does not persist, replay, acknowledge, retry, or dead-letter event messages. S3-compatible storage delegates bounded transient retries to the AWS SDK when the provider creates the client; injected clients keep their own retry configuration.

Runtime recipes

Background and operational work should run from explicit bounded entrypoints. The web process should serve HTTP, cron routes should trigger one unit of work, workers should own repeated or queue-backed work, provider-backed function hosts should own provider invocations, and tasks should run from operator or CI commands.

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

See Runtime recipes for the concrete layouts: web-only apps, cron routes, outbox drains, BullMQ workers, Inngest functions, beignet schedule run, beignet outbox drain, beignet task run, and provider readiness checks for Redis, S3-compatible storage, search, queues, and databases.

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. Uploads are protected by default, so add authorize(...) unless the workflow is intentionally public and declares access: "public". 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 or authorization. See Uploads and Storage for definition options, signature verification, checksums, scanner hooks, and object ownership. Public storage routes set nosniff and download active content types by default; keep that default unless the app intentionally serves active public assets from the application origin.

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.