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 --strictbun 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 routesIf 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:
- 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.
- Upload body limits match the largest allowed Beignet upload definition.
- OpenAPI and devtools routes have the intended exposure.
- Source maps, stack traces, and error reporting settings match the team's incident response plan.
CORS, CSRF, and client IPs
For credentialed browser requests:
- Do not combine
credentials: truewithorigin: "*"ororigins: ["*"]. Use an explicit origin allow-list per environment. Beignet's doctor warns about credentialed wildcard CORS. - Keep cookies
HttpOnly,Securein production, andSameSite=LaxorSameSite=Strictunless your auth flow requires cross-site cookies. For cross-site cookie flows, add app-owned CSRF protection through your auth provider or a route hook.
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:
- 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.
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.
| Work | Runtime entrypoint | Beignet surface |
|---|---|---|
| API requests | The app's catch-all route or fetch handler | server.api, createNextServer(...), or createFetchHandler(...) |
| Cron-triggered schedules | Platform cron route, scheduled function, or worker-hosted task | features/<feature>/schedules/, server/schedules.ts, beignet schedule run <name> |
| Durable event and job delivery | Cron route, worker process, queue consumer, or schedule | server/outbox.ts, createOutboxDrainRoute(...), beignet outbox drain |
| One-off maintenance | Local shell, CI job, release job, or admin worker | features/<feature>/tasks/, server/tasks.ts, beignet task run <name> |
| Provider-backed jobs | Provider route or worker host | Job 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.000ZSchedules 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 100Both 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.
Related pages
- Config for typed environment validation.
- Schedules, Outbox, Jobs, and Tasks for the runtime entrypoints this page wires up.
- 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.