SaaSyBase
SaaSyBase

Security

SaaSyBase ships with production-grade security defaults — HTTP headers, error sanitization, encryption, rate limiting, audit logging, and more. This page explains what's configured and how to extend it.

HTTP Security Headers

The app sets strict HTTP headers via next.config.mjs. These apply to every response served by Next.js:

HeaderValuePurpose
X-Frame-OptionsDENYPrevents clickjacking by blocking all framing
X-Content-Type-OptionsnosniffBlocks MIME-type sniffing
Referrer-Policyorigin-when-cross-originLimits referrer leakage
X-XSS-Protection1; mode=blockLegacy XSS protection for older browsers
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=()Limits browser API access
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS for 1 year
Content-Security-PolicyRestrictive source allowlist for scripts, frames, connections, and embedsReduces XSS and third-party injection risk while still allowing configured analytics and payment providers
Cache-Control (API routes)no-store, max-age=0Disables API response caching

Tip

These values come directly from next.config.mjs. If you introduce extra browser capabilities or embed contexts, update the headers there so the runtime behavior and docs stay aligned.

The CSP is intentionally pragmatic rather than nonce-based: SaaSyBase uses inline theme/bootstrap scripts, admin-configurable head/body snippets, analytics, Twitter embeds, and provider checkout scripts. The policy still restricts script, frame, and connect sources to a known allowlist instead of leaving those channels open.

Error Sanitization

In production, internal error details (stack traces, database errors, validation internals) are never exposed to the client. The error system works in two layers:

API error handling

Use handleApiError(error) from lib/api-error.ts in all API route catch blocks. It returns a clean JSON response with the right status code and a safe message. Sensitive details are logged server-side but stripped from the response.

API route pattern
import { handleApiError } from '@/lib/api-error';

export async function POST(req: Request) {
  try {
    // ...your logic
  } catch (error) {
    return handleApiError(error);
  }
}

Secure error responses

For finer control, use createErrorResponse() from lib/secure-errors.ts. This function:

  • Returns generic messages in production, detailed messages in development
  • Maps known error types to appropriate HTTP status codes
  • Logs sanitized error data via the secure logger

AppError classes

SaaSyBase defines typed error classes for structured error handling:

ClassStatusUse case
AppErrorConfigurableBase operational error type for safe structured failures
ApiErrorConfigurableGeneral API errors with status code
AuthenticationError401Missing or invalid credentials
AuthorizationError403Insufficient permissions
NotFoundError404Resource not found
ValidationError400Input validation failures
PaymentError402Billing or provider-related failures
RateLimitError429Too many requests

Encryption

Set ENCRYPTION_SECRET to a strong random string. This key is used for:

  • Encrypting sensitive data at rest, including reusable payment authorization codes stored for provider-managed renewals
  • Generating HMACs for internal token verification

Warning

ENCRYPTION_SECRET must stay constant across deployments. Rotating it requires re-encrypting all existing encrypted values. Generate it with openssl rand -hex 32.

Note

For real staging and production environments, keep ENCRYPTION_SECRET and the rest of your server-side secrets in platform-native encrypted envs or in the optional Infisical/Doppler bootstrap instead of shared env files. The setup guide lives in Secrets & Providers.

Password Storage & Reset Flow

When using a self-hosted credentials flow, passwords are never stored in plaintext. SaaSyBase hashes credential passwords with bcrypt using 12 salt rounds before they are written to the database.

  • Forgot-password tokens are generated with crypto.randomBytes(32)
  • Only the SHA-256 hash of the reset token is stored in the database
  • Reset links expire after 1 hour and are deleted after use or on expiration
  • Password reset increments tokenVersion and deletes active sessions to force re-authentication
  • Forgot-password and resend-verification endpoints return generic success messages to reduce email enumeration risk

Note

Clerk-managed passwords and password resets remain under Clerk's own auth stack. These details apply to the built-in Better Auth and NextAuth credentials flows.

Webhook Signature Verification

All incoming payment webhooks are signature-verified before processing. The centralized webhook router at /api/webhooks/payments auto-detects the originating provider from request headers and delegates to the correct signature verification routine.

Webhook secrets support rotation via comma-separated values:

STRIPE_WEBHOOK_SECRET="whsec_new_secret,whsec_old_secret"

During rotation, both the old and new secrets are accepted. Once you've confirmed the new secret works, remove the old one.

Rate Limiting

Rate limiting is database-backed and distributed-safe (works across serverless instances). Apply it to any API route with a single function call:

