# Competitive Study — HOMEX (Condo-Management SaaS Admin/Agent Surface) **Target:** https://www.homex.ph/ **Method:** Clean-room, public-signal-only reconnaissance (`robots.txt`, `sitemap.xml`, homepage/login HTML, public Next.js JS chunks — including the app's own i18n string table, which was shipped in full to unauthenticated clients — and the two documented public JSON endpoints, `/api/public/buildings` and `/api/public/units`). No login, no credential submission, no authenticated route access, no user enumeration. Every inference is marked `(inferred)`; everything else is a direct observation with a quoted URL/path/string. **Date observed:** 2026-07-11. --- ## 1. Admin surface overview HOMEX is **not a listings marketplace with a bolted-on admin panel** — it is a **condo/rental **property-management SaaS** whose entire product *is* a multi-role back office**, with a thin public marketing/browse layer (`/`, `/units`, `/units/{id}`, `/about`, `/contact`) sitting in front of it. The landing page states this outright: > `landing.feature1.title`: "Four-Role System" — `landing.feature1.desc`: "Admin, agents, landlords, and tenants each get a tailored dashboard with the right level of access." There is **one gated route tree**, `/dashboard` and its children (`robots.txt` explicitly disallows `/dashboard/` and `/api/`), served from the same Next.js app as the public marketing pages — there is no separate admin subdomain, no separate framework, no separate auth provider. Gating is done server-side (Next.js middleware issuing HTTP 307 redirects to `/login` for any unauthenticated request under `/dashboard/*`, and for `/register` — see §2), not client-side route hiding. All four roles (Admin, Agent, Landlord, Tenant) share the **same `/dashboard` URL**; the role-specific view (labels, nav sections, data scope) is rendered client-side based on the authenticated session, not via distinct per-role paths — confirmed by role-scoped i18n strings such as `dashboard.admin.subtitle`: "Global overview — all agents, condos & leases" existing alongside a single generic `dashboard.title`/`dashboard.subtitle` pair, and `nav.adminSection`: "Admin" as a conditionally-shown nav group rather than a separate app shell. Auth is classic **email + password** (`POST /api/auth/login`, JSON body `{email, password}`) with a forced first-login password change (`first_login` flag in the login response routes to `/change-password` before `/dashboard`) and a token-based reset flow (`/reset-password/{token}`, `/forgot-password`). No OAuth/SSO, no magic link, no OTP/SMS was observed in the shipped JS. Self-service sign-up appears to have been **built but is currently disabled in production**: the i18n table still ships a full registration vocabulary (`register.iAm`: "I am a...", `register.createAccount`, `register.phone`, etc.) and a `/register` route exists in the route tree, but every request to `/register` is unconditionally 307-redirected to `/login` server-side (Next.js `NEXT_REDIRECT` digest observed directly in the RSC payload). This is consistent with the product's actual account-creation model, which is **admin/agent-provisioned, not self-serve**: creating a lease auto-creates a Tenant account from an email address, and creating a unit auto-creates a Landlord account from the owner's email (`leases.tenantEmail`: "Tenant Email (auto-creates account)"; `units.ownerEmailHint`: "If the owner email is new, a landlord account will be auto-created and an invitation email sent."). Agent accounts are the one type provisioned explicitly by an Admin (`accounts.newAgent`: "New Agent Account"). --- ## 2. Discovered admin/agent entry points All paths below were checked directly with `curl` against `https://www.homex.ph` and are quoted verbatim. | Path | Observed behavior | |---|---| | `/robots.txt` | `Disallow: /dashboard/` and `Disallow: /api/`; `Sitemap: https://www.homex.ph/sitemap.xml`. This is the first public signal that a `/dashboard` tree exists. | | `/sitemap.xml` | Only public marketing/browse URLs indexed: `/`, `/units?building_id={1..7}`, `/units/{id}` (5 unit detail pages, priority 0.8). No admin paths listed (expected — they're behind auth). | | `/login` | HTTP 200. Plain email/password form, EN/中文 language toggle, "Forgot Password?" link. Client bundle: `app/login/page-8219dd6e7bb9d5c5.js`. | | `/register` | HTTP 307 → `/login` (server-side `NEXT_REDIRECT`, unconditional). Self-serve signup is wired in the codebase (i18n strings present) but disabled/removed from the live flow. | | `/dashboard` | HTTP 307 → `/login` (unauthenticated). | | `/dashboard/accounts` | HTTP 307 → `/login`. Admin-only user/account management (§4). | | `/dashboard/units` | HTTP 307 → `/login`. | | `/dashboard/leases` | HTTP 307 → `/login`. | | `/dashboard/rent` | HTTP 307 → `/login`. | | `/dashboard/commission` | HTTP 307 → `/login`. | | `/dashboard/notifications` | HTTP 307 → `/login`. | | `/dashboard/chat` | HTTP 307 → `/login`. | | `/change-password` | HTTP 200 (accessible pre-full-login as part of the forced first-login flow). | | `/forgot-password` | HTTP 200. | | `/reset-password/{token}` | HTTP 200 (dynamic route, e.g. `/reset-password/abc123`); `/reset-password` with no segment is 404 — token is a path segment, not a query param. | | `/api/auth/login` | POST-only JSON auth endpoint (found via static analysis of `login/page-*.js`; not invoked). | | `/api/public/buildings`, `/api/public/units` | Public, unauthenticated JSON (see §6) — explicitly in scope per task instructions. | | `/api/img-wm?src={path}` | Public GET, returns `image/jpeg` — **server-side image watermarking** of unit photos (JS: `"/api/img-wm?src=".concat(encodeURIComponent(e))`), confirmed live against `/uploads/units/studio-bayview-01.png`. | | `admin.homex.ph`, `app.homex.ph`, `agent.homex.ph`, `portal.homex.ph`, `dashboard.homex.ph`, `api.homex.ph`, `staging.homex.ph`, `docs.homex.ph` | All HTTP 404 — **no admin/agent subdomain exists**; everything lives on `www.homex.ph`. | | `/pricing`, `/plans`, `/features`, `/faq`, `/help`, `/docs`, `/terms` | All HTTP 404 — no public pricing/docs/help pages exist. | | `/privacy` | HTTP 200 (only legal page found). | | No CSP header | The homepage response has no `Content-Security-Policy` header at all (only `strict-transport-security`) — no CSP-derived host list was available as a discovery signal for this target (unlike some other portals). | --- ## 3. Role & account model Confirmed directly from the `role.*` i18n keys shipped in the global layout bundle (`chunk 768`): | Role | Label (EN) | Description string (`role.{role}.desc`) | |---|---|---| | `admin` | Admin | "Full access — manage all condos, units, leases & rent" | | `agent` | Agent | "Manage condos, leases & rent for clients" | | `landlord` | Landlord | "Track your condos & rent income" | | `tenant` | Tenant | "View your lease & pay rent on time" | Account lifecycle, reconstructed from `accounts.*` and `leases.*`/`units.*` strings: - **Admin-created Agent accounts**: `accounts.newAgent` ("New Agent Account"), `accounts.passwordPlaceholder` ("Set initial password"), `accounts.tempPassword`/`accounts.tempPasswordNotice` ("Share this password with the user. They will be asked to change it on first login.") — admin sets an initial/temp password and the system forces a change on first login (matches the `first_login` flag observed in the `/api/auth/login` response-handling code). - **Auto-created Landlord accounts**: triggered by entering a new owner email while creating/editing a Unit (`units.ownerEmailHint`). - **Auto-created Tenant accounts**: triggered by entering a new tenant email while creating a Lease (`leases.tenantEmail`, `leases.tenantEmailsHint` — supports multiple comma-separated tenant emails/names/phones per lease, i.e. co-tenants). - **Account admin actions** (`accounts.*`): Create, Disable/Enable (`accounts.disable`/`accounts.enable`/`accounts.disabled`), Reset Password (generates a new temp password — `accounts.confirmReset`), Delete (`accounts.confirmDelete`), search by name/email (`accounts.searchPlaceholder`), filter/sort by Role and Status. This is a single unified "Accounts" admin screen covering **agents, landlords, and tenants** together (`accounts.subtitle`: "Manage all user accounts — agents, landlords, and tenants"). - **No self-serve registration** in production (§1/§2) — despite `register.iAm` ("I am a...") suggesting a formerly-live role-picker signup form. - **No organization/brokerage/tenant-of-tenant concept observed**: the public unit data (§6) shows a single agent (`agent_id: 2`, "Maria Santos") across the whole seeded dataset, and no `organization_id`/`brokerage_id`/`company` field appears anywhere in the shipped strings or public API — this reads as a **single-tenant-per-deployment** SaaS (one HOMEX instance = one property-management office with its own admin + several agents/landlords/tenants), not a multi-brokerage marketplace or white-label platform (not observed). --- ## 4. Admin feature inventory Reconstructed from the ~500-key i18n table shipped in `chunk_768` (translation namespaces map directly onto admin screens/nav items: `nav.accounts`, `nav.properties`, `nav.units`, `nav.leases`, `nav.rent`, `nav.commission`, `nav.notifications`, `nav.chat`, `nav.changePassword`, `nav.adminSection`). **Listing / unit management** (`units.*`, `nav.units`/`nav.properties`) - Full unit CRUD: unit number, floor, area (sqm), unit type, monthly rent, description, tags, status (`available` / `leased` / `maintenance`). - Owner fields directly on the unit: owner name, owner contact, owner email (`units.ownerEmailHint` — new owner email auto-creates a Landlord account + sends an invitation email). - Duplicate-unit guard: `units.duplicateUnit`: "This unit already exists in this condo and cannot be duplicated." - Image management: multi-image upload (`units.uploadImages`, "JPEG, PNG, WebP, max 5MB each"), gallery view — images served publicly watermarked via `/api/img-wm` (§2). - Filters: by City, Condo, Tower, Status; free-text search on unit number/type/owner. **Building/property (condo) management** (`buildings.*`, `nav.properties`) - Condo CRUD: name, address, city, comma-separated tower list, "Admin Contact (optional)", "Move in requirements (optional)". - Filter by city; search by condo name; drill into a condo to view/hide its units inline (`buildings.viewUnits`/`buildings.hideUnits`). **Tenant/lease management** (`leases.*`, `nav.leases`) - Lease CRUD scoped to a specific Condo → Tower → Unit (cascading selects), with **auto-linked Landlord** (from the unit's owner email, `leases.landlordAutoLinked`) and multi-tenant support (comma-separated tenant names/emails/phones on one lease). - Lease terms: start/end date, deposit (₱), payment frequency (Monthly / Every 2 Months / Quarterly / Semi-Annual / Annual / Other). - Document handling: **Upload Contract** and **Upload Receipt** (PDF, images, or ZIP/RAR/7Z, max 20MB), with an "Uploaded"/"Not uploaded" state per document. - Deleting a lease cascades: `leases.confirmDelete` warns it "will also remove all associated rent records, commissions, and notifications. The unit status will be reset to available." — i.e. Lease is the parent record for Rent, Commission, and Notification records in the data model. - Filters: by City, Condo, Tower, Agent; lease status Active/Ended. **Rent-payment & proof-of-payment workflow** (`rent.*`, `nav.rent`) — the deepest feature area observed - A lease generates a **Payment Schedule** (`rent.schedule`, `rent.paymentsGenerated`: "payment records generated") — an installment plan derived from lease term × payment frequency, not one flat "rent" field. - Per-installment states: Pending, Paid, Overdue, "Awaiting confirmation" (`rent.awaitingConfirmation`). - **Tenant-side**: Upload Proof (`rent.uploadProof`) against a due installment. - **Admin/Agent/Landlord-side**: View Proof (`rent.viewProof`), Confirm Payment (`rent.confirmPayment` / `rent.confirmPaymentQ`: "Confirm this payment? This will mark the rent as PAID."), records who confirmed it (`rent.confirmedBy`), and a Revert action (`rent.revert`) to undo a confirmation. - Automated overdue handling: `rent.overdueAlert`, `rent.overdueMsgSingular/Plural`: "rent record(s) is/are overdue. All parties have been notified." — plus tiered reminders `rent.reminder10d`/`reminder3d`/`reminderDue`/`reminderOverdue` ("Due in 10 days" / "3 days" / "today" / "Overdue"), all synced to agent, landlord, and tenant simultaneously (`rent.subtitle`: "All rent records — synced across agent, landlord & tenant"). - Rich filtering (City/Condo/Tower/Unit/Agent) and per-payment history expansion (`rent.clickToExpand`: "Click to view all payments"). **Commission tracking** (`commission.*`, `nav.commission`) - A distinct **access-gated** module: `commission.accessDenied`: "You do not have access to commission data" — i.e. commission visibility is role-restricted (inferred: Admin + the owning Agent only, not Landlord/Tenant), unlike Rent which is explicitly "synced" to all three parties. - Records: Agent, Condo, Tower, Unit, Move-in Date, Commission amount (PHP), Status (Paid/Unpaid), Paid Date. - Manual creation (`commission.add`/`commission.addTitle`: "Add New Commission" — a manual override path alongside whatever auto-generates commission records from closed leases, `commission.subtitle`: "Agent commission tracking for closed leases"). - Toggle Mark Paid / Mark Unpaid; a **Monthly Summary** view (`commission.monthlySummary`) with aggregate stats: Total Commission, Total Leases, Paid/Unpaid counts and amounts, "Uncollected" — filterable by Agent, City, Condo, Month, Status. **Media** - Unit image gallery upload (see Listing mgmt above); no separate DAM/media-library screen was observed (not observed) — media appears to be scoped per-unit only. **Lead/inquiry (chat)** (`chat.*`, `nav.chat`) - An in-app **Conversations** inbox (agent-facing): list of conversations, unread indicator, Mark Read, Close/Reopen a conversation. - Public-side inquiry entry point is a unit-detail "Contact Agent" action that opens **Telegram** (`chat.contactAgent`: "Contact Agent on Telegram"; `chat.telegramPrompt`: "Open Telegram to chat with the agent about this unit.") — suggesting the in-app chat inbox is a system of record/handoff layer around a Telegram-based actual conversation channel, seeded with a per-unit inquiry template (`chat.unitInquiry`: "Inquiry about {condo} — Unit {unit}"). - No CRM pipeline stages (new/contacted/qualified/won) were observed — this is a message inbox, not a lead-scoring CRM (not observed beyond read/unread + open/closed). **Analytics/reporting** - **Not observed as a dedicated screen.** The only aggregate numbers found are the per-dashboard summary tiles (Active Leases, Pending Rent, Overdue, Total Collected, Outstanding Balance, Condos/Units counts — `dashboard.*`) and the Commission Monthly Summary (above). No `analytics`, `report`, `export`, or `csv` feature strings were found anywhere in the shipped JS beyond generic React/webpack internals (a false-positive check was run and excluded). No CSV/PDF export action exists in the UI vocabulary. **Billing/subscription** - **Not observed.** No `billing`, `subscription`, `plan`, `invoice`, or `checkout` strings anywhere in the app bundles; no `/pricing` page exists (404). HOMEX's own monetization model is not discoverable from the public surface — the product doesn't appear to bill *its own* end customers (the property-management offices) through the app itself (inferred: likely sold directly/offline, or this is a pre-monetization/demo-stage build). **Team/sub-account management** - Covered by the unified Accounts screen (§3) — Admin creates/disables/deletes Agent (and, indirectly, Landlord/Tenant) accounts from one table. No org-chart, permission-group, or seat-count concept was observed (not observed). **Notifications** (`notifications.*`, `nav.notifications`) - A notification center: unread count/badge (singular/plural strings), "Mark all as read," "All caught up!" empty state, notifications reference a specific Unit. Fed by the rent-reminder engine (§ above) and presumably lease/commission events (inferred). **Verification/moderation** - **Not observed.** No listing-approval, "verified," "boost/featured," or moderation-queue strings/fields were found anywhere (checked explicitly: `verify`, `moderat`, `approve`, `boost`, `feature` — only generic UI "feature" hits, no product concept). Consistent with HOMEX's positioning as an internal ops tool for a property-management team managing *their own* inventory, not a multi-party marketplace needing trust/verification gates. --- ## 5. Implementation & tech - **Next.js App Router**, confirmed by chunk naming (`app/layout-*.js`, `app/page-*.js`, `app/login/page-*.js`), the `x-matched-path` response header, RSC payload format (`self.__next_f.push(...)`), and `NEXT_REDIRECT` digests in redirect responses. - Hosted on **Vercel** (`server: Vercel` header, `x-vercel-cache`, `x-vercel-id`). - Client-side i18n via a custom `I18nProvider`/`useI18n` hook (`t()` function, `lang`/`setLang`), **English and Simplified Chinese (`zh`)** — notably *not* Japanese/Filipino, despite the product being Philippines-focused; the whole `role.*`/`nav.*`/`dashboard.*`/etc. string table (~500 keys × 2 languages) ships in a single shared client chunk (`chunk 768`) loaded on every page, which is what made this entire feature inventory (§4) readable without authentication. - Auth: custom email/password (`POST /api/auth/login` → JSON `{email, password}`, response includes a `first_login` flag), no third-party auth provider script (no Auth0/Clerk/Firebase/NextAuth signal found). - Access control: **server-side middleware redirect** (307 → `/login`) on protected routes, not a client-only guard — confirmed by the redirect happening before any dashboard JS/data is served (the RSC payload itself contains `NEXT_REDIRECT` and no dashboard component tree). - Image pipeline: uploads stored under `/uploads/units/{filename}`, served **watermarked** through a first-party API route `/api/img-wm?src=...` rather than directly — a deliberate anti-scraping/anti-poaching measure for listing photography. - Analytics: Google Analytics 4 only (`gtag.js`, measurement ID `G-NDRSVM1GCM`); no CSP header, no separate `/api.homex.ph`-style API host (all API routes are same-origin under `/api/*`). - Route map observed directly (static + dynamic segments): `/`, `/units`, `/units/[id]`, `/about`, `/contact`, `/privacy`, `/login`, `/register`, `/forgot-password`, `/reset-password/[token]`, `/change-password`, `/dashboard`, `/dashboard/accounts`, `/dashboard/units`, `/dashboard/leases`, `/dashboard/rent`, `/dashboard/commission`, `/dashboard/notifications`, `/dashboard/chat`. --- ## 6. Data model glimpsed from `/api/public/*` **`GET /api/public/buildings`** → `{ "buildings": [ {id, name, city, towers} ] }` - 4 buildings observed, e.g. `{"id":1,"name":"BGC High Street Condo","city":"Taguig","towers":"Tower 1, Tower 2"}`. `towers` is a **denormalized comma-separated string**, not an array/child table (matches the admin form field `buildings.towers`: "Towers (comma-separated, e.g. Tower 1, Tower 2)"). **`GET /api/public/units`** → `{ "units": [ {...} ] }` - 5 units observed. Full field list per unit: `id, building_id, tower, floor, area, unit_type, unit_number, owner_name, owner_contact, owner_email, monthly_rent, description, images, status, created_at, tags, building_name, building_city, building_towers, agent_id, agent_name, agent_avatar`. - `images` is a **comma-separated string of paths** (e.g. `"/uploads/units/studio-bayview-01.png,/uploads/units/Modern_studio_apartment_interi_2026-07-07T02-52-54.png"`), not a normalized image table/array. - `tags` is a free-text/comma-separated field (observed values: `null`, `"balcony,pet"`). - **Notable data-shape/exposure finding:** this "public" browse API returns the **landlord's personal contact details unredacted** — `owner_name`, `owner_contact` (e.g. `"+63 919 345 6789"`), and `owner_email` (e.g. `"anna@homex.ph"`) — to any anonymous caller, on every unit, alongside the assigned agent's name. This is denormalized directly from the admin `units` table (the exact fields editable in the admin Unit form, `units.ownerContact`/`units.ownerEmail`/`units.ownerName`) with no field-level allowlist between the internal data model and the public API response. - Single agent (`agent_id: 2`, `agent_name: "Maria Santos"`) across the entire seeded/demo dataset — consistent with a single-office deployment, not a multi-agent marketplace at scale (in this instance). --- ## 7. Takeaways for property-portal's future admin/CMS 1. **A rent/installment schedule generated from lease terms — not a single "rent amount" field — is the single highest-leverage admin data-model decision observed here.** HOMEX derives a `Payment Schedule` from `lease.start_date/end_date × payment_frequency`, producing individually-trackable installments with their own status (Pending/Paid/Overdue/Awaiting confirmation) and proof-of-payment attachment. For property-portal's eventual admin, model rent/installments as a **child collection of a Lease**, generated on lease creation, rather than a scalar field — this is what unlocks reminders, overdue detection, and per-installment proof workflows cheaply later. 2. **Auto-provision counterpart accounts from email addresses entered in another entity's form, instead of building separate self-serve signup flows for every role.** HOMEX creates Landlord accounts from a Unit's owner-email field and Tenant accounts from a Lease's tenant-email field(s), with an invitation email and forced first-login password change. This collapses "account management" into "data entry" for the admin/agent, and is worth adopting directly for property-portal's future Landlord/Tenant surfaces instead of a traditional multi-role registration form (which HOMEX itself built, then apparently disabled — see §1/§2). 3. **A proof-of-payment upload + admin/landlord confirm-with-revert workflow is a small, well-scoped feature that produces outsized trust value** — tenant uploads proof, an authorized party (admin/agent/landlord) confirms it (recording who confirmed and when), with a revert escape hatch for mistakes. This is cheap to build (a status enum + an attachment + an actor/timestamp pair) and directly addresses the "did my payment go through?" anxiety HOMEX's own marketing copy calls out as the core pain point (`about.story1`). 4. **Gate sensitive aggregate data (commission-style financials) with an explicit access-denied state at the module level, separately from the general auth check**, the way HOMEX's `commission.accessDenied` string implies a second, role-specific authorization layer distinct from "logged in or not." Plan for property-portal's future admin to have at least two authorization tiers from day one: "authenticated dashboard access" and "authorized-for-this-module" (financials/commission being the obvious first candidate), rather than bolting it on after a flat RBAC check ships. 5. **Treat the public listing/browse API as a DTO boundary, not a straight table dump, from the very first version.** HOMEX's `/api/public/units` leaks the landlord's name, phone, and email to any anonymous caller because the public endpoint appears to return the same row shape as the internal admin table. property-portal should define an explicit public-facing serializer/allowlist for any `/api/public/*`-style route before it ships, specifically excluding any owner/landlord/agent PII field that isn't meant to be public (agent name/avatar is presumably intentional for contact purposes; owner phone/email is very likely not). 6. **Consider watermarking listing photography server-side on the fly (`/api/img-wm?src=...`) rather than storing pre-watermarked copies**, if/when property-portal starts hosting owner- or agent-submitted photos at any scale — it's a low-cost anti-scraping/anti-poaching measure (one image-processing API route) that HOMEX ships even at what otherwise looks like an early/demo-stage product maturity, suggesting it's cheap enough to be worth doing early rather than retrofitting. --- ## Admin matrix row | Roles | Admin entry points | Listing mgmt (bulk/feed?) | Lead CRM | Analytics | Promotion/boost admin | Billing/credits | Team/brokerage mgmt | Developer/project mgmt | Verification/moderation | White-label CRM product | Tech/implementation | Standout admin capability | |---|---|---|---|---|---|---|---|---|---|---|---|---| | 4 roles: Admin, Agent, Landlord, Tenant — one shared `/dashboard` shell, role-scoped views | `/login` (email/password), `/dashboard` + children (`/dashboard/accounts`, `/units`, `/leases`, `/rent`, `/commission`, `/notifications`, `/chat`) all server-redirect (307) to `/login` when unauthenticated; `/register` exists in the route tree but is currently force-redirected to `/login` (self-serve signup disabled) | Per-unit CRUD only; no bulk-edit/CSV-feed endpoint observed — not observed | Basic conversation inbox (read/unread, close/reopen) handed off to Telegram for the actual chat; no pipeline/stages — not observed beyond that | Not observed as a dedicated screen — only dashboard summary tiles + Commission Monthly Summary; no export/CSV/report feature strings found | Not observed (no `featured`/`boost` fields or strings found) | Not observed — no `/pricing`, no billing/subscription/invoice strings anywhere in the app | Single unified Accounts screen (Admin creates/disables/resets/deletes Agent, Landlord, Tenant accounts); no org/brokerage/multi-tenant concept observed | Not observed — no developer/project entity or content type found | Not observed — no listing verification/approval/moderation queue or fields found (single-office internal tool, not a marketplace) | Not observed — no evidence of a separate white-label CRM product; this *is* the product | Next.js (App Router) on Vercel, server-side middleware auth redirects, same-origin `/api/*` routes, GA4 only, no CSP header, EN/中文 i18n shipped in full to every client | **Rent-payment schedule + proof-of-payment confirm/revert workflow synced live across agent, landlord, and tenant**, paired with tiered automated overdue reminders (10d/3d/due-today/overdue) — the deepest, most fully fleshed-out feature area in the whole product |