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:
authenticatedfor write,authenticatedOrPublishedor tenant-scoped for read where applicable. - Media: create/update/delete require
authenticated; read isanyone(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
maxLoginAttemptsandlockTimeso repeated failed logins lock the account temporarily. - Max depth: Payload config sets
maxDepthto 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
corsinpayload.config.tsto 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
accessor add file scanning in hooks.
Secrets and environment
- Validation: Server and client env are validated with
@t3-oss/env-nextjsinsrc/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.tsto redirect unauthenticated users away from/dashboardand authenticated users away from/authfor 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 innext.config.jsfor stronger browser security.