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:
- Never put secrets behind the client prefix.
- Validate required production secrets at startup.
- Prefer platform secret stores over committed
.envfiles. - Rotate provider credentials outside the app deploy when possible.
- Keep
.env.exampleuseful but empty of real values. - Use
runtimeEnvStrictwhen the host only bundles variables that are explicitly referenced.
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:
- Do not combine
credentials: truewithorigin: "*"ororigins: ["*"]. - Use an explicit origin allow-list per environment.
- 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 that validates an origin, referer, or CSRF token.
- Prefer bearer tokens or same-origin requests for API clients when that fits the product.
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:
- Add an
authorizecallback to the devtools route. - Use a secret, session check, VPN, or internal network control.
- Keep persistence paths outside public static directories.
- Keep event retention short.
- Redact app-specific sensitive fields before recording custom events.
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:
- Require
authorize(...)on upload definitions that write user-owned data. - Keep object keys tenant- or owner-scoped.
- Use private visibility by default.
- Set direct-upload expiration windows as short as the product allows.
- Persist durable attachment rows in app-owned repositories.
- Treat virus scanning, moderation, quarantine, and retention as app/provider responsibilities.
- Do not expose raw provider errors or credentials through upload completion responses.
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:
- Database credentials are scoped to the app and environment.
- Redis, Upstash, S3/R2, Resend, SMTP, Inngest, Better Auth, and other provider
secrets are present in
lib/env.tsor.env.example. - Provider credentials are not logged, audited, or recorded in devtools.
- S3-compatible credentials use the narrowest bucket/key prefix possible.
- Production and preview environments do not share writable credentials unless that is intentional.
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:
- Store actor, tenant, request, resource, and action metadata.
- Avoid raw request/response bodies.
- Avoid storing secrets, tokens, passwords, or full provider payloads.
- Define retention and deletion behavior before production launch.
Deployment 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.
- 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.
Related pages
- Config for typed environment validation.
- Deployment for runtime entrypoints and preflight commands.
- Authentication and Authorization for request identity and business policy.
- Uploads and Storage for file constraints and object storage.
- Audit and activity logging for actor, tenant, redaction, and retention patterns.
- Privacy lifecycle for retention, export, deletion, anonymization, and sensitive-data boundaries.
- Devtools for local diagnostics and production-enable semantics.