DO NOT INVENT HOTEL DOMAIN LOGIC.
All hotel domain knowledge comes from kb/HAIP_KNOWLEDGE_BASE.md. If something is ambiguous or missing, STOP and surface the question. Do not guess. Do not hallucinate hotel operations.
HAIP (Hotel AI Platform) is an open-source, TypeScript/Node.js, API-first hotel PMS. Sister project to OTAIP. HAIP handles lodging. OTAIP handles air.
Architecture: Option B — PMS is standalone, OTAIP agents connect via API (not embedded).
- TypeScript strict mode, Node.js >=20
- NestJS framework
- PostgreSQL (multi-tenant, property_id on every table)
- Drizzle ORM (TypeScript-native, no magic)
- Redis + BullMQ (cache, queue, pub/sub)
- REST API, OpenAPI 3.0 auto-generated from NestJS decorators
- OAuth 2.0 / OpenID Connect
- pnpm workspaces
- Vitest for testing
- tsup for building packages
- Docker + docker-compose
- All API endpoints go through NestJS controllers with Swagger decorators
- Every table has
property_idfor multi-tenancy - Never store raw card data (PCI DSS — use Stripe/Adyen tokenization)
- Audit log all data modifications (GDPR compliance)
- Use Drizzle ORM for all database queries — no raw SQL except migrations
- Tests required for all business logic
- Use the webhook event pattern: entity.action (e.g., reservation.created)
Every service method that queries a table with propertyId MUST filter by it
alongside any id filter. This applies to reads, updates, and deletes — even
for methods called only internally, because future controllers may call them.
WHERE id = $1on a property-scoped table is a BUG. Useand(eq(table.id, id), eq(table.propertyId, propertyId)).- Controllers:
propertyIdis a REQUIRED query param (UUID-validated) on every:idroute, not optional. List endpoints also require it. propertyIdmust come from the request, never inferred from other entity lookups (that creates a confused-deputy bug — the attacker supplies the id, the server derives a matching propertyId, scoping becomes a no-op).- Exceptions — document the reason in a code comment when you deviate:
gueststable: the ROW is cross-property by design (one person stays at multiple hotels), but API access MUST verify a reservation link at the requesting property — i.e. scope reads/updates/deletes by "has this guest at least one reservation atpropertyId?". Creation is the only exception (walk-ins have no reservation yet); the linking reservation is created immediately after.propertiestable: the property IS the tenant- Connect API (
/api/v1/connect/*): bearer-credential model viaconfirmationNumber— scoped by credential possession, not propertyId - Internal cron/webhook receivers invoked with trusted server-side ids
When adding a new controller route or service method that touches a property-scoped table, the WHERE clause is the first thing to verify.
haip-project/
├── apps/api/ # NestJS API application
├── packages/database/ # Drizzle ORM schema and migrations
├── packages/shared/ # Shared types and utilities
├── kb/ # Knowledge base (domain truth)
├── instructions/ # Project rules
├── specs/ # Agent specs (YAML)
├── briefs/ # Claude Code build briefs
└── docker-compose.yml # Local dev environment
Each domain module in apps/api/src/modules/ follows:
module-name/
├── module-name.module.ts # NestJS module definition
├── module-name.controller.ts # REST endpoints with Swagger
├── module-name.service.ts # Business logic
└── dto/ # Request/response DTOs
- Don't add dependencies without justification
- Don't skip tests
- Don't commit research files (kb/research/raw/)
- Don't commit .env files
- Don't invent hotel domain concepts not in the KB
- Don't use raw SQL outside of migration files
- Don't store sensitive data unencrypted