# Haven KE — Technical Implementation

> **Read [`PRD.md`](./PRD.md) first.** It defines *what* to build and *how it should feel*. This document covers *how the system is wired* — the implementation mechanics.

---

## Architecture overview

Haven KE is currently a **static-site prototype** with the architecture shaped to make backend integration straightforward. The frontend is built as if the API layer existed; the data layer (currently `localStorage`) is the only thing that needs replacing to go to production.

```
┌─────────────────────────────────────────────────────────────┐
│  Browser (HTML + CSS + JS)                                   │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │  index.html  │  │ provider.    │  │  seeker.     │      │
│  │  (Landing)   │  │  html        │  │  html        │      │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
│         │                 │                 │              │
│         └─────────────────┼─────────────────┘              │
│                           │                                 │
│  ┌────────────────────────▼─────────────────────────┐       │
│  │  app.js (init, search, map, mode switcher)       │       │
│  ├──────────────────────────────────────────────────┤       │
│  │  portal-ui.js (modals, forms, KYC, video, admin) │       │
│  ├──────────────────────────────────────────────────┤       │
│  │  portal.js (auth, KYC, deals, commissions)      │       │
│  ├──────────────────────────────────────────────────┤       │
│  │  data.js (properties, pricing, requirements)    │       │
│  └──────────────────────────────────────────────────┘       │
│                           │                                 │
│                  ┌────────▼────────┐                        │
│                  │  localStorage   │ ← (replace with API)  │
│                  └─────────────────┘                        │
└─────────────────────────────────────────────────────────────┘
```

**Three HTML pages**, each with `<body data-app="...">`. The `app.js#detectApp()` reads this and renders the right pricing block, locks the mode, and wires cross-app nav links.

---

## File map

```
realestate-app/
├── PRD.md                  # Product vision & design system (READ FIRST)
├── TECHNICAL.md            # This file
├── SECURITY.md             # Threat model & mitigations
├── README.md               # Project index
│
├── index.html              # Landing page
├── provider.html           # Provider app
├── seeker.html             # Seeker app
│
├── manifest.json           # PWA manifest
├── sw.js                   # Service worker (haven-ke-v2 cache)
│
├── icons/
│   ├── icon-192.png        # PWA icon
│   └── icon-512.png
│
├── css/
│   └── styles.css          # All styles (~3,500 lines)
│
└── js/
    ├── data.js             # PROPERTIES, KYC_REQUIREMENTS,
    │                       # PROPERTY_DOC_REQUIREMENTS,
    │                       # BROKER_COMMISSION_TIERS,
    │                       # PROVIDER_PRICING, SEEKER_PRICING,
    │                       # formatKES, validateMpesaPhone
    │
    ├── portal.js           # Portal IIFE — auth, KYC, deals,
    │                       # commissions, Security helpers
    │
    ├── portal-ui.js        # PortalUI IIFE — all modals,
    │                       # 3-step submit, KYC flow,
    │                       # video recorders (x2), admin dashboard
    │
    └── app.js              # Init, search/filter, explore map,
                            # PWA install, mode switcher,
                            # bottom nav, page detection
```

---

## Core modules

### `data.js` — Static data + helpers

- `PROPERTIES` (22 sample Kenya properties)
- `KYC_REQUIREMENTS` (per-role document checklist)
- `PROPERTY_DOC_REQUIREMENTS` (per-listing-status document checklist)
- `BROKER_COMMISSION_TIERS` (tiered 2%–6% + 1% rentals)
- `PROVIDER_PRICING` (4 plans + KYC + pay-per-post)
- `SEEKER_PRICING` (4 plans)
- `formatKES(n)` — `"KSh " + n.toLocaleString("en-US")`
- `validateMpesaPhone(raw)` — accepts `07XX`, `01XX`, `2547XX`, `2541XX`
- `calcBrokerCommission(priceKES, kind)` — returns `{pct, amount, tier}`

