Add project guidance, security/governance/hygiene rules, and product wiki

- CLAUDE.md + AGENTS.md: canonical guiding principles for AI agents and
  developers (tenant isolation, no RLS, stack, security, conventions,
  environment gotchas)
- SECURITY.md: required security controls (isolation, secrets, auth,
  input handling, least privilege) with VAPT status
- GOVERNANCE.md: how changes are validated before merge — deterministic
  gates + AI review, risk tiers, human accountability
- CONTRIBUTING.md: code hygiene, definition of done, file hygiene
- docs/wiki/: product reference (overview, domain model, features,
  roles & access, API reference, glossary)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nilesh.p
2026-10-10 13:31:04 +05:30
co-authored by Claude Opus 4.8
parent 45b5b34680
commit 035711f08c
12 changed files with 788 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
# Overview
## What TravelHub is
TravelHub is a **multi-tenant flight-booking platform** in the travel domain. It
serves travel agencies: each agency's staff use it to search flight inventory,
sell tickets to travellers, and manage the resulting bookings. It is built as a
reference implementation of a secure, multi-tenant full-stack application.
- **Frontend:** Angular single-page app (the agent console).
- **Backend:** Go HTTP API.
- **Database:** PostgreSQL.
- **Auth:** HTTP Basic, scoped per tenant.
## The multi-tenant model
A **tenant** is one travel agency. Many tenants share a single deployment and a
single database. The central product promise: **no tenant can see another
tenant's data** — not its flights, not its customers, not its revenue, not even
whether a given booking exists.
Storage uses a **shared database, shared schema, `tenant_id` discriminator**:
every tenant-owned row carries the id of the tenant that owns it. Reference data
that is genuinely global — airports and airlines — is shared and has no tenant.
Isolation is enforced in the **application layer**: the tenant is derived from
who you authenticated as, and every query is scoped to that tenant. (This is a
deliberate design choice; PostgreSQL row-level security is intentionally not
used — see [CLAUDE.md](../../CLAUDE.md).)
### What makes isolation visible
The two seeded demo agencies carry the **same routes at different fares** —
Skyline at list price, Globetrotter at a negotiated markup — and different
availability. Signing in as each and searching the same route shows different
results. That contrast is the product's isolation made tangible, not an
accident of the demo data.
## Who uses it
Agency staff in two roles — **admin** and **agent** (see
[Roles & access](04-roles-and-access.md)). There is no traveller-facing
self-service; a traveller is a passenger on a booking an agent makes.
## What it deliberately is not (yet)
A booking is a single one-way flight. Round trips, seat maps, ancillaries,
payment capture, ticketing, and tenant self-signup are out of scope for this
reference build.
+70
View File
@@ -0,0 +1,70 @@
# Domain model
## Entities
| Entity | Scope | Purpose |
| ------ | ----- | ------- |
| **Tenant** | — | A travel agency. The owner of all tenant-scoped data. |
| **User** | per-tenant | Agency staff who sign in (admin or agent). |
| **Airport** | global | Reference data (IATA code, city, country, timezone). |
| **Airline** | global | Reference data (IATA code, name). |
| **Flight** | per-tenant | Sellable inventory: a route on a date at a fare, with seats. |
| **Booking** | per-tenant | A confirmed sale against a flight. |
| **Passenger** | per-tenant | A traveller on a booking. |
"Per-tenant" entities carry a `tenant_id` and cascade-delete with the tenant.
"Global" entities are shared by all tenants and have no tenant.
## Relationships
```
Tenant 1───* User
Tenant 1───* Flight ───* Booking ───* Passenger
│ │
Airline, Airport User (the agent who sold it)
(global refs)
```
- A **Flight** references a global Airline and two global Airports (origin,
destination), but belongs to one tenant — so the same physical route exists as
a separate, independently-priced Flight row per tenant.
- A **Booking** references a Flight, the User who made it, and has many
Passengers.
## Key fields & rules
**Tenant** — `slug` (unique), `name`, `status` ∈ {`active`, `suspended`}. A
suspended tenant's users cannot sign in, and it is hidden from the public tenant
list.
**User** — `email`, `password_hash` (bcrypt), `full_name`, `role` ∈
{`admin`, `agent`}. Email is **unique per tenant, not globally** — the same
person can hold accounts at several agencies.
**Flight** — `flight_number`, `departure_time`/`arrival_time`, `cabin_class` ∈
{`economy`, `premium_economy`, `business`, `first`}, `base_price`, `currency`,
`seats_total`, `seats_available`, `status` ∈ {`scheduled`, `delayed`,
`cancelled`}. Constraints guarantee `seats_available ≤ seats_total` and
`arrival_time > departure_time`.
**Booking** — `booking_reference` (the PNR, **unique per tenant**),
`passenger_count`, `total_amount`, `currency`, `status` ∈ {`confirmed`,
`cancelled`}, contact email/phone.
**Passenger** — name, optional date of birth, passport number, seat.
## Invariants the system must uphold
- **No overselling.** A booking is created in a transaction that row-locks the
flight (`SELECT … FOR UPDATE`) before reading availability, so two concurrent
sales of the last seat cannot both succeed — the second gets a 409.
- **Seat accounting round-trips.** Confirming a booking decrements
`seats_available` by the passenger count; cancelling returns them (guarded so
a double-cancel can't credit twice).
- **PNR uniqueness is per tenant**, generated from an alphabet that omits
easily-confused characters (no I/O/0/1).
- **Tenant isolation** on every read and write (see
[Overview](01-overview.md) and [SECURITY.md](../../SECURITY.md)).
The authoritative schema is in
[`backend/migrations/001_init.sql`](../../backend/migrations/001_init.sql).
+58
View File
@@ -0,0 +1,58 @@
# Features
The agent console, screen by screen. Each maps to a feature area used in the
test suite and the evidence report.
## Sign in
- The agency picker lists active tenants (the only unauthenticated read).
- The agent chooses an agency, then enters credentials. The same email/password
is only valid against the agency it was issued for — valid credentials aimed
at the wrong agency are rejected.
- Demo-account shortcuts fill the form for quick access (see
[Roles & access](04-roles-and-access.md)).
- Sessions survive a reload (credentials held client-side) but not a sign-out;
stale credentials are discarded and the user returned to sign-in.
## Search flights
- Filter the agency's inventory by **origin/destination** (IATA), **date**,
**cabin**, and **number of travellers**.
- Dropdowns offer only the airports the agency actually flies.
- Sort by **lowest fare**, **shortest trip**, or **earliest departure**.
- Results are paged; each card shows carrier, times (with a +1 for overnight
arrivals), duration, fare, cabin, and seats remaining (scarce inventory is
flagged). Only future, non-cancelled flights with enough seats appear.
## Checkout & booking
- Selecting a flight opens checkout with the fare carried over.
- Enter one or more travellers (up to 9) and contact details; the total reprices
live as travellers change.
- Confirming creates the booking and shows a confirmation with the **PNR**.
- Guardrails: the party can't exceed remaining seats; a seat sold while the form
was open surfaces as a clear "just sold" message (409), not an oversell.
## Bookings
- List every reservation the agency has sold, filterable by status
(confirmed/cancelled), newest first.
- Open a booking to see its full itinerary, travellers, totals and contact.
- **Cancel** a booking (two-step confirm); the seats return to inventory. A
cancelled booking still appears in the list, marked cancelled.
## Dashboard
- The agency's trading position: total revenue, confirmed/cancelled bookings,
seats sold, future flights in stock.
- Best-selling routes and the most recent bookings.
- All figures are strictly per-agency; a sale moves them and a cancellation
moves them back.
## Cross-cutting
- **Multi-tenant throughout:** every screen shows only the signed-in agency's
data; the sidebar always names the current agency.
- **Responsive:** works at desk and phone width; no horizontal scrolling.
- **Observable correctness:** the e2e suite captures screenshots and the API
request/response behind each of these flows into the evidence report.
+61
View File
@@ -0,0 +1,61 @@
# Roles & access
## Authentication
HTTP **Basic auth**, scoped per tenant. Every API call (except the two public
endpoints) carries:
- an `Authorization: Basic base64(email:password)` header, and
- an `X-Tenant` header naming the agency (or the agency is taken from the
subdomain).
The password is verified against the user row **inside the named tenant**, so
credentials from one agency never authenticate at another. Passwords are stored
as bcrypt hashes.
> Basic auth replays a reusable credential on every request and the SPA holds it
> client-side. That is acceptable for this reference build behind TLS; a public
> deployment should move to short-lived tokens. See
> [SECURITY.md](../../SECURITY.md).
## Roles
| Role | Intended capability |
| ---- | ------------------- |
| **agent** | Search, book, manage and cancel bookings; view the dashboard. |
| **admin** | Everything an agent can do, plus agency administration. |
Both roles exist on the user record and the API exposes an `admin`/`agent`
distinction. **Note:** admin-only functionality is not yet built, so today the
two roles are functionally identical — the `RequireAdmin` gate exists but is
wired to no route. When admin features are added they must be gated explicitly
(see [GOVERNANCE.md](../../GOVERNANCE.md)).
## Tenant status
A tenant is `active` or `suspended`. A suspended tenant's users cannot sign in,
and the tenant is hidden from the public agency picker.
## Demo accounts
Seeded by `002_seed.sql`. Password for all: **`demo1234`**.
| Agency | Email | Role |
| ------ | ----- | ---- |
| Skyline Travel | `admin@skyline.test` | admin |
| Skyline Travel | `agent@skyline.test` | agent |
| Globetrotter Tours | `admin@globetrotter.test` | admin |
| Globetrotter Tours | `agent@globetrotter.test` | agent |
Sign in to each agency and search `JFK → LHR`: the same route is priced
differently per tenant — the isolation made visible.
## Public (unauthenticated) endpoints
Only two, by necessity:
- `GET /api/health` — liveness.
- `GET /api/tenants` — the agency list for the sign-in picker (returns slug and
name only; active tenants only).
Everything else requires a valid, tenant-scoped credential.
+67
View File
@@ -0,0 +1,67 @@
# API reference
Base URL (local): `http://localhost:8080`. All responses are JSON with camelCase
fields. All endpoints require `Authorization` (Basic) and `X-Tenant` headers
**except** the two marked public.
| Method | Path | Purpose |
| ------ | ---- | ------- |
| GET | `/api/health` | Liveness (**public**) |
| GET | `/api/tenants` | Agency list for the sign-in picker (**public**) |
| GET | `/api/session` | Current user + tenant; validates credentials |
| GET | `/api/flights` | Search inventory (see parameters) |
| GET | `/api/flights/{id}` | One flight |
| GET | `/api/airports` | Airports in this tenant's network |
| GET | `/api/airlines` | Carriers in this tenant's inventory |
| GET | `/api/bookings` | List bookings (`?status=confirmed|cancelled`) |
| POST | `/api/bookings` | Create a booking |
| GET | `/api/bookings/{id}` | One booking with passengers |
| POST | `/api/bookings/{id}/cancel` | Cancel; returns seats to inventory |
| GET | `/api/dashboard` | Agency totals, top routes, recent bookings |
## Flight search parameters
`GET /api/flights` accepts: `origin`, `destination` (IATA codes), `date`
(`YYYY-MM-DD`), `cabin` (`economy|premium_economy|business|first`), `passengers`
(min seats free), `sort` (`price|duration|departure`), `limit`, `offset`.
Only future, non-cancelled flights with enough seats are returned.
```bash
curl -u agent@skyline.test:demo1234 -H 'X-Tenant: skyline' \
'http://localhost:8080/api/flights?origin=JFK&destination=LHR&sort=price&limit=5'
```
## Create a booking
`POST /api/bookings`
```json
{
"flightId": "uuid",
"contactEmail": "ops@example.com",
"contactPhone": "+1-555-0100",
"passengers": [
{ "firstName": "Dana", "lastName": "Whitfield",
"dateOfBirth": "1988-04-12", "passportNumber": "X1234567" }
]
}
```
Server-controlled (never trust the client for these): the tenant, the fare,
the total, and the PNR. Unknown fields are rejected (`400`).
## Status codes & contracts
| Code | When |
| ---- | ---- |
| 200 / 201 | success (201 on create) |
| 400 | invalid input (bad date/cabin, malformed JSON, unknown field, validation) |
| 401 | missing/invalid credentials, or credentials for the wrong tenant |
| 403 | admin-only route refused to an agent |
| 404 | not found **or** belongs to another tenant (existence is hidden) |
| 409 | not enough seats (oversell prevented) |
| 500 | server error (generic message; no internals leaked) |
The full request/response of each call is captured in the e2e evidence report
(with the `Authorization` header redacted). Types are defined in
[`backend/internal/models`](../../backend/internal/models/models.go).
+22
View File
@@ -0,0 +1,22 @@
# Glossary
| Term | Meaning |
| ---- | ------- |
| **Tenant** | One travel agency using the platform. The owner of all tenant-scoped data. Identified by a `slug` (e.g. `skyline`). |
| **Agency** | The user-facing word for a tenant. |
| **Agent** | Agency staff member who searches, books and manages reservations. |
| **Admin** | Agency staff member with (intended) administration rights on top of agent capabilities. |
| **Tenant isolation** | The guarantee that one tenant cannot see or affect another's data. Enforced in the application layer. |
| **PNR / Booking reference** | The 6-character code identifying a booking (e.g. `QX7A2M`). Unique within a tenant. |
| **Flight (inventory)** | A sellable route on a date at a fare with a seat count, owned by one tenant. The same physical route is a separate row per tenant. |
| **Cabin class** | `economy`, `premium_economy`, `business`, or `first`. |
| **Seats available / total** | Remaining and original seats on a flight. Bookings decrement available; cancellations restore it. |
| **Oversell** | Selling more seats than exist. Prevented by a row-locked transaction at booking time. |
| **IATA code** | Standard airport (3-letter, e.g. `JFK`) or airline (2-letter, e.g. `AA`) code. |
| **Reference data** | Global, non-tenant data: airports and airlines, shared by all tenants. |
| **X-Tenant** | The HTTP header naming which agency a request is for (or taken from the subdomain). |
| **Basic auth** | The authentication scheme: `Authorization: Basic base64(email:password)`, verified within the named tenant. |
| **Suspended tenant** | An agency whose users cannot sign in and which is hidden from the agency picker. |
| **Evidence report** | The e2e test report showing screenshots and the API request/response for each scenario (`e2e/evidence-report/`). |
| **Scenario card** | A structured definition of one test scenario in `qa/scenarios/`; the source a Playwright test is generated from. |
| **Binding (test layer)** | A test of the seam between layers — that the UI sends what the API expects and renders what it returns. |
+24
View File
@@ -0,0 +1,24 @@
# TravelHub product wiki
The product reference for TravelHub — what it is, how it is modelled, and how it
behaves. For engineering conventions see [CLAUDE.md](../../CLAUDE.md); for
running and testing see [TESTING.md](../../TESTING.md) and
[qa/README.md](../../qa/README.md).
## Pages
1. [Overview](01-overview.md) — what TravelHub is and the multi-tenant model
2. [Domain model](02-domain-model.md) — entities, relationships, the data model
3. [Features](03-features.md) — capabilities, screen by screen
4. [Roles & access](04-roles-and-access.md) — admin/agent, auth, demo accounts
5. [API reference](05-api-reference.md) — endpoints and contracts
6. [Glossary](06-glossary.md) — domain terms
## In one paragraph
TravelHub is a multi-tenant flight-booking platform for travel agencies. Several
agencies ("tenants") share one deployment and one database, but each sees only
its own inventory, customers and revenue. An agency's staff sign in, search the
agency's flight inventory, sell tickets, manage and cancel bookings, and see a
trading dashboard. It is built as a reference sample: Angular SPA, Go API,
PostgreSQL, with strict per-tenant isolation.