Skip to Content
Security

Security

This page documents the security measures already configured in this boilerplate. For Payload-specific hardening (login limits, depth, CSRF, CORS, GraphQL, uploads), see Payload: Preventing Production API Abuse .


Overview

  • Admin vs frontend: Admin is protected by Payload auth (Users collection). Frontend pages are public or tenant-scoped; optional Better Auth powers customer/dashboard sessions.
  • Payload: Restrictive access by default; Local API calls that pass a user always use overrideAccess: false.
  • Secrets: Server-only env validation; no client secrets; webhooks verified with signing secrets.

Payload

Access control

Every collection that holds sensitive data has access defined (read, create, update, delete):

  • Tenants, Pages, Posts, Categories: authenticated for write, authenticatedOrPublished or tenant-scoped for read where applicable.
  • Media: create/update/delete require authenticated; read is anyone (public assets).
  • Users, Customers, Leads, Products: authenticated or admin-only as needed.

Access helpers live in src/payload/access/ (e.g. authenticated, anyone, authenticatedOrPublished).

Local API and overrideAccess

The Local API skips access control by default. Whenever you pass user into a Payload call (e.g. from the frontend or tenant context), you must set overrideAccess: false so permissions are enforced. This boilerplate does that in:

  • Frontend pages: [tenantDomain]/posts, [tenantDomain]/[slug], [tenantDomain]/layout, sitemaps.
  • Tenant resolution: src/lib/tenant.ts, src/payload/utils.ts.

Server-side code that intentionally runs as admin (e.g. revalidation hooks, newsletter signup) uses overrideAccess: true and does not pass a user.

Preventing abuse (Payload config)

Aligned with Payload: Preventing Production API Abuse :

  • Failed login limits: Users collection has maxLoginAttempts and lockTime so repeated failed logins lock the account temporarily.
  • Max depth: Payload config sets maxDepth to limit relationship depth and avoid deep/circular queries and timeouts.
  • GraphQL: GraphQL is disabled (graphQL.disable: true) because this app uses the Local API and REST; disabling reduces surface area and complexity limits.
  • CSRF: Payload’s cookie-based auth includes CSRF protection; see Payload cookies & CSRF .
  • CORS: If you use the Payload API from another origin (e.g. a separate frontend), set cors in payload.config.ts to an allowlist of origins (see Payload configuration ).
  • Uploads: Media has create/update/delete restricted to authenticated users; read is public. For stricter control, tighten Media access or add file scanning in hooks.

Secrets and environment

  • Validation: Server and client env are validated with @t3-oss/env-nextjs in src/env.ts. Server-only variables (e.g. PAYLOAD_SECRET, DATABASE_*, STRIPE_*) are never exposed to the client.
  • Webhooks: Stripe webhooks verify signatures using STRIPE_WEBHOOKS_ENDPOINT_SECRET; secrets stay server-side.

Auth (Better Auth and procedures)

  • Session: Better Auth is integrated with Payload; session is read from request headers via payload.betterAuth.api.getSession({ headers }).
  • Procedures: Server actions use zsa procedures in src/lib/procedures.ts: authProcedure (session required), stripeCustomerProcedure, paywallProcedure. Protected routes (e.g. dashboard, auth pages) check session at the page or layout level.

Optional next steps

  • Middleware: Add middleware.ts to redirect unauthenticated users away from /dashboard and authenticated users away from /auth for clearer route protection.
  • Rate limiting: Add rate limiting (e.g. Upstash) for public mutations (newsletter, lead forms) to limit abuse.
  • Security headers: Add X-Frame-Options, X-Content-Type-Options, and optionally CSP in next.config.js for stronger browser security.