### `portal.js` — Backend-shaped API

The `Portal` IIFE exposes functions that **mirror what a real REST API would look like**. When you wire a backend, every function here becomes a `fetch()` call to that endpoint with the same return shape.

**Auth:**
- `Portal.signUp({name, email, phone, role})` → `{ok, user}`
- `Portal.signIn(email)` → `{ok, user}`
- `Portal.signOut()`
- `Portal.isLoggedIn()`, `Portal.isAdmin()`, `Portal.isBroker()`
- `Portal.state.user`, `Portal.state.myListings`, `Portal.state.payments`, `Portal.state.subscription`, `Portal.state.access`, `Portal.state.kycApplications`, `Portal.state.listingDocuments`, `Portal.state.commissions`, `Portal.state.deals`

**Listings:**
- `Portal.submitProperty(data, images)` → `{ok, property}`
- `Portal.approveListing(id)`, `rejectListing(id)`
- `Portal.getAllListings()`

**KYC:**
- `Portal.submitKYC(role, documents)` → `{ok, application}`
- `Portal.getMyKYC()`, `Portal.getAllKYC()`
- `Portal.approveKYC(id, notes)`, `Portal.rejectKYC(id, reason)`

**Listing documents:**
- `Portal.submitListingDocuments(listingId, documents, video)` — metadata only
- `Portal.getListingDocuments(listingId)`, `Portal.verifyListing(id)`

**Deals & commissions:**
- `Portal.closeDeal({listingId, seekerId, kind, priceKES, brokerId})`
- `Portal.getBrokerEarnings(brokerId)` → `{total, pending, earned, paid, deals, dealsList}`
- `Portal.getAllCommissions()`

**Security helpers (`Portal.Security`):**
- `esc(s)` — HTML escape
- `isEmail(s)`, `isPhoneKE(s)`
- `validFile(file, {allowed, maxSize})` — MIME + size allow-list
- `rateLimit(key, limit, windowMs)` — token bucket
- `validateVideoDuration(file, minSec, maxSec)` → Promise

**Admin:**
- `Portal.adminStats()` → counts for admin dashboard
- `Portal.ADMIN_CODE` = `"haven-admin-2025"` (hard-coded for demo)

### `portal-ui.js` — All UI

The `PortalUI` IIFE owns every modal and form. Exposed to `window.PortalUI`.

**Auth & users:**
- `showAuth(tab)` — sign in / sign up
- `showKYCFlow(role)` — full KYC submission
- `showKYCPending()`
- `showDashboard()` — user dashboard

**Listings:**
- `showSubmitProperty(defaultStatus)` — 3-step form
- `showPayment({amount, title, desc, onSuccess})` — M-Pesa + card
- `showBrokerApply()` (now wraps `showKYCFlow("broker")`)

**Admin:**
- `showAdminSignup()` — admin code entry
- `showAdminDashboard()` — 8 tabs (Overview, KYC, Brokers, Listings, Commissions, Users, Payments, Logs)

**Misc:**
- `close(modal)`, `renderListingPlan(plan)`, `renderAccessPlan(plan)`, `subscribeToPlan(plan)`, `subscribeToAccessPlan(plan)`

### `app.js` — Page-level wiring

- Page detection (`body[data-app]`)
- Mode switcher (legacy — replaced with cross-app nav links)
- Search & filter (hero searchbar, filter chips)
- Explore map (Leaflet, Kenya OSM tiles, custom price pins)
- Bottom nav (mobile)
- PWA install prompt
- Pricing render (page-aware: provider plans, seeker plans, or landing tabs)
- Admin link wiring
- Favorites drawer

---

## Page detection & cross-app navigation

```js
function detectApp() {
  return document.body.dataset.app || "landing";
}
```

Each HTML file declares its app:
- `index.html` → `<body data-app="landing" data-mode="seeker">`
- `provider.html` → `<body data-app="provider" data-mode="provider">`
- `seeker.html` → `<body data-app="seeker" data-mode="seeker">`

