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 checkIt 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 preflightIt 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 routesIf 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:
- Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.
- Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.
- Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.
- CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.
- Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.
- Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.
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:
- Never put secrets behind the client prefix.
- Validate required production secrets at startup.
- Prefer platform secret stores over committed
.envfiles, and keep.env.exampleuseful but empty of real values. - Use
runtimeEnvStrictwhen the host only bundles variables that are explicitly referenced. See Config for the strict runtime helpers.
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:
NODE_ENV=productionis set for production builds.- TLS is enforced by the platform.
- Preview and production environments have separate secrets and databases, and do not share writable credentials unless that is intentional.
- Cron routes receive the expected
Authorizationheader. - Build logs do not print secrets.
requestBody.maxBytesand upload routerlimitsmatch the largest expected JSON, direct-upload metadata, and server-handled multipart requests.- OpenAPI and devtools routes have the intended exposure.
- Source maps, stack traces, and error reporting settings match the team's incident response plan.
Liveness and readiness
Keep liveness and readiness separate:
/api/healthshould be cheap and local. It answers whether the process can respond./api/readyshould run bounded dependency checks. It answers whether the app should receive traffic.
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:
- Do not combine
credentials: truewithorigin: "*"ororigins: ["*"]. Use an explicit origin allow-list per environment. Beignet rejects this configuration increateCorsHooks(...), anddoctorwarns about the same pattern before deploy. - Keep cookies
HttpOnly,Securein production, andSameSite=LaxorSameSite=Strictunless your auth flow requires cross-site cookies. For cookie-backed browser mutations, installcreateCsrfHooks(...); keep provider webhooks and auth callbacks on explicitskiprules when another verifier owns the route.
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:
- Database clients and migrations are ready for the target environment.
- Cache, mail, job, auth, logging, and rate-limit providers have required env.
- Unit of Work and after-commit event behavior are tested with the real adapter.
- Dev-only providers and devtools routes are gated appropriately.
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.
Related pages
- Config for typed environment validation.
- Runtime recipes for web, cron, worker, provider-function, task, and readiness layouts.
- Schedules, Outbox, Jobs, and Tasks for the Beignet primitives those runtimes invoke.
- Authentication and Authorization for request identity and business policy.
- Rate limiting for trusted client IPs and key strategies.
- Privacy lifecycle for retention, export, deletion, anonymization, and sensitive-data boundaries.