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 (
userscollection). Frontend pages are public or tenant-scoped; Better Auth powers customer/dashboard sessions on thecustomerscollection. - Payload Local API — Bypasses access control by default. Every call that passes a
userin this codebase setsoverrideAccess: 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_KEYis 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_KEYenv var.
Cookie scoping (multi-tenant)
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. - Cron —
CRON_SECRET(when configured) protects scheduled jobs invoked from external schedulers.
Optional next steps
- Middleware — add
middleware.tsto redirect unauthenticated users away from/dashboardand authenticated users away from/auth. - Rate limiting — add rate limiting (e.g. Upstash) for public mutations (newsletter, lead forms, generation calls).
- Security headers —
X-Frame-Options,X-Content-Type-Options, optional CSP innext.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.