`initModeSwitcher()` reads the data-app:
- If `provider` → "Provider" button is active, clicking "Seeker" navigates to `seeker.html`
- If `seeker` → "Seeker" button is active, clicking "Provider" navigates to `provider.html`
- If `landing` → toggle stays in-page, persisted to localStorage

**Cross-app nav links** are regular `<a href="provider.html">` and `<a href="seeker.html">` — full page navigation, no SPA tricks.

---

## Data flow

### Sign up
1. User submits form → `Portal.signUp(...)` → user saved to `localStorage.haven.user` (or `haven.users` array)
2. If role is `provider/landlord/broker`, `setTimeout` → `PortalUI.showKYCFlow(role)`
3. User uploads docs + records video → `Portal.submitKYC(role, documents)` → application created
4. Admin reviews in `showAdminDashboard` → KYC tab → `Portal.approveKYC(id)` → `user.verified = true`

### List a property (provider flow)
1. `PortalUI.showSubmitProperty("For Sale" | "For Rent")` → modal opens
2. Step 1: basic info (type, location, price, beds/baths)
3. Step 2: features + photos + **required documents** + **non-paused video**
4. Step 3: review + payment (KSh 1,500) via M-Pesa / card
5. On payment success → `Portal.submitProperty(data, images)` + `Portal.submitListingDocuments(id, docs, video)`
6. Listing goes into `state.myListings` with `approvalStatus: "pending"`
7. Admin reviews in admin dashboard → "Listings" tab → `Portal.approveListing(id)` → goes live

### Close a deal (broker flow)
1. `Portal.closeDeal({listingId, seekerId, kind: "sale" | "rent", priceKES, brokerId})`
2. Tier is calculated: `calcBrokerCommission(priceKES, kind)` → `{pct, amount, tier}`
3. Deal + commission entries created
4. `getBrokerEarnings(brokerId)` aggregates them

---

## Storage

All state lives in `localStorage` under the `haven.*` namespace:

| Key | Contents |
|---|---|
| `haven.user` | Current user (if logged in) |
| `haven.users` | All users (keyed by email) |
| `haven.brokerApps` | Broker applications |
| `haven.kycApplications` | KYC submissions |
| `haven.myListings` | User-submitted listings |
| `haven.payments` | Payment history |
| `haven.listingDocuments` | Per-listing document metadata |
| `haven.deals` | Closed deals |
| `haven.commissions` | Broker commission records |
| `haven.securityLog` | Append-only audit |
| `haven.favs` | Saved homes (seeker) |
| `haven.subscriptions` | Provider subscriptions |
| `haven.access` | Seeker subscriptions |
| `haven.adminLog` | Admin action log |
| `haven.mode` | Last mode toggle (landing page only) |

**Privacy note:** KYC and listing documents are stored as **metadata only** — `{name, size, type, uploadedAt}`. No blobs. The "Verified" badge is all seekers ever see.

---

## Security

See [`SECURITY.md`](./SECURITY.md) for the full threat model. Highlights:

- CSP meta tag: `default-src 'self' 'unsafe-inline' data: blob: https://*.tile.openstreetmap.org ...`
- `Referrer-Policy: strict-origin-when-cross-origin`
- All user content escaped via `Portal.Security.esc()` before `innerHTML`
- File upload validated by MIME + size (`Portal.Security.validFile`)
- Video upload validated by duration (`Portal.Security.validateVideoDuration`)
- Rate limiting per action key
- Admin code gated behind `Portal.ADMIN_CODE` (demo only — replace with real RBAC in production)

---

## Demo vs. production

