Skip to Content
Security

Security

This page documents the security measures already configured in ShipMore. For Payload-specific hardening, 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; Better Auth powers customer/dashboard sessions on the customers collection.
  • Payload Local API — Bypasses access control by default. Every call that passes a user in this codebase sets overrideAccess: false.
  • Secrets — Server-only env validation via @t3-oss/env-nextjs; no client secrets; webhooks verified with signing secrets.
  • CLI auth — operator-issued API keys, sent as bearer tokens by the CLI and MCP. No tokens on disk; SHIPMORE_API_KEY is read from the env var only.

Payload

Access control

Every collection that holds sensitive data defines explicit read, create, update, and delete access:

  • Public assets like media are readable by anyone (they’re served on the public site); writes require authentication.
  • Everything else is authenticated or tenant-scoped as appropriate — write access requires a signed-in operator, and reads are gated to published content or the operator’s own tenant.

Local API: overrideAccess is non-negotiable

The Local API skips access control by default. When passing user to a Payload call, always set overrideAccess: false:

// ✅ enforces user permissions await payload.find({ collection: 'pages', user, overrideAccess: false }) // ❌ access control bypassed — runs with admin privileges await payload.find({ collection: 'pages', user })

For intentional admin operations (scripts, seed data, revalidation hooks), omit user entirely — overrideAccess defaults to true with no user.

Transaction safety in hooks

Nested Payload operations inside hooks run on the same request transaction, so a hook’s writes commit or roll back atomically with the change that triggered them. Preserve this when extending hooks.

Hook loop prevention

When a hook writes back to the same document it’s reacting to, a context flag short-circuits the second pass so the hook can’t recurse into itself.

Preventing abuse

Aligned with Payload: Preventing Production API Abuse :

  • Failed login lockout — repeated failed admin logins lock the account for a cooldown window.
  • Max depth — relationship query depth is capped to avoid circular queries and timeouts.
  • GraphQL — disabled; the app uses Local API + REST + MCP only, which shrinks the abuse surface.
  • CSRF — Payload’s cookie-based auth includes CSRF protection.
  • CORS — configurable if you call the API from another origin.
  • Uploads — Media writes are authenticated; reads are public. Tighten or add file scanning in hooks if needed.

Auth

Better Auth (frontend customers)

  • Sessions are read server-side from the request headers via the auth domain’s session helper.
  • The base URL is inferred per request — critical for multi-tenant.
  • Server actions are guarded by composable procedures: a base auth guard, a Stripe-customer guard, and a paywall guard (see Auth).

CLI / agent auth

  • Operators issue themselves an API key from the Payload admin.
  • The CLI and MCP clients authenticate with Authorization: Bearer <key>.
  • The key never touches disk on the agent’s machine — it’s read only from the SHIPMORE_API_KEY env var.

Auth cookies are scoped to the domain they were set on. A customer signed in on tenant-a.com is not signed in on tenant-b.com — this is expected browser behavior and provides proper tenant isolation.


Secrets and environment

  • Validation — server and client env are validated with @t3-oss/env-nextjs. Server-only variables (PAYLOAD_SECRET, DATABASE_*, STRIPE_*, RESEND_API_KEY, …) are never exposed to the client.
  • Webhooks — Stripe webhooks verify signatures using STRIPE_WEBHOOKS_ENDPOINT_SECRET; secrets stay server-side.
  • CronCRON_SECRET (when configured) protects scheduled jobs invoked from external schedulers.

Optional next steps

  • Middleware — add middleware.ts to redirect unauthenticated users away from /dashboard and authenticated users away from /auth.
  • Rate limiting — add rate limiting (e.g. Upstash) for public mutations (newsletter, lead forms, generation calls).
  • Security headersX-Frame-Options, X-Content-Type-Options, optional CSP in next.config.ts.
  • More sign-in options — magic-link sign-in is scaffolded (wire its sender to your email system to enable it), and Better Auth can layer in additional social/SSO providers as needed.