Security Baseline
Default protections for startup env validation, security headers, error boundaries, and API error handling.
MuseMVP ships with a set of security defaults out of the box: environment validation at server startup, security response headers on every route, layered error boundaries, unified API error handling, plus hardening for billing webhooks and email flows. All of these are enabled by default with no extra configuration.
Design Principle
Misconfiguration in production should fail fast at startup instead of surfacing later as a confusing runtime error mid-request; non-fatal issues degrade to warning logs and never block a deployment.
Startup Environment Validation
Server-side environment variables are validated by a Zod schema in src/lib/env.ts, invoked once at server startup via register() in src/instrumentation.ts.
- Hard error: a production runtime missing
BETTER_AUTH_SECRETorMUSE_SETTINGS_SECRETthrows immediately and aborts startup.MUSE_SETTINGS_SECRETmust be at least 32 characters; it wraps payment and storage secrets in the database.next buildand local dev are unaffected — errors are only logged there. - Non-fatal warnings: the following misconfigurations only emit warning logs and never block startup.
| Scenario | Warning Condition | Impact |
|---|---|---|
| Database connection | DATABASE_CONNECTION_STRATEGY=database_url_first but DATABASE_URL is not set | Database connections fail |
| Email service | MAIL_PROVIDER=resend but RESEND_API_KEY is not set | Transactional email fails |
Security Response Headers
The headers() function in next.config.ts sends the following security headers on every route (/:path*):
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | Enforce HTTPS |
X-Frame-Options | SAMEORIGIN | Block cross-site iframe embedding (clickjacking) |
X-Content-Type-Options | nosniff | Disable MIME type sniffing |
Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leakage |
Permissions-Policy | camera=(), microphone=(), geolocation=(), browsing-topics=() | Disable sensitive browser capabilities by default |
About CSP
A Content-Security-Policy is intentionally not included yet: Next.js inline scripts require per-request nonces, and third-party scripts (analytics, Turnstile, Google One Tap) need explicit allow-listing. It is planned as a dedicated follow-up.
Error Boundaries
The App Router uses layered error boundaries so an uncaught exception never results in a blank screen:
Global Fallback
src/app/global-error.tsx — Last line of defense; replaces the root layout, renders its own html/body with inline styles and non-localized copy.
Landing Page Boundary
src/app/(landing-page)/[locale]/error.tsx — Catches exceptions within the marketing segment and renders the shared fallback.
App Boundary
src/app/(saas-page)/app/error.tsx — Catches exceptions within the SaaS app segment and renders the shared fallback.
Both segment boundaries share the ErrorState fallback component, offering Try again and Back to home actions, with copy maintained in the i18n errors namespace (en/zh).
API Error Handling
The Hono API (src/backend/api/app.ts) handles errors and request body size at the entry layer:
Unified error shape: app.onError catches every exception that escapes a handler, logs it, and returns a unified JSON error structure ({ ok: false, error: "Internal Server Error" }) without leaking internal details to clients.
HTTPException passthrough: an HTTPException thrown deliberately by a route keeps its own response and is never overwritten.
Request body limit: hono/body-limit caps request bodies at 1MB, returning 413 when exceeded. File uploads go directly to object storage via presigned URLs and are unaffected.
Billing and Email Hardening
- Enforced webhook signature verification: in production, Creem, Stripe, and Dodo webhooks require a webhook secret stored in Admin → Runtime settings and throw immediately when it is missing. Waffo does not use a webhook secret;
/api/muse-billing/notify/muse_waffoverifiesx-waffo-signaturewith@waffo/pancake-tsverifyWebhookand rejects unsigned or invalid payloads even in development. - Encrypted runtime settings: payment and storage secrets are encrypted at rest with HKDF-SHA256 + AES-256-GCM. Each field derives its own key (
infoand AAD bound toscope:key). Ciphertext is unprefixed base64url;cipher_versionrecords the algorithm.MUSE_SETTINGS_SECRETis never stored in the database. See Runtime Settings. - Newsletter unsubscribe tokens: unsubscribe link tokens are signed with HMAC-SHA256 and verified with a constant-time comparison, making them unforgeable — this also fixed a previous reflected XSS issue. The signing secret is the Newsletter tab in Admin → Runtime settings, and falls back to
BETTER_AUTH_SECRETwhen that field is empty. - Auth emails report real failures: when an OTP, magic link, or password-reset email fails to send, the client receives a real error instead of a silent success that leaves the user waiting for an email that never arrives.