import { rateLimit, RATE_LIMITS } from '@/lib/rateLimit';

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? 'unknown';
  const limited = await rateLimit(ip, RATE_LIMITS.API_GENERAL);
  if (limited) return limited; // Returns 429 response
  // ...continue
}

If x-forwarded-for is missing, every such request falls into the same fallback bucket. In production, make sure your proxy or platform forwards real client IPs so unrelated users do not share one rate-limit identity.

SaaSyBase ships with six preconfigured tiers:

TierWindowMax requestsUse case
AUTH15 min20Login attempts and auth-sensitive endpoints
API_GENERAL15 min100Standard API endpoints
API_SENSITIVE15 min10Password changes, account deletion, other sensitive operations
CHECKOUT1 min5Checkout session creation
WEBHOOK1 min1000Incoming webhook deliveries
EXPORT1 min20Data export endpoints

Note

You can create custom rate-limit tiers for your own endpoints. The structure is { limit, windowMs, message? }. Some endpoints also use helper wrappers like adminRateLimit when actor-aware throttling is needed.

Logging & Audit Trail

Secure logger

Always use the logger from lib/logger.ts instead of console.log. The logger:

  • Auto-redacts secrets, tokens, passwords, and API keys from log output
  • Persists warnings and errors to the database for admin review
  • Provides structured info, warn, error, and debug levels
import { Logger } from '@/lib/logger';

Logger.info('User signed up', { userId: user.id });
Logger.warn('Unusual login pattern', { ip, attempts });
Logger.error('Payment webhook failed', { error, providerId });

Admin action log

All admin operations are logged to the AdminActionLog table with: the acting admin, the action performed, the target entity, a before/after snapshot, and the originating IP. These entries are viewable in the admin panel under the Activity section.

Account suspensions

SaaSyBase includes application-level suspension controls for both users and organization workspaces. These controls are enforced in the app layer, so the behavior stays consistent across Clerk, Better Auth, and NextAuth deployments.

TargetWhere admins manage itWhat happens
User/admin/usersAdmins can apply temporary or permanent suspensions. Suspended users are blocked from sign-in and existing provider-backed session resolution treats them as signed out.
Organization workspace/admin/organizationsAdmins can suspend or restore a workspace without deleting its local row. Suspended workspaces expire pending invites and display a dashboard notice while suspended.

Password policy applies to the app-managed credentials lanes. Clerk handles its own password requirements, while Better Auth and NextAuth use the local password-policy checks.

Session Activity Tracking

Active sessions are tracked with device detection and IP-based geolocation:

  • Browser name and version
  • Operating system
  • Approximate geographic location, using IPINFO_LITE_TOKEN when configured and country.is as a fallback
  • Last active timestamp

Users see their session history inside /dashboard/profileunder the Security & Data tab.

For the built-in credentials sign-in route, the session cookie is also set with HttpOnly,SameSite=Lax, and the Secure flag on HTTPS deployments.

Geolocation is best-effort. If neither provider returns a result, the session remains valid and the location field is simply left blank.

Moderator Roles

Moderators have limited admin access — they can manage specific sections without full admin privileges. The system is configurable through the admin panel:

  • Assign the MODERATOR role to any user
  • Configure which admin sections each moderator can access
  • Moderator activity is logged alongside admin actions
  • Sections like billing, system settings, and moderator management are admin-only by default

Tip

Use moderators for support staff who need to manage users, tickets, or content without having access to billing configuration or system settings.

Maintenance Mode

Toggle maintenance mode from the admin panel. When active:

  • All public pages redirect to /maintenance and show the branded maintenance screen
  • API routes return 503 Service Unavailable as JSON
  • Admin routes are exempt — admins can still access the admin panel
  • Auth routes, webhook routes, cron, health checks, and /access-denied remain accessible

The feature is DB-backed and controlled via admin settings rather than an environment variable or restart-only flag. Enforcement happens in middleware.

Because the setting lives in the shared database, changes take effect across running instances without a rebuild. The important prerequisite is that every instance points at the same database.

Password Policy

The built-in password policy enforces:

  • Minimum 8 characters
  • At least one uppercase letter, one lowercase letter, and one number
  • Configurable requirements via lib/password-policy.ts

Note

Password policy only applies to the app-managed credentials lanes. Clerk handles its own password requirements, while Better Auth and NextAuth use the local password-policy rules.

Input Validation

All API input is validated with Zod schemas defined in lib/validation.ts. Never trust raw request bodies:

import { someSchema } from '@/lib/validation';

export async function POST(req: Request) {
  const body = await req.json();
  const parsed = someSchema.parse(body);
  // parsed is now typed and validated
}

Zod validation errors are automatically caught by handleApiError() and returned as 400 Bad Request with field-level error messages.