Skip to content

Architecture

┌──────────────────────────────────────────────┐
Browsers ───────▶│ Edge (Caddy — TLS, custom domains) [prod] │
└───────┬──────────────┬──────────────┬────────┘
│ │ │
┌───────▼─────┐ ┌──────▼──────┐ ┌─────▼───────┐
│ apps/web │ │ apps/admin │ │apps/landlord│
│ Nuxt 4 SSR │ │ Vue+Vuetify │ │ (v1: role- │
│ www.* per │ │ SPA │ │ based admin │
│ tenant │ │ admin.* │ │ portal) │
└───────┬─────┘ └──────┬──────┘ └─────┬───────┘
│ │ │
└──────────────▼──────────────┘
┌───────────────┐
│ apps/api │
│ Bun + Hono │
│ REST /v1 │
└──────┬────────┘
│ Drizzle ORM
┌──────▼────────┐
│ MariaDB │
│ pms_dev │
└───────────────┘

In the MVP prototype the landlord portal is delivered by the admin app with role-based reduction; a dedicated apps/landlord build on the shared component packages follows in P6.

pms/
├─ apps/
│ ├─ api/ Bun + Hono REST API, serves generated demo photos
│ ├─ admin/ Vue 3 + Vuetify SPA (Vite)
│ └─ web/ Nuxt 4 SSR public website
├─ packages/
│ ├─ shared/ types, money/date utils, pricing engine, source colours, UUIDv7
│ └─ db/ Drizzle schema, SQL DDL, seed with real BMyGuest portfolio
├─ docs/ German planning docs (actors, use cases, ADRs, data model …)
└─ docs-site/ this documentation (Astro Starlight)
DecisionWhy
Bun runtime everywhereNative TS, fast installs & test runner, single toolchain
Hono for HTTPMature, portable, tiny; typed routes; runs on Bun natively
Drizzle + MariaDBSQL-near TS ORM; calendar queries stay hand-tunable
Vue everywhereOne framework for admin (Vuetify), website (Nuxt) and the shared calendar core
JWT + revocable refreshAccess = JWT HS256 (15 min, httpOnly cookie, iss/aud/exp verified on every portal endpoint); refresh = opaque DB session (7 d, revoked server-side on logout). Strict CORS allow-list, security headers (HSTS, nosniff, frame-deny), rate limits on login & public forms
Amounts as integer minor units77000 = AED 770.00 — no floating-point money bugs
Nights as DATE stringsHotel-night semantics; timezone-proof
booking_nights guard tablePK (unit_id, night) makes double bookings impossible at DB level

The full ADR list (15 decisions incl. alternatives) lives in the repository: docs/04-architektur.md.

Every occupying booking writes one row per night into booking_nights. The composite primary key (unit_id, night) turns an overlap into a constraint violation inside the transaction:

  1. INSERT bookings + INSERT booking_nights (one row per night) in one transaction.
  2. Duplicate night → ER_DUP_ENTRY → rollback → API returns 409 BOOKING_CONFLICT.
  3. Cancelling/moving deletes and re-inserts nights in the same transaction.

Availability search is a cheap NOT EXISTS over the same table — one source of truth for the calendar, the website search and the quote validation.