Skip to content

Latest commit

 

History

History
374 lines (302 loc) · 17.5 KB

CLAUDE.md

File metadata and controls

374 lines (302 loc) · 17.5 KB

CLAUDE.md

Guidance for building and maintaining this project. Read this before writing code, and keep it current as decisions change.


1. Project Overview

A ferry / sea-travel ticketing system for the Philippines. Passengers browse voyages, reserve seats online, and pay over-the-counter (OTC) at a terminal or partner agent. Staff confirm payment, issue e-tickets, and manage voyages and Coast Guard manifests.

Scale target: ~20,000 passengers/day sustained, with large seasonal peaks (Holy Week, Christmas, Undas/All Saints') that can reach ~10× normal load. Design every read path for caching and every write path for correctness under concurrency.

Guiding principles — in priority order when they conflict:

  1. Correctness — never oversell a voyage; never lose a confirmed booking.
  2. Security & privacy — passenger PII is regulated (RA 10173).
  3. Accessibility — WCAG 2.1 AA is a requirement, not a nice-to-have.
  4. Performance — fast on low-end Android phones over spotty mobile data.
  5. Offline resilience — boarding passes must work without signal at the port.

2. Tech Stack

Concern Choice Notes
Framework Next.js 15 (App Router, RSC) Server Components by default; "use client" only when needed.
UI runtime React 19
Language TypeScript (strict: true) No any; no non-null ! to silence the compiler.
Database / Auth / Storage Supabase (Postgres + Auth + RLS + Storage + Realtime) RLS on for every table.
DB access @supabase/ssr Cookie-based SSR auth; separate server/browser/middleware clients.
Connection pooling Supavisor (transaction mode) Required for serverless; never hold long-lived connections.
Server cache / data TanStack Query v5 Client caching, mutations, optimistic updates.
Data grids TanStack Table v8 Admin: manifests, voyages, bookings. Server-side pagination/sort/filter.
Styling Tailwind CSS v4
Components shadcn/ui (Radix primitives) Accessible by default; prefer over hand-rolled widgets.
Validation Zod One schema per boundary, shared between client and server.
Forms React Hook Form + Zod resolver
PWA Serwist Maintained successor to next-pwa. Service worker + offline e-tickets.
Dates/TZ Luxon (or date-fns-tz) All voyage times in Asia/Manila. Store UTC, render Manila.
Errors / analytics Sentry + Vercel Analytics + Speed Insights
Rate limiting Upstash Redis (optional) Reservation + auth endpoints.
Hosting Vercel Edge CDN; security headers via next.config + middleware.
Package manager pnpm
Quality gates ESLint, Prettier, Husky, lint-staged, Conventional Commits
Testing Vitest + Testing Library, Playwright, RLS policy tests

When adding a dependency, prefer the options above. If you must introduce something new, note why here.


3. Directory Structure

app/
  (public)/            # Passenger booking flow (search → select → reserve → ticket)
  (admin)/             # Staff: voyages, inventory, bookings, manifests, OTC confirmation
  api/                 # Route handlers (webhooks, QR validation, cron triggers)
  layout.tsx           # Root layout; injects CSP nonce
  manifest.ts          # PWA web app manifest
components/
  ui/                  # shadcn/ui primitives
  <feature>/           # Feature components
lib/
  supabase/            # server.ts, client.ts, middleware.ts (three clients — do not mix)
  validation/          # Zod schemas shared client/server
  query/               # TanStack Query client, query keys, hooks
  utils/               # money, datetime (Asia/Manila), qr
db/
  migrations/          # SQL migrations (source of truth; checked into git)
  policies/            # RLS policies
  functions/           # SQL functions (atomic inventory ops, hold expiry)
tests/
  unit/  e2e/  rls/
middleware.ts          # CSP nonce + auth session refresh + security headers
next.config.ts         # Static security headers, image config

4. Domain Model

Core entities (all tables: id uuid pk, created_at, updated_at, RLS enabled):

  • ports — name, UN/LOCODE or local code, city/province, coordinates.
  • vessels — name, type (fastcraft | roro | ferry), operator, accommodation config, total capacity.
  • routes — origin_port_id, destination_port_id, distance, typical duration.
  • voyages — scheduled sailings: route_id, vessel_id, departure_at (UTC), arrival_at, status (scheduled | boarding | departed | cancelled).
  • accommodation_classes — economy | tourist | cabin | business (per vessel).
  • voyage_inventory — (voyage_id, class_id) → capacity, remaining. The single source of truth for availability. All holds/releases mutate remaining atomically.
  • bookings — reference (human-readable), status (pending → confirmed → cancelled | expired), contact info, total_centavos, hold_expires_at.
  • booking_passengers — manifest PII: full name, age, sex, passenger type (adult | child | infant | senior | pwd | student), fare, discount applied.
  • discounts — Senior Citizen 20% (RA 9994) and PWD 20% (RA 10754), both VAT-exempt; student/other fares as configured.
  • payments — method = 'otc', OR (Official Receipt) number, cashier_id/terminal, paid_at, amount. Modeled as a separate table so an online gateway can be added later without schema churn.
  • tickets — one per passenger: signed QR token, boarding status (issued | boarded | void).
  • staff / roles — via Supabase Auth + a role claim (agent, cashier, ops, admin).

Money: store as integer centavos (*_centavos bigint). Never use floats for money.


5. Booking & OTC Reservation Flow

This is the heart of the system. The flow:

  1. Reserve (online): passenger picks a voyage, class, and passengers. Create a booking in pending with hold_expires_at = now() + interval '2 hours' (tunable) and atomically hold inventory.
  2. Pay (OTC): passenger pays at a terminal/agent using the booking reference.
  3. Confirm: cashier records the OR number → booking → confirmed → e-tickets issued (QR generated, available offline via PWA).
  4. Expire: a scheduled job releases inventory for any pending booking past hold_expires_at and sets it to expired.

Non-negotiable invariant: never oversell

All inventory changes go through an atomic, conditional update inside a Postgres function (SECURITY DEFINER), never via read-then-write in app code:

-- Hold: succeeds only if enough remains. Returns rows affected.
UPDATE voyage_inventory
   SET remaining = remaining - $qty
 WHERE voyage_id = $voyage_id
   AND class_id  = $class_id
   AND remaining >= $qty;
-- 0 rows affected  => sold out; reject the reservation.
  • Wrap hold + booking insert in a single transaction.
  • Releases (remaining = remaining + qty) happen on cancel/expire.
  • Reservation writes must be idempotent (dedupe by client-supplied request key) so a retried request during a peak never double-holds.

Hold expiry job

Use pg_cron (or a scheduled Supabase Edge Function) to sweep expired holds every minute. Inventory release must be idempotent (only release once per booking).


6. Supabase / Data Layer Conventions

  • RLS is ON for every table. No table ships without policies. Default deny; grant the minimum. Test policies (see §14).
  • Three clients, never mixed: lib/supabase/server.ts (RSC/route handlers, reads cookies), lib/supabase/client.ts (browser), lib/supabase/middleware.ts (session refresh).
  • service_role key never reaches the client and is never imported into anything under app/(public). Use it only in trusted server contexts (cron, admin actions) and prefer RLS-respecting flows where possible.
  • Migrations are the source of truth — every schema/policy/function change is a checked-in SQL migration in db/. No manual changes in the Supabase dashboard.
  • Pooling: connect through Supavisor (transaction mode) for serverless; do not hold connections across requests.
  • Indexes: index every foreign key and every column used in WHERE/ORDER BY on hot paths (e.g. voyages(departure_at), voyage_inventory(voyage_id, class_id), bookings(reference), bookings(status, hold_expires_at)).

7. Data Fetching & State

  • Reads: prefer React Server Components for initial data (schedules, voyage details) — cheaper, no client waterfall, better TTFB.
  • Client interactivity & mutations: TanStack Query v5. Centralize query keys in lib/query/keys.ts. Use optimistic updates for seat selection, with rollback on the atomic-hold failure (sold out).
  • Admin grids: TanStack Table v8 with server-side pagination, sorting, and filtering — manifests and booking lists can be large; never fetch all rows to the client.
  • Tables are TanStack, always. Every route that renders a tabular list of records MUST use the shared components/ui/data-table.tsx (DataTable, TanStack Table v8) — search, sortable headers, and 10/20/50/100 pagination come built in. Do not hand-roll a <table> for a data list; add per-view filters through the toolbar slot. (Small fixed key/value or summary tables that aren't record lists are exempt.)
  • Set sensible staleTime for schedule data (it changes infrequently); invalidate precisely after mutations rather than refetching everything.

8. Security

Target: securityheaders.com Grade A (Referrer-Policy excluded per requirement)

Set static headers in next.config.ts headers() and the CSP nonce in middleware.ts.

Header Value (starting point)
Strict-Transport-Security max-age=63072000; includeSubDomains; preload
Content-Security-Policy strict, nonce-based (see below) — no unsafe-inline
X-Content-Type-Options nosniff
X-Frame-Options DENY (and frame-ancestors 'none' in CSP)
Permissions-Policy lock down unused features, e.g. camera=(), microphone=(), geolocation=(), interest-cohort=()
X-DNS-Prefetch-Control on

CSP (nonce-based, not unsafe-inline): generate a per-request nonce in middleware.ts, pass it to next/script and inline styles, and build a directive like:

default-src 'self';
script-src 'self' 'nonce-{NONCE}' 'strict-dynamic';
style-src 'self' 'nonce-{NONCE}';
img-src 'self' data: https://*.supabase.co;
connect-src 'self' https://*.supabase.co wss://*.supabase.co;
font-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';

Referrer-Policy exception: intentionally out of scope per project requirement. Note that securityheaders.com counts Referrer-Policy among its six graded headers — if a scan shows Grade A is blocked solely by its absence, add Referrer-Policy: strict-origin-when-cross-origin and revisit.

Beyond headers

  • AuthN: Supabase Auth. AuthZ: RLS + role claims — never trust the client for authorization.
  • Validate every boundary with Zod (route handlers, server actions, form input). Treat all input as hostile.
  • Rate-limit reservation, login, and QR-validation endpoints (Upstash Redis).
  • Secrets: in Vercel env vars only; never commit. NEXT_PUBLIC_* is public — only the anon key and URL belong there. service_role and other secrets are server-only.
  • PII (Data Privacy Act RA 10173 / NPC): manifest data is personal information. Minimize collection, gate access behind RLS + staff roles, log access, and define retention. No PII in logs, URLs, or analytics.

9. Performance & Scalability

  • Cache aggressively on reads: ISR / revalidate for schedule and voyage pages; they change rarely and are read constantly. Cache at the edge (Vercel CDN).
  • Keep writes atomic and short (see §5) so the DB stays healthy during peaks.
  • DB: index hot paths (§6); use Supavisor pooling; avoid N+1 (select joined data in one query / RPC).
  • Idempotent reservations so retries during peaks don't double-book.
  • Images: next/image; serve modern formats; size for mobile.
  • Bundle: prefer Server Components; lazy-load heavy client components; keep the booking path's client JS small.
  • Performance budgets (mobile, mid-range Android): LCP < 2.5s, INP < 200ms, CLS < 0.1. Track via Speed Insights; treat regressions as bugs.

10. Accessibility (WCAG 2.1 AA)

  • Semantic HTML first; ARIA only to fill gaps. Use Radix/shadcn primitives for dialogs, menus, tabs, etc. (focus trapping and keyboard handling come built in).
  • Full keyboard operability; visible focus states; logical focus order; manage focus on route/dialog changes.
  • Color contrast ≥ 4.5:1 for text; never use color as the only signal.
  • Forms: every input has a <label>; errors announced inline and programmatically associated; don't block paste.
  • Respect prefers-reduced-motion.
  • CI gate: automated axe (jest-axe / Playwright-axe) + Lighthouse a11y; plus manual keyboard + screen-reader spot checks for the booking flow.

11. PWA

  • Serwist for the service worker. Provide app/manifest.ts, maskable icons, theme color, and an offline fallback.
  • Installable and passes Lighthouse PWA checks.
  • Caching strategy: static assets cache-first; schedule data network-first with cache fallback; e-tickets (QR) cached for offline access — a passenger at the port with no signal must still show a valid boarding pass.
  • Don't cache authenticated/PII responses in the SW beyond what's needed for offline tickets, and scope/encrypt appropriately.

12. Internationalization & Time

  • Languages: English + Filipino, with Cebuano for Visayas/Mindanao routes. Keep all user-facing strings in message catalogs — no hard-coded copy.
  • Timezone: store timestamps in UTC, render in Asia/Manila (Luxon). Never do date math in local browser time for voyage schedules.
  • Currency formatting: PHP (₱), derived from integer centavos.

13. Coding Standards

  • TypeScript strict; no any; model domain types explicitly (lean on Zod-inferred types).
  • Title Case all UI labels. Every static UI string that is a label — button text, page headings, nav items, table column headers, form field labels, sidecar/dialog titles, tab/menu labels — uses Title Case (capitalize the first letter of every word): "Add User", not "Add user"; "Save Changes", not "Save changes". Write the literal in Title Case at the source — do not rely on CSS text-transform, which mangles acronyms ("RORO") and user data. Exempt (leave as written): sentence-case help/description text, error and toast messages, dynamic values from the database (names, addresses), and placeholder examples.
  • Money = integer centavos, always. Format only at the display edge.
  • Clear server/client boundary: secrets and service_role never cross into client bundles; keep "use client" as low in the tree as possible.
  • Errors: fail loud server-side (log + Sentry), fail gracefully client-side (user-friendly messages, never leak internals).
  • Conventional Commits; small, focused PRs; lint/typecheck/test must pass pre-commit (Husky + lint-staged).
  • Match existing patterns in the file/feature you're editing.

14. Testing Strategy

  • Unit (Vitest + Testing Library): money/discount math (senior/PWD), datetime/TZ, validation schemas, components.
  • RLS / policy tests (tests/rls): for each table, assert that each role can do exactly what it should and nothing more. Critical security gate.
  • Concurrency test: simulate parallel holds on the last seats and assert no oversell.
  • E2E (Playwright): full booking → OTC confirm → e-ticket flow; hold-expiry release; offline e-ticket display.
  • A11y: automated axe + Lighthouse in CI (§10).

15. Commands

Scaffolding doesn't exist yet — create these scripts in package.json when initializing.

pnpm dev            # local dev server
pnpm build          # production build (must pass before merge)
pnpm start          # run production build
pnpm lint           # ESLint
pnpm typecheck      # tsc --noEmit
pnpm test           # Vitest unit + RLS tests
pnpm test:e2e       # Playwright

# Supabase (via supabase CLI)
supabase migration new <name>
supabase db push          # apply migrations
supabase gen types typescript --local > lib/supabase/types.ts

16. Compliance Checklist (Philippines)

  • MARINA / Philippine Coast Guard / PPA (Philippine Ports Authority) manifests — accurate per-voyage passenger manifest (name, age, sex, type) exportable for every sailing.
  • RA 10173 (Data Privacy Act / NPC) — PII minimization, access control (RLS + roles), retention policy, breach readiness; no PII in logs/URLs/analytics.
  • RA 9994 (Senior Citizens) — 20% discount + VAT exemption.
  • RA 10754 (PWD) — 20% discount + VAT exemption.
  • Discounts require ID validation at OTC payment; record the basis on the booking.

Open / To-Decide

  • Seat-level assignment vs. class-capacity only (depends on vessel type — RORO/economy is often capacity-only; cabins are assigned).
  • Hold duration default (assumed 2h) — confirm with operations.
  • Whether to add an online payment gateway later (the payments table is modeled to allow it).