Files
TS_Proto/README.md
T
nilesh.pandClaude Opus 4.8 a9dc273e09 Add Docker setup: one script to set up/start/stop the full stack
A new developer needs only Docker Desktop — no local Go, Node, or PostgreSQL.

- docker-compose.yml: PostgreSQL + Go API + Angular (nginx) with healthcheck,
  migrations auto-applied via docker-entrypoint-initdb.d, data in a volume
- backend/Dockerfile: static Go binary on distroless (nonroot)
- frontend/Dockerfile: Angular prod build served by nginx, which proxies /api
  to the api service (same origin, no CORS)
- docker.ps1: setup / start / stop / restart / reset / rebuild / status / logs;
  reads DB creds from backend/.env (single source, nothing duplicated)
- .dockerignore keeps .env and local artifacts out of image build contexts
- README: "Run with Docker" quick start

Not yet run end to end here (Docker is not installed on this machine); the
compose and script are validated for syntax and structure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-10 13:39:15 +05:30

11 KiB

DEMO APPLICATION FOR QA AUTOMATION TravelHub

A multi-tenant flight booking application: Angular frontend, Go backend, PostgreSQL database. Built as a reference sample for travel-domain SaaS, where several travel agencies ("tenants") share one deployment but never see each other's inventory, customers, or revenue.

Authentication is HTTP Basic auth, as requested, scoped per tenant.

Documentation


Run with Docker (quickest — no local toolchain)

A new developer needs only Docker Desktop — no local Go, Node, or PostgreSQL. One script sets up, starts and stops the whole stack:

# one-time: set the DB password the containers will use
cp backend\.env.example backend\.env   # then edit DB_PASSWORD

.\docker.ps1 setup       # build images, start everything, seed the database
.\docker.ps1 stop        # stop (keeps data)
.\docker.ps1 start       # start again
.\docker.ps1 reset       # wipe data and re-seed with fresh flight dates
.\docker.ps1 logs -Follow

Then open http://localhost:4200 (nginx serves the SPA and proxies /api to the Go API on :8080). Sign in with any demo account, password demo1234.

The database is created and seeded on first setup via the migrations mounted into PostgreSQL. Don't run the native app.ps1 at the same time — it uses the same ports.

For local development without containers, use the native setup below.


Prerequisites

Tool Version used here
Go 1.27
Node.js 24 LTS
PostgreSQL 17

Quick start

1. Configure the database connection

cd backend
cp .env.example .env    # then edit .env with your PostgreSQL credentials

2. Create the database and load the schema

createdb -U postgres travelhub
psql -U postgres -d travelhub -f migrations/001_init.sql
psql -U postgres -d travelhub -f migrations/002_seed.sql
psql -U postgres -d travelhub -f migrations/003_demo_bookings.sql

All three are safe to re-run: 002 skips rows that already exist, and 003 returns early once its bookings are present, so seats are never double-decremented.

On Windows, createdb and psql live in C:\Program Files\PostgreSQL\17\bin.

