Skip to content
Engineering Atlas
SystemsShelter4U — Property Brokerage & Channel Partners CRM

Shelter4U — Property Brokerage & Channel Partners CRM

Led development of a property brokerage CRM covering the full lead-to-closure lifecycle, self-deployed on AWS EC2 behind NGINX, and directly diagnosed the production issues that came with owning that infrastructure.

Status
Archived
Timeline
Apr 2023 – Dec 2025
Reading Time
6 min
Domains
Full-Stack Architecture, Deployment & Infrastructure, Access Control
Technologies
React, TypeScript, NestJS, PostgreSQL, AWS EC2, NGINX, PM2
Contents

Executive Summary

Shelter4U is a CRM for a property brokerage's channel partners, built to own the full lead-to-closure lifecycle — lead intake, follow-ups, site visits, and deal finalization — as one connected flow rather than disconnected tools. Led development and owned the technical decisions end to end, including a relational model mapping Clients to the Properties they're interested in, backend RBAC via NestJS Guards, and a self-managed deployment on AWS EC2 with PM2 and an NGINX reverse proxy under a unified domain with SSL. Also diagnosed and resolved the production issues that came with running that infrastructure directly — permission errors, NGINX rewrite loops, SQL enum mismatches, and database constraint violations.

Business Context

Problem
A property brokerage needed a CRM for its channel partners covering the entire lead-to-closure lifecycle — from initial lead intake through follow-ups, site visits, and deal finalization — with Properties and Clients modeled relationally so that a client's interest across multiple properties, and a property's interest from multiple clients, stayed consistent as leads moved through stages.
Users
Channel partners and internal staff managing property leads from intake through deal closure, with role-specific access to CRM modules.
Business Goals
Give one system ownership of the entire lead-to-closure flow instead of splitting it across disconnected tools. Deploy and operate the platform independently on self-managed infrastructure.
Success Metrics
Leads tracked consistently from intake to closure without gaps between stages. Production incidents in the self-managed stack diagnosed and resolved directly, without depending on a managed platform to abstract them away.
Environment
Production system self-deployed on AWS EC2 (Ubuntu), process-managed with PM2, served through an NGINX reverse proxy under a unified domain with SSL.
Stakeholders
Owned technical decisions across the system; channel partners and property brokerage staff as end users.

Constraints

Full lifecycle ownership

Technical decisions spanned the entire lead-to-closure flow — intake, follow-ups, site visits, deal finalization — so the data model had to hold up consistently across every stage, not just one module.

Self-managed infrastructure

There was no managed platform underneath the application. EC2, PM2, NGINX, and SSL were all configured and operated directly, which meant infrastructure failure modes — permissions, reverse-proxy config, process supervision — were fully owned rather than abstracted away.

Unified domain routing

The SPA frontend and backend API had to be served under one domain through NGINX, making reverse-proxy configuration a central, correctness-critical piece of the deployment rather than an incidental detail.

Relational integrity for many-to-many interest mapping

Clients and Properties needed structured relational mapping so a client's interest across multiple properties, and vice versa, stayed consistent as leads progressed through stages.

System Blueprint

A React SPA is served, together with the backend API, through a single NGINX reverse proxy on a unified domain, talking to a NestJS backend that enforces RBAC and reads/writes a relational PostgreSQL schema for Properties and Clients.

Frontend

React + TypeScript SPA covering lead intake, follow-ups, site visits, and deal finalization.

ReactTypeScript

API Layer

NestJS backend enforcing role-specific access to CRM modules via NestJS Guards.

NestJS

Data Layer

PostgreSQL schema with structured relational mapping between Clients and the Properties they're interested in.

PostgreSQL

Deployment

Self-managed AWS EC2 (Ubuntu) host, process-managed with PM2, fronted by an NGINX reverse proxy serving both the SPA and the API under one domain with SSL.

AWS EC2NGINXPM2
  • Frontend (React SPA) → NGINX reverse proxy (unified domain, SSL) → NestJS backend API → PostgreSQL
  • PM2 ↔ NestJS backend process (supervision and restart on the EC2 host)

Architecture

Lead-to-closure lifecycle ownership

The system covers lead intake, follow-ups, site visits, and deal finalization as one connected flow, with technical decisions owned across the whole lifecycle rather than one slice of it.

Relational modeling for Properties and Clients

Clients and Properties are mapped relationally rather than denormalized, so a client interested in multiple properties — or a property with multiple interested clients — stays consistent as records change.

RBAC via NestJS Guards

Role-specific access to CRM modules is enforced with NestJS Guards on the backend, controlling what each channel partner or staff role can reach.

Self-managed deployment topology

Services run on an AWS EC2 Ubuntu host under PM2, with NGINX reverse-proxying both the SPA and the API under a unified domain and SSL — a topology fully configured and operated directly rather than delegated to a managed platform.

Engineering Decisions

Unified NGINX domain vs. separate frontend/backend domains

Problem
The SPA frontend and backend API both needed to be publicly reachable without introducing CORS complexity or exposing multiple separate public endpoints.
Options
  • Serve the frontend and backend on separate subdomains.
  • Serve both under one domain, with NGINX routing requests to the SPA or the API based on path.
Decision
Served the SPA and backend API under a single domain through NGINX path-based routing.
Trade-offs
NGINX's rewrite and routing configuration becomes a single, correctness-critical piece of the deployment — a misconfigured rule affects both frontend and API delivery at once, which is exactly the failure mode that later surfaced in production as rewrite loops.
Outcome
One domain and one SSL certificate to manage, at the cost of NGINX configuration sitting on the critical path for the entire application.

Relational mapping vs. denormalized records for Properties and Clients

Problem
Clients could be interested in multiple properties, and properties could have multiple interested clients — a denormalized structure would duplicate that relationship and risk drifting out of sync.
Options
  • Store client interest as a field on the property record (or vice versa), duplicating data per relationship.
  • Model Clients and Properties with a structured relational mapping between them.
Decision
Built Properties and Clients as separate modules connected by a structured relational mapping.
Trade-offs
Requires join queries to reconstruct a client's full interest list or a property's full lead list, instead of reading a single denormalized field.
Outcome
A client's interest across multiple properties, and a property's interest from multiple clients, stays consistent without duplicated, driftable data.

Production Stories

Diagnosing infrastructure failures on a self-managed EC2 stack

Problem
Production issues surfaced across several unrelated layers of the self-managed stack: permission errors, NGINX rewrite loops, SQL enum mismatches, and database constraint violations.
Investigation
Each issue traced back to a different layer of infrastructure owned directly rather than abstracted by a managed platform — file and process permissions on the Ubuntu host, the NGINX reverse-proxy rewrite rules routing the unified domain, and PostgreSQL enum types and constraints drifting from what the application actually wrote.
Root Cause
Running the full stack on a self-managed EC2 host under PM2 and NGINX meant infrastructure-level failure modes — permissions, reverse-proxy configuration, and database schema constraints — surfaced directly in production, with no managed platform layer to catch them first.
Resolution
Corrected file and process permissions on the host, fixed the NGINX rewrite rules causing the routing loop, and resolved the PostgreSQL enum and constraint mismatches directly in the schema, verifying recovery on each layer independently.
Reflection
Self-managed infrastructure trades platform abstraction for direct control — every layer, from process supervision to reverse-proxy config to database constraints, is a production surface you diagnose yourself, with no managed layer absorbing the failure first.

Lessons Learned

  1. Self-managed infrastructure (EC2 + PM2 + NGINX) puts every layer's failure modes directly in your hands — there's no managed platform smoothing over permission, proxy, or schema issues.

  2. Routing both a frontend and an API through one NGINX config under a unified domain keeps deployment simple, but makes that config a single point where a bad rule affects the whole application at once.

  3. Modeling a many-to-many relationship relationally, instead of denormalizing it onto one side, is what keeps a client's interest across properties (and a property's interest from clients) from silently drifting out of sync.



Command Palette

Search for a command to run...