This document describes the security posture of the BonVoyage ticketing system. It complements the security conventions in CLAUDE.md §8.
Please report suspected vulnerabilities privately to the maintainers (do not open a public issue). Include steps to reproduce and impact. We aim to acknowledge within 72 hours. Do not access, modify, or exfiltrate other users' data while testing.
- Next.js 15 (App Router) on Vercel; Supabase (Postgres + Auth + RLS) backend.
- Customer-facing pages are currently disabled; the live surface is the staff app at
/admin/*. - Payment is over-the-counter cash — the system stores no card/financial instrument data, so it is out of PCI-DSS scope.
- AuthN: Supabase Auth (email + password). Sessions are cookie-based and refreshed in
middleware.ts. There is no public sign-up — staff accounts are provisioned only by a superadmin (/admin/users) or thescripts/create-superadmin.mjsbootstrap. - AuthZ: role-based, enforced in two layers:
- Database RLS — every table has Row Level Security enabled. Role checks go through
has_staff_role(...);superadminsatisfies every check (wildcard). - App route guards —
requireStaff,requireManifestAccess,requireSuperadmin(lib/auth/roles.ts) gate server components and actions.
- Database RLS — every table has Row Level Security enabled. Role checks go through
- Roles:
cashier(booth agent),purser(manifests),ops,admin,superadmin. Least privilege: a cashier can sell tickets and view the manifest of a voyage via aSECURITY DEFINERRPC, but has no blanket read of the passenger PII table.
- Passenger PII (manifest data in
booking_passengers) is regulated under the Philippine Data Privacy Act of 2012 (RA 10173). Direct table reads are restricted by RLS topurser/ops/admin/superadmin; booth agents reach only per-voyage data through a scoped RPC. Minimize collection, and define a retention/disposal policy before go-live. - No PII in URLs or logs. The customer booking-lookup flow keeps the contact email in an httpOnly cookie rather than the query string.
- Secrets.
SUPABASE_SERVICE_ROLE_KEYandTICKET_SIGNING_SECRETare server-only and must never be prefixedNEXT_PUBLIC_or imported into client code. Only the Supabase URL and anon key are public. Secrets live in environment variables (Vercel /.env.local, which is git-ignored).
Targets securityheaders.com Grade A. Set in next.config.ts + middleware.ts:
| Header | Value |
|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
Content-Security-Policy |
strict, per-request nonce, no unsafe-inline |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY (+ CSP frame-ancestors 'none') |
Permissions-Policy |
camera/mic/geo/payment/topics disabled |
Accepted exception: Referrer-Policy is intentionally omitted per project requirement.
This is a deliberate, documented deviation (CLAUDE.md §8); add
strict-origin-when-cross-origin if a stricter grade is later required.
- No overselling. All seat holds go through an atomic, conditional
UPDATE ... WHERE remaining >= ninsideSECURITY DEFINERSQL functions (create_reservation,create_walkin_booking) in a single transaction — never a read-then-write in app code. - Input validation. Every server action and route handler validates input with Zod before use. Treat all client input as hostile.
- SQL safety. Data access goes through the Supabase client / parameterized RPCs; no string-concatenated SQL.
- Payments. Over-the-counter payments are recorded with the cashier's id and the OR (Official Receipt) number for auditability.
- Rate limiting. Apply rate limits to auth and ticket-selling endpoints (e.g. Upstash Redis) before production.
- Service-role usage. The service-role client (
lib/supabase/admin.ts) bypasses RLS and is used only for trusted server tasks (user provisioning, scheduled jobs). Keep its blast radius small and audited. - Dependencies. Keep
next,@supabase/*, and other deps patched; reviewpnpm auditoutput regularly. Note:@supabase/ssrmust stay compatible with the installed@supabase/supabase-js(typed-client generics). - Backups & least privilege. Use Supabase's automated backups; grant database/admin access on a need-to-have basis; rotate keys if exposure is suspected.
The main branch receives security fixes. Pin and update dependencies via the lockfile.