Production security

Beignet gives production apps a set of guardrails, but it does not make security automatic. Treat this page as the pre-deploy security checklist for a Beignet app.

Run the CLI checks first:

bunx -p @beignet/cli beignet lint
bunx -p @beignet/cli beignet doctor --strict

beignet lint checks dependency direction so sensitive infrastructure code does not leak into domain, use case, route, or component layers. doctor --strict catches common production drift 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.

Environment variables and secrets

Keep deployment configuration in lib/env.ts with @beignet/core/config. 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:

CORS and CSRF

CORS controls which origins can read browser responses. CSRF controls whether a browser can be tricked into sending a state-changing request with the user's cookies.

For credentialed browser requests:

Beignet's doctor warns about credentialed wildcard CORS in route, server, and infra files.

Cron routes and operational entrypoints

Cron routes, outbox drains, schedules, and operational commands can mutate production data without a browser user. Protect every HTTP-triggered operational entrypoint.

Use CRON_SECRET for platform cron routes:

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

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

For custom cron routes, compare Authorization: Bearer <secret> before doing work. For worker hosts or CI jobs, prefer CLI entrypoints such as beignet outbox drain, beignet schedule run, and beignet command run so the work does not need an exposed HTTP route.

Do not start background loops from provider lifecycle hooks in serverless apps. Use bounded cron, queue, schedule, or worker entrypoints instead.

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 production environment:

export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
  basePath: "/api/devtools",
  enabled: env.DEVTOOLS_ENABLED,
  authorize: (req) =>
    req.headers.get("x-devtools-token") === env.DEVTOOLS_TOKEN,
});

Beignet's doctor warns when a devtools route is explicitly enabled without an authorization callback.

Uploads and storage

Every upload definition should set explicit file constraints:

export const profilePhotoUpload = defineUpload("profile.photo", {
  file: {
    contentTypes: ["image/jpeg", "image/png"],
    maxSizeBytes: 2 * 1024 * 1024,
    visibility: "private",
  },
  // metadata, authorize, key, and onComplete belong here
});

Production upload rules:

Beignet's doctor warns when feature-owned upload definitions omit maxSizeBytes.

Provider credentials

Provider packages adapt external systems, so credentials should be owned by the deployment environment and read through app config.

Check provider setup before launch:

doctor --strict checks common first-party provider environment variables when the corresponding package is installed.

Logging, audit, and redaction

Logs, audit entries, and devtools events should help operators debug without leaking secrets or sensitive domain data.

Read Privacy lifecycle before production launch to define retention, export, deletion, anonymization, redaction, and "what not to log" rules across app-owned data.

Use the shared redaction helpers for structured metadata:

import { redactHeaders, redactValue } from "@beignet/core/ports";

log.info("Provider response", redactValue(providerPayload));
log.info("Request headers", { headers: redactHeaders(request.headers) });

Default redaction hides secret-shaped keys such as authorization, cookie, set-cookie, x-api-key, token, password, secret, and credentials. Your app still owns domain-specific redaction for PHI, financial data, private messages, access tokens embedded in URLs, and other sensitive fields.

For audit logs:

Deployment settings

Before launch, verify the host configuration:

Related pages