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
+27
View File
@@ -0,0 +1,27 @@
# AGENTS.md
This project's guidance for AI coding agents lives in **[CLAUDE.md](CLAUDE.md)**.
It is tool-agnostic — read it before generating or changing code here.
Quick pointers:
- **Guide (read first):** [CLAUDE.md](CLAUDE.md)
- **Security rules:** [SECURITY.md](SECURITY.md)
- **Governance rules:** [GOVERNANCE.md](GOVERNANCE.md)
- **Contributing & hygiene:** [CONTRIBUTING.md](CONTRIBUTING.md)
- **Product wiki:** [docs/wiki/](docs/wiki/README.md)
- **Generating Playwright tests:** [e2e/CLAUDE.md](e2e/CLAUDE.md)
- **QA scenario → test workflow:** [qa/README.md](qa/README.md)
- **Testing overview:** [TESTING.md](TESTING.md)
- **Security findings to respect:** [security/VAPT-REPORT.md](security/VAPT-REPORT.md)
The non-negotiables, in one line each:
1. Tenant isolation is enforced in the repository layer; tenant id comes from
the authenticated identity, every tenant query filters `tenant_id`, misses
return 404. **Do not add PostgreSQL RLS.**
2. Never commit secrets; `.env` is git-ignored and read at runtime.
3. A change isn't done until its tests pass and the evidence proves it; never
weaken an assertion.
4. Stay in scope; a human owns the merge for anything tenant- or
security-sensitive.
+183
View File
@@ -0,0 +1,183 @@
# TravelHub — project guide for AI agents and developers
Guiding principles and working conventions for this repository. Read this
before generating or changing code here. It is the canonical guide; other
tools' entry points (`AGENTS.md`) point to it.
TravelHub is a **multi-tenant flight-booking platform**: an Angular SPA, a Go
API, and PostgreSQL. Several travel agencies ("tenants") share one deployment
and one database, and **no tenant may ever see another's data**. That promise is
the point of the product and the first thing to protect in any change.
**Required reading** (rules are enforced, not advisory):
[SECURITY.md](SECURITY.md) · [GOVERNANCE.md](GOVERNANCE.md) ·
[CONTRIBUTING.md](CONTRIBUTING.md). Product reference:
[docs/wiki/](docs/wiki/README.md). Testing: [TESTING.md](TESTING.md),
[qa/README.md](qa/README.md).
---
## Stack (do not swap without being asked)
- **Frontend:** Angular 22 (standalone, zoneless), TypeScript, SCSS.
- **Backend:** Go 1.26, module `github.com/travelswitch/travelhub/backend`,
`pgx/v5`, standard-library HTTP (`net/http`), `log/slog`.
- **Database:** PostgreSQL 17.
- **Tests:** Go (integration, real DB), Angular (Vitest), Playwright (e2e).
New work for this team defaults to this stack and to multi-tenancy; design
tenant isolation in from the start, never retrofit it.
---
## Guiding principles
1. **Tenant isolation is enforced in application code, and it is sacred.**
- The tenant id comes from the **authenticated identity**, never from the
URL, query string, or request body. A client cannot name its own tenant.
- **Every** query touching a tenant-owned table (`users`, `flights`,
`bookings`, `passengers`) filters on `tenant_id` as its first parameter.
- A lookup that misses within the tenant returns `ErrNotFound` → **HTTP 404**,
never 403 — so probing for another tenant's record is indistinguishable
from asking for one that does not exist.
- **Do not introduce PostgreSQL row-level security (RLS).** It is a deliberate
choice for this project: per-request session state (`SET LOCAL`) fights
connection pooling and a missed reset leaks across tenants rather than
failing closed. Enforce isolation in the repository layer with the
convention above. If stronger isolation is wanted at scale, prefer
schema- or database-per-tenant, not RLS.
2. **Security is not optional.** See the security rules below and
`security/VAPT-REPORT.md`. Never weaken a security control to make something
work; flag it instead.
3. **A change isn't done until its tests pass and prove it.** Assert on
behaviour a user can observe, not on implementation. Never loosen an
assertion to get green.
4. **Scope discipline.** Do what was asked; don't widen, narrow, or sneak in
unrelated changes. AI-generated PRs that drift in scope are rejected.
5. **The human is accountable for what merges.** AI generates and reviews;
people own the merge decision on anything security- or tenant-sensitive.
---
## Layout
```
backend/
cmd/api/ server entrypoint, graceful shutdown
internal/
httpapi/ the route tree (shared by server and tests)
handlers/ HTTP layer — takes tenant id from the identity
middleware/ basic auth + tenant resolution, CORS, recoverer
repository/ ALL SQL; every tenant query filters tenant_id = $1
auth/ identity carried in request context
config/ .env loading, DSN assembly
database/ pgx pool
models/ API types (camelCase JSON)
testsupport/ per-test tenant harness for the Go suite
migrations/ 001 schema · 002 seed · 003 demo bookings (idempotent)
frontend/ Angular SPA (features/, core/, layout/, shared/)
e2e/ Playwright: pages/ fixtures/ tests/ reporters/
CLAUDE.md conventions for generating Playwright tests
qa/ scenario cards, the generation prompt, the QA runbook
scripts/ db.mjs, report + hook helpers
app.ps1 start/stop/restart/rebuild/status/logs the app
run-tests.ps1 run any test layer, log, emit JUnit + evidence
```
---
## Running, testing, data
Everything has an npm script or a ps1; prefer them over ad-hoc commands.
```powershell
.\app.ps1 start # DB up, API (:8080) + Angular (:4200)
.\app.ps1 rebuild -Clean # build Go binary + Angular bundle
.\run-tests.ps1 -Layer all # backend + web + ui, logged, JUnit + evidence
.\run-tests.ps1 -Layer unit # fast: backend + web, no browser
npm run db:migrate | db:reset # schema + seed (reset re-seeds fresh)
npm run report # open the evidence report
```
- **Test output** lands in `.testlogs/<timestamp>/` (logs, `summary.json`,
`junit.xml`) and `e2e/evidence-report/` (screenshots + API request/response).
All are git-ignored run artifacts.
- **Demo data is time-relative** — seeded flights depart within 21 days of seed
time and go stale after ~3 weeks. `run-tests.ps1` re-seeds automatically via
`npm run db:check`; if the UI shows no flights, run `npm run db:reset`.
---
## Conventions
**Go.** `gofmt` + `go vet` clean. Errors wrapped with `%w` and context. SQL only
in `repository/`, always parameterised (`$1…$n`) — never string-concatenate
values into SQL. Whitelist anything that must reach the SQL text (e.g. sort
columns). Return `ErrNotFound`/`ErrNoSeats`; handlers map them to status codes.
**Angular.** Standalone components, signals, lazy-loaded routes. Services in
`core/`. The auth interceptor stamps credentials + `X-Tenant`; handlers never
trust a client-sent tenant. Keep SCSS theme-aware and responsive (no horizontal
scroll at 390px — grid tracks use `minmax(0, 1fr)`, wide tables scroll in their
own container).
**SQL/migrations.** Idempotent and re-runnable. Multi-tenant tables carry
`tenant_id` with `ON DELETE CASCADE`; global reference data (airports, airlines)
does not. Unique constraints are per-tenant (e.g. PNR unique within a tenant).
**Tests.** See `e2e/CLAUDE.md` for Playwright specifics and `qa/README.md` for
the scenario-card workflow. Go tests get a fresh tenant each via `testsupport`.
---
## Security rules (hard)
- **Never commit secrets.** `.env` and `*.env` are git-ignored (only
`.env.example` is tracked). Credentials, tokens and DB passwords live in
`.env`, read at runtime. The `pre-commit` hook blocks committed secrets; don't
defeat it.
- **Never print or store a live credential.** The e2e API-capture redacts the
`Authorization` header (it's the Basic-auth secret); keep it redacted.
- Passwords are bcrypt; hash in Go, and keep the real and the timing-dummy hash
at the **same cost** (a cost mismatch created a username-timing oracle — see
the VAPT).
- For production, the app must connect to PostgreSQL as a **least-privilege
role**, not a superuser, and over TLS. The open findings are tracked in
`security/VAPT-REPORT.md`.
---
## Git & CI
- Branch from `main`; don't commit or push unless asked. On push, if the remote
has moved, **rebase** onto it (keep history linear) rather than force-push.
- The Gitea remote is `origin`; auth uses `GITEA_TOKEN` from `.env`. Never embed
the token in `.git/config` or in output — pass it inline for one push,
redacted.
- Install hooks once per clone: `.\scripts\install-hooks.ps1`
(pre-commit = secret scan, pre-push = unit tests). `core.hooksPath` is local
and does not travel with a push.
- Commit/PR attribution lines are set by the session; follow whatever the
current reminder specifies.
- CI contract: `run-tests.ps1` emits `junit.xml` + the evidence report for any
pipeline to publish. Azure DevOps integration is planned but not yet wired.
---
## This machine (known gotchas)
- **Windows + PowerShell 5.1.** No `&&`/`||`/ternary. `app.ps1` and
`run-tests.ps1` refresh `PATH` from the registry themselves, because a
terminal opened before Go/Node were installed won't see them — if a fresh
shell can't find `go`/`npm`, that's why (or restart VS Code).
- **Native stderr isn't failure.** Treat a tool's exit code as truth; a warning
on stderr must not throw (use `Continue` around native calls).
- **`npx playwright show-report` ≠ the evidence report.** That opens Playwright's
own debug report from `e2e/playwright-report/`. The rich report (screenshots +
API traffic) is `e2e/evidence-report/`, opened with `npm run report`.
- `psql` is at `C:\Program Files\PostgreSQL\17\bin`; the PostgreSQL service is
`postgresql-x64-17`.
+73
View File
@@ -0,0 +1,73 @@
# Contributing & code hygiene
Working rules for anyone (human or AI) changing this repository. Read
[CLAUDE.md](CLAUDE.md) for the architecture and the full conventions; this is
the day-to-day hygiene.
## First-time setup
```powershell
npm run setup # frontend + e2e dependencies, Playwright browser
npm run db:migrate # create and seed the app database
npm run db:test # create the empty database the Go tests use
.\scripts\install-hooks.ps1 # activate pre-commit / pre-push gates
```
`core.hooksPath` is a local git setting — every clone must run the last step.
## Branching & commits
- **Branch from `main`.** Never commit or push directly to `main`; it is
protected and changes land via pull request.
- One logical change per commit; write a clear subject line (imperative mood)
and a body explaining the *why*.
- Keep attribution lines as the project specifies for commits and PRs.
- If a push is rejected because the remote moved, **rebase** onto it (keep
history linear) — do not force-push.
## Code style
- **Go:** `gofmt` and `go vet` must be clean. Wrap errors with `%w` and context.
SQL only in `repository/`, always parameterised.
- **Angular/TypeScript:** `tsc` clean; Prettier formatting; standalone
components and signals; services in `core/`. No `any` where a type is known.
- **SCSS:** theme-aware and responsive; no horizontal scroll at 390px.
- **SQL/migrations:** idempotent and re-runnable; tenant tables carry
`tenant_id` with `ON DELETE CASCADE`.
## Tests are part of the change
- Every behavioural change ships with tests. New behaviour gets a **scenario
card** in `qa/scenarios/` first (see [`qa/README.md`](qa/README.md)).
- Run the relevant layer before pushing: `.\run-tests.ps1 -Layer <layer>`.
The pre-push hook runs the unit layers automatically.
- **Never weaken an assertion** to get green. Assert on observable behaviour,
not implementation.
- Leave data as you found it — a test that creates a booking cancels it.
## Definition of done
A change is done only when **all** hold:
- [ ] Tests for the change pass (`run-tests.ps1` green).
- [ ] For UI/binding work, the evidence report proves the scenario.
- [ ] No secret added; `.env` untouched in the diff.
- [ ] Scope is exactly the task — nothing unrelated.
- [ ] Docs updated if behaviour, API, or conventions changed (incl. this file,
CLAUDE.md, the wiki).
- [ ] `gofmt`/`go vet`/`tsc` clean.
## File hygiene
- **Never commit:** `.env`/secrets, `node_modules/`, build output
(`dist`, `bin`, `.angular`), run artifacts (`.testlogs/`, `e2e/results/`,
`e2e/evidence-report/`, `.run/`), coverage files, PDFs. These are git-ignored;
keep it that way.
- Do not check in large binaries or generated reports — they are regenerated.
- Keep the docs in sync with the code; a stale wiki is worse than none.
## Destructive actions
Ask before anything hard to reverse or outward-facing (dropping data, force
operations, pushing to a shared remote, deleting). `db:reset` drops the dev
database — expected in dev, never run it against shared data.
+75
View File
@@ -0,0 +1,75 @@
# Governance rules
How code changes — especially AI-generated ones — are validated before they
reach `main`. The goal is to let multiple developers generate code with AI
without losing control of quality, security, or the tenant-isolation guarantees.
## Principle
**Enforcement is server-side and mandatory; local checks are for speed.**
Anything on a developer's machine (hooks, a local AI session) is advisory and
skippable. The real gate is in CI on the pull request, with branch protection
that blocks merge until it passes.
## The validation layers
A change passes through layers, cheapest first. Deterministic tools do the
repeatable work; AI does only the judgment work.
1. **Deterministic gates (blocking, no AI):** build, `go vet` + `gofmt`,
`tsc`, the full test suite via `run-tests.ps1`, coverage threshold, secret
scan, dependency audit, and a diff-size/scope check. If these fail, stop.
2. **AI governance review (judgment):** does the change match its stated intent
and nothing more; does it conform to the architecture (tenant scoping, no
RLS, repository conventions); subtle correctness and security the scanners
miss; are the tests adequate, not just present.
3. **Human sign-off:** required for medium/high-risk changes (below).
## Risk tiers
Routing is by path and diff size, so it cannot be gamed.
| Tier | Touches | Requires |
| ---- | ------- | -------- |
| **Low** | docs, comments, tests only | deterministic gates; may auto-approve |
| **Medium** | feature code | gates + AI review + 1 human approval |
| **High** | auth, tenant isolation, DB schema/migrations, dependencies, CI config, `security/` | gates + AI review + **mandatory human (CODEOWNERS) approval**, no exceptions |
## Rules for AI-generated changes
1. **Scope discipline.** Do exactly what the task asks. No drive-by refactors,
no unrelated edits, no scope creep — a classic AI-PR failure mode.
2. **Never weaken a control or an assertion** to make something pass. If the app
is wrong, surface it as a finding; do not code around it.
3. **The diff is untrusted input.** A reviewing AI treats PR content as data,
emits a structured verdict, and cannot itself merge.
4. **A human is accountable for the merge** on anything medium/high. AI never
auto-merges tenant- or security-sensitive code.
5. **Policy is code.** These rules, `SECURITY.md`, `CLAUDE.md`, and the test
conventions are version-controlled and reviewed like any other change.
6. **Proof of work.** Every change ships with passing tests and, for UI/binding
scenarios, the evidence report showing what was exercised. Review the
evidence, not just the diff.
## The gate's output
Each change produces reviewable artifacts: `junit.xml` (results), the evidence
report (screenshots + API request/response), and — for a governed review — a
findings report by dimension and severity. These are the audit trail.
## Branch protection (intended)
`main` is protected: no direct pushes, required status checks (the CI run of
`run-tests.ps1`), and CODEOWNERS approval on high-risk paths. A documented
break-glass path exists for emergencies, with after-the-fact audit.
## Still to decide
- **CI platform:** Azure DevOps integration is planned; the runner already emits
the artifacts any pipeline needs. Until then, the hooks + local runner are the
shift-left gate.
- **Blocking posture:** the AI review starts advisory and becomes blocking after
one calibration cycle, to avoid false-positives getting it disabled.
See [`qa/README.md`](qa/README.md) for how tests are authored and run, and the
strategy context in the project history.
+79
View File
@@ -0,0 +1,79 @@
# Security rules
Required security controls for TravelHub. These are not suggestions — a change
that breaks one is rejected. They exist because this is a **multi-tenant** system
holding traveller PII (names, passport numbers, contact details) where a single
leaked `WHERE` clause exposes every tenant.
Findings from the whitebox assessment and their status are in
[`security/VAPT-REPORT.md`](security/VAPT-REPORT.md).
## 1. Tenant isolation
- The tenant id is derived from the **authenticated identity**, never from the
URL, query string, header value, or request body. A client cannot choose or
forge the tenant it acts as.
- Every query against a tenant-owned table (`users`, `flights`, `bookings`,
`passengers`) filters on `tenant_id` as its first parameter. No exceptions.
- A miss within the tenant returns **404**, never 403 — existence of another
tenant's record must not be observable.
- **No PostgreSQL row-level security.** Isolation is enforced in the repository
layer by the rules above (see [CLAUDE.md](CLAUDE.md) for the rationale). At
scale, prefer schema- or database-per-tenant, not RLS.
## 2. Secrets
- **Never commit a secret.** `.env` and `*.env` are git-ignored; only
`.env.example` (placeholders) is tracked. Credentials, tokens and DB passwords
live in `.env` and are read at runtime.
- The `pre-commit` hook scans staged content for credential patterns and blocks
them. Do not bypass it (`--no-verify`) to commit a secret.
- **Never print or log a credential.** The e2e API-capture redacts the
`Authorization` header (it is the Basic-auth secret); keep it redacted in any
report or log.
- **If a secret is ever committed or exposed, rotate it immediately** — assume
it is compromised. Scrubbing history is not enough on its own.
## 3. Authentication & credential storage
- Passwords are hashed with **bcrypt in application code** at cost ≥ 12.
- The real hash and the not-found timing-dummy hash must use the **same cost** —
a mismatch creates a username-enumeration timing oracle (VAPT FINDING-2).
- 401 responses omit `WWW-Authenticate` (so the browser dialog doesn't fight the
SPA sign-in); a missing user still pays for a bcrypt comparison (timing).
- Authentication endpoints must be **rate-limited** before any public exposure.
## 4. Input handling
- **All SQL is parameterised** (`$1…$n`). Never concatenate a value into SQL.
Anything that must reach the SQL text (e.g. a sort column) is whitelisted.
- JSON request bodies use `DisallowUnknownFields` (blocks mass-assignment) and
are size-capped (`http.MaxBytesReader`).
- Validate and normalise free-text on input; treat the API as the trust
boundary, not the client.
## 5. Transport & least privilege
- Any non-local database connection uses **TLS** (`sslmode=verify-full`);
`disable` is only for loopback dev.
- The running service connects to PostgreSQL as a **least-privilege role**
(DML only), never a superuser. Migrations run under a separate, higher
privileged role the service never uses.
## 6. Dependencies & headers
- Run dependency scanning in CI: `govulncheck` (Go) and `npm audit` (frontend).
- Responses carry baseline security headers (`nosniff`, `X-Frame-Options`,
`Referrer-Policy`, `Cache-Control: no-store` on authenticated JSON, HSTS behind
TLS); the SPA ships a restrictive CSP.
## 7. Data handling
- Treat passenger PII as sensitive. Do not log it; do not put real PII in seed
or test data (use synthetic values).
- Reports (evidence, test) may contain synthetic test data only.
## Reporting a vulnerability
Raise security issues privately with the repository owner (do not open a public
issue). Include steps to reproduce and the affected files.
+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.