The security headers themselves are not documented here. next.config.ts sets
the static headers (HSTS, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, Permissions-Policy); the root middleware.ts builds the
Content-Security-Policy and the per-request nonce. Read those files for the
current values — a table here would only drift from them. How to author a CSP
change (production-branch-first, what is already allowlisted, the x-nonce
contract) is in the pinpoint-security skill; the enforced rules are
CORE-SEC-* and CORE-SSR-* in docs/NON_NEGOTIABLES.md.
What follows is the part that lives nowhere else: choices made against a specific threat, and the gaps we know we have.
The 'strict-dynamic' directive allows scripts loaded by nonce'd scripts to execute without their own nonce. This is necessary for:
- Next.js dynamic imports
- Third-party scripts loaded by trusted code
- Client-side routing and code splitting
Trade-off: Slightly relaxes CSP but required for modern JavaScript frameworks. The initial script must still have a valid nonce.
Supabase project-level captcha protection must remain disabled for the project. Because PinPoint does not send captcha tokens with authentication or report requests, enabling captcha at the Supabase project level will cause auth requests to fail with captcha_failed.
PinPoint is a single-tenant application with no multi-tenancy and no database-level Row Level Security (RLS) enforcement on application queries:
- Drizzle connects via
POSTGRES_URLwithBYPASSRLS: The Drizzle database user hasBYPASSRLS. Database RLS policies are not evaluated for application queries, and the database does not filter rows based on session context. - Application endpoints and page loaders form the primary application authorization perimeter: Because the database does not enforce row security for application queries, all request entry points—Server Components and page data loaders (
src/app/**), Server Actions ("use server"), Route Handlers (src/app/api/**), and MCP endpoints—must enforce authorization at the application layer before disclosing restricted data or persisting mutations. - Layered endpoint authorization:
- Server Components & Page Loaders: Layouts and data-loaders query Drizzle directly with
BYPASSRLS. Restricted layouts (such assrc/app/(app)/admin/layout.tsx) gate rendering by checking permissions, and resource-specific pages apply access predicates (e.g.canViewCollection) before disclosing entity data. - Protected Server Actions: Non-anonymous Server Actions must authenticate the user (
getUser()). Actions exercising role- or matrix-governed capabilities verify authorization viacheckPermission()againstsrc/lib/permissions/matrix.ts(or domain predicates undersrc/lib/permissions/), while pre-auth actions (loginAction,signupAction,forgotPasswordAction) remain intentionally open. - Route Handlers: Enforce route-specific authorization guards appropriate to the surface, such as HMAC token verification (
/api/unsubscribe), CRON secret headers viaassertCronAuthorized()(/api/cron/*), or webhook signature checks. - MCP Tools: Protected by a layered gate—HTTP-level admin bearer token authentication (
withMcpAuthinsrc/app/api/mcp/mcp/route.ts) before dispatch, followed by per-tool permission and ownership checks.
- Server Components & Page Loaders: Layouts and data-loaders query Drizzle directly with
- SECURITY DEFINER RPCs (Database-level boundary): Postgres functions returning secrets (e.g.,
get_discord_config(),get_pinballmap_credentials()) are exposed directly to HTTP callers over PostgREST (POST /rest/v1/rpc/<name>). Because Supabase re-grantsEXECUTEonpublic.*functions toauthenticatedat connection time,REVOKE/GRANTalone is insufficient defense in depth. These functions enforce an explicit database-level authorization boundary via an in-bodyIF COALESCE(auth.role(), '') <> 'service_role' THEN RAISE EXCEPTION ...check (see migrations 0029 and 0062) to prevent authenticated members from retrieving Vault secrets. - Client and UI checks are UX affordances only: Client-side checks (
canEdit,getPermissionDeniedReason,getPermissionState) exist only to drive UI state (e.g., disabling buttons or rendering explanatory messages). They are never security boundaries.
- CSRF: Requires additional token-based protection (not yet implemented)
- SQL Injection: Prevented by Drizzle ORM parameterization (separate concern)
- Abuse on non-auth surfaces: Rate limiting is not comprehensive across the
app. Auth actions are protected — IP + account rate limiting via
~/lib/rate-limit— but other surfaces are not comprehensively rate-limited. - DDoS: Requires infrastructure-level protection
- CSS injection:
style-srckeeps'unsafe-inline'for CSS-in-JS and Next.js server-rendered styles, so inline styles are permitted. This is a deliberate weakening, not an oversight; tightening it to a nonce would mean proving every component's inline styles carry one first.