Architecture
System overview
Section titled “System overview” ┌──────────────────────────────────────────────┐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.
Monorepo layout
Section titled “Monorepo layout”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)Key decisions (short form)
Section titled “Key decisions (short form)”| Decision | Why |
|---|---|
| Bun runtime everywhere | Native TS, fast installs & test runner, single toolchain |
| Hono for HTTP | Mature, portable, tiny; typed routes; runs on Bun natively |
| Drizzle + MariaDB | SQL-near TS ORM; calendar queries stay hand-tunable |
| Vue everywhere | One framework for admin (Vuetify), website (Nuxt) and the shared calendar core |
| JWT + revocable refresh | Access = 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 units | 77000 = AED 770.00 — no floating-point money bugs |
Nights as DATE strings | Hotel-night semantics; timezone-proof |
booking_nights guard table | PK (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.
The double-booking guard
Section titled “The double-booking guard”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:
INSERT bookings+INSERT booking_nights(one row per night) in one transaction.- Duplicate night →
ER_DUP_ENTRY→ rollback → API returns409 BOOKING_CONFLICT. - 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.