| Feature | Demo (this build) | Real production |
|---|---|---|
| M-Pesa | STK push UI flow with 92% simulated success | Safaricom Daraja `/mpesa/stkpush/v1/processrequest` + signed B2C callback |
| Card | Form validation + simulated 3D-secure | Stripe / Flutterwave / Pesapal tokenization (PCI-DSS scope) |
| KYC | Client-side validation, admin approves manually | OCR + face liveness (e.g. Smile Identity) + EATB API check |
| Documents | Metadata only in localStorage | S3 / R2 private bucket, signed time-limited URLs, virus scan, ffmpeg re-encode |
| Auth | Plain localStorage | OAuth 2.0 + SMS OTP + JWT (15min) + refresh tokens in httpOnly cookies |
| Database | localStorage | Postgres with row-level security, daily backups |
| Maps | Leaflet + OSM | Google Maps Platform (better coverage in Kenya) |
| Email / SMS | None | SES / Twilio with verified senders |
| Hosting | Static deploy (Tengine) | Cloudflare or AWS with WAF + DDoS protection |

---

## Migration plan to Tailwind (per PRD §7) — in progress

The current build uses handcrafted CSS. The PRD mandates Tailwind. Migration started in v10.

### What's done (v10)

✅ **Tailwind v3 installed** via npm (`tailwindcss` + `tailwindcss-animate`)
✅ **`tailwind.config.js`** with PRD §8 theme tokens: `primary` (Deep Navy), `accent` (Royal Gold), `success/warning/error`, `glass` utilities, custom keyframes (`fade-in-up`, `pin-drop`, `shimmer`, etc.), custom shadows, ease-spring timing
✅ **`css/tailwind.css`** source file with `@tailwind base/components/utilities` + custom `@layer components`:
  - `.glass`, `.glass-strong`, `.glass-dark`, `.glass-soft` — PRD §9 glassmorphism
  - `.btn`, `.btn-primary`, `.btn-accent`, `.btn-ghost`, `.btn-outline`
  - `.input`, `.input-search`
  - `.eyebrow`, `.h-display`
  - `.chip`, `.chip-active`, `.badge`, `.badge-success/warning/error/accent/ink`
  - `.card`, `.card-body`, `.card-hover`
  - `.section`, `.section-soft`, `.container-app`
  - `.nav-link`, `.nav-pill`, `.nav-pill-active`
  - `.plan`, `.plan-best`
  - `.float`, `.fab`
  - `.skeleton`
  - Custom utilities: `.text-gradient`, `.scrollbar-hide`, `.glass-highlight`
✅ **Build script** — `npx tailwindcss -i ./css/tailwind.css -o ./css/tw.css`
✅ **`index.html` (landing) fully migrated** to Tailwind utility classes
✅ **Dark mode** — `class="dark"` on `<html>`, toggled via button + persisted to localStorage, respects `prefers-color-scheme` on first visit
✅ **Glassmorphism** — every major card uses `.glass` / `.glass-strong` with `backdrop-blur-md` + translucent borders
✅ **Animation system** — `animate-fade-in-up`, `animate-pulse-soft`, hover lift on cards
✅ **`prefers-reduced-motion` respected** — global CSS rule disables animation
✅ **Zero JS errors** across all 3 pages

### What's still legacy

🟡 `provider.html` and `seeker.html` still use the old handcrafted CSS (`css/styles.css`)
🟡 The dynamic modals (auth, KYC, payment, admin dashboard) still use legacy classes because they're generated by JS template strings (`portal-ui.js`)
🟡 The 240+ legacy component classes in `styles.css` are still loaded for the non-landing pages
🟡 Dark mode only fully applies on the landing page (other pages need their Tailwind equivalents)

### Migration pattern (for the next person)

For each page, the workflow is:

1. **Migrate static HTML** to Tailwind utility classes (use landing as the reference)
2. **Migrate JS template strings** — replace each legacy class with its Tailwind equivalent (or create a `legacy-aliases.css` bridge)
3. **Apply glassmorphism + dark mode** as you go
4. **Test in browser** for visual regression
5. **Remove legacy CSS rules** that are no longer used (can use PurgeCSS to detect)
6. **Update tests/screenshots**

### Component-class bridge strategy

For dynamic modals rendered by JS, two options:

**Option A** — Rewrite JS template strings to use Tailwind classes (verbose, but cleanest)
**Option B** — Create a `css/legacy-bridge.css` that maps legacy class names to Tailwind utilities:

```css
.admin__head { @apply flex items-center justify-between mb-6 pb-4 border-b border-ink/8; }
.plan { @apply glass-strong p-8 flex flex-col; }
.btn { @apply inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-full font-medium text-sm; }
```

This is faster but requires Tailwind to scan the bridge file. Option A is the long-term answer; Option B is the migration tool.

### Estimated effort to complete

- Migrate `seeker.html` (medium complexity, mostly static): **1–2 days**
- Migrate `provider.html` (high complexity, lots of sections): **2–3 days**
- Migrate `portal-ui.js` template strings: **3–5 days** (most tedious)
- Remove legacy `styles.css`: **1 day**
- Full dark mode pass: **1 day**
- **Total: ~2 weeks for a single engineer with Tailwind experience**

### Build commands

```bash
# Install dependencies
npm install

# Build CSS once
npx tailwindcss -i ./css/tailwind.css -o ./css/tw.css

# Build with minification
npx tailwindcss -i ./css/tailwind.css -o ./css/tw.css --minify

# Watch for changes during development
npx tailwindcss -i ./css/tailwind.css -o ./css/tw.css --watch
```

---

## Running locally

```bash
cd realestate-app
python3 -m http.server 8765
# open http://localhost:8765
```

Or any static host. No build step required for the current prototype.

For Tailwind migration (next iteration):
```bash
npm init -y
npm install -D tailwindcss
npx tailwindcss -i ./css/input.css -o ./css/styles.css --watch
```

---

## Testing

Currently manual + Playwright smoke tests in `/workspace/*.js`. To run:

```bash
node /workspace/three_test.js  # loads each page, checks for errors
```

For full test coverage, target:
- Unit tests for `Portal.*` (auth, KYC, deals, commissions)
- Unit tests for `calcBrokerCommission` and `validateMpesaPhone`
- E2E tests for signup → KYC → list property → close deal
- E2E test for Seeker: browse → save → contact
- Visual regression tests (Percy / Chromatic)
- Accessibility tests (axe-core in CI)
- Performance tests (Lighthouse CI)

---

## Open technical debt

1. **Tailwind migration** (PRD §7) — top priority
2. **Dark mode** (PRD §8) — tokens defined, no implementation
3. **Glassmorphism pass** (PRD §9) — partial, needs to be systematic
4. **Animation system** (PRD §10) — basic transitions only, needs unified motion language
5. **Real backend** — `Portal.*` functions need to be wired to `fetch()` calls
6. **Form data preservation** — multi-step submit form loses state on accidental refresh
7. **Map performance** — 22 markers is fine, but for 1000+ listings need clustering
8. **Bundle size** — current single JS files total ~80KB minified; fine for now

---

## Custom domain setup

See `DNS-CONFIG.md` for the full guide. TL;DR:

- The deployment platform is `space.minimax.io` (Tengine on Alibaba Cloud, backed by OSS + `cdngslb.com` CDN).
- Point `haven.apolloadverts.net` at the deployment via CNAME.
- DNS zone: add a CNAME record, host `haven`, value `an60t9lzhbxak.space.minimax.io` (or current deployment URL).
- SSL: auto-issued by most hosts via Let's Encrypt.
- Verify with `bash scripts/verify-dns.sh haven.apolloadverts.net`.

**Critical caveat:** Not all static deployment platforms support custom domains natively. If the current platform doesn't, the site won't load at the custom domain even with a perfect CNAME. Workaround: redeploy to Cloudflare Pages / Netlify / Vercel — all support custom domains for free.

---

*See [`PRD.md`](./PRD.md) for the product vision driving these technical decisions.*