3. Run the API (listens on http://localhost:8080)

cd backend
go run ./cmd/api

4. Run the frontend (serves http://localhost:4200)

cd frontend
npm install
npm start

Open http://localhost:4200 and sign in with any demo account below.

Demo accounts

Every seeded account uses the password demo1234.

Tenant 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 tenant in turn and search JFK → LHR: both agencies sell the same routes, but Globetrotter's fares carry a 12% negotiated markup and availability differs. That contrast is the point of the sample — it makes tenant isolation visible rather than theoretical.


How multi-tenancy works

Storage model: shared database, shared schema, tenant_id discriminator. Every tenant-owned table (users, flights, bookings, passengers) carries a tenant_id foreign key. Reference data that is genuinely global — airports and airlines — does not.

This model was chosen over schema-per-tenant or database-per-tenant because it keeps migrations single-shot and connection pooling simple, which is the right trade for a sample and for most early-stage travel SaaS. The cost is that isolation is enforced in application code rather than by the database, so the codebase holds that line in three specific places:

  1. The tenant is resolved from credentials, not from client input. The X-Tenant header (or subdomain) names an agency, but the password is then verified against the user row inside that tenant. Credentials valid at one agency return 401 at another.
  2. Handlers never read a tenant id from the request. They take it from the authenticated identity in the request context. See the package comment in internal/handlers.
  3. Every tenant-scoped query filters on tenant_id as $1. This is a stated convention in internal/repository, and a lookup that misses returns ErrNotFound → HTTP 404, so probing for another tenant's record is indistinguishable from asking for one that does not exist.

If you take this to production, add PostgreSQL row-level security as a second line of defence, so a missed WHERE clause fails closed at the database rather than leaking rows.

Authentication

Basic auth, per the brief. Credentials travel in the standard Authorization: Basic base64(email:password) header alongside X-Tenant. Passwords are stored as bcrypt hashes (seeded via pgcrypto's crypt()/gen_salt('bf'), which golang.org/x/crypto/bcrypt verifies directly).

Two deliberate details:

  • 401 responses omit WWW-Authenticate. Sending it would make the browser open its own credential dialog, which would fight with the Angular sign-in screen.
  • A missing user still pays for a bcrypt comparison, so response timing does not reveal which email addresses are registered.

Production note: Basic auth sends re-usable credentials on every request. It is fine behind TLS for an internal tool or a sample, but a public deployment wants short-lived tokens (JWT or session cookies) and a refresh flow. The auth logic is isolated in internal/middleware and internal/auth, so swapping it touches two files and no handler.

API

All endpoints require Authorization and X-Tenant headers except the two marked public.

Method Path Purpose
GET /api/health Liveness check (public)
GET /api/tenants Agency list for the sign-in picker (public)
GET /api/session Current user and tenant; validates sign-in
GET /api/flights Search inventory
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)
POST /api/bookings Create a booking
GET /api/bookings/{id} One booking with passengers
POST /api/bookings/{id}/cancel Cancel and return seats to inventory
GET /api/dashboard Agency totals, top routes, recent bookings

Flight search parameters: origin, destination (IATA codes), date (YYYY-MM-DD), cabin (economy, premium_economy, business, first), passengers, sort (price, duration, departure), limit, offset.

Example:

curl -u agent@skyline.test:demo1234 -H 'X-Tenant: skyline' \
  'http://localhost:8080/api/flights?origin=JFK&destination=LHR&sort=price&limit=5'

Overselling

POST /api/bookings runs in one transaction and takes a row lock on the flight (SELECT ... FOR UPDATE) before reading availability, so two agents booking the last seats concurrently cannot oversell — the second waits and then sees the decremented count. Cancelling restores the seats and is guarded against double-crediting by filtering on status = 'confirmed' in the UPDATE.

Project layout

TS_Proto/
├── backend/
│   ├── cmd/api/main.go            server entrypoint, routes, shutdown
│   ├── internal/
│   │   ├── auth/                  identity in request context
│   │   ├── config/                .env loading, DSN assembly
│   │   ├── database/              pgx pool
│   │   ├── handlers/              HTTP layer
│   │   ├── middleware/            basic auth, tenant resolution, CORS
│   │   ├── models/                API types
│   │   └── repository/            all SQL
│   ├── migrations/
│   │   ├── 001_init.sql           schema
│   │   ├── 002_seed.sql           demo tenants, users, routes, inventory
│   │   └── 003_demo_bookings.sql  demo reservations, so the dashboard is alive
│   ├── internal/testsupport/      per-test tenant harness for the Go suite
│   └── .env                       local credentials (git-ignored)
├── frontend/                      Angular application (*.spec.ts alongside source)
├── e2e/                           Playwright coded UI tests
│   ├── pages/                     one page object per screen
│   ├── fixtures/                  shared fixtures and the console-error guard
│   └── tests/                     the specs
├── scripts/db.mjs                 create / migrate / reset the database
└── .github/workflows/ci.yml       runs all three suites

Testing

Three suites — 311 tests in total. TESTING.md explains how they are structured; TEST-CATALOG.md lists every test by name and is regenerated with npm run docs:tests.

npm run setup       # one-time: dependencies and a browser
npm run db:test     # one-time: the empty database the Go tests use

npm run test        # everything
npm run test:unit   # Go + Angular only, no browser
npm run docs:tests  # regenerate TEST-CATALOG.md
Suite Count What it proves
Go 115 Tenant isolation, seat accounting and validation against a real PostgreSQL
Angular 114 Services, guards, the auth interceptor, and component behaviour
Playwright 82 The whole stack in a real browser, plus 19 simulated-condition tests covering failing, slow and offline backends

CI runs all three on every push — see .github/workflows/ci.yml.

Known limitations

This is a sample, and these are deliberate omissions rather than oversights:

  • No migration runner — migrations are applied with psql (or npm run db:migrate).
  • Round trips, seat maps, ancillaries, payment capture, and ticketing are all out of scope; a booking is a single one-way flight.
  • Tenant and user administration is seed-data only; there is no signup or invite flow.