RE/AMP Multi-Tenant SaaS Platform Case Study
A production multi-tenant platform for real estate transaction and investor operations
RE/AMP is a platform for real estate transaction and investor operations. I started the codebase in March 2024 and I lead its development, across the database, backend, frontend, infrastructure, and production support.
RE/AMP is proprietary, so this page covers architecture and decisions, not code or customer data. A few companies use it regularly.
The problem
Real estate investors run acquisitions and sales across spreadsheets, shared drives, email, text messages, and document tools. Nothing connects a property to its tasks, contracts, conversations, and history.
The goal was one system where each company manages its properties, workflows, documents, contacts, and communications, without seeing any other company’s data.
Scale
- 2,500+ of the codebase’s 5,000+ commits are mine.
- The codebase is 180,000+ lines of React, Node/Express, and SQL.
- Other developers have contributed to it.
- At one staging checkpoint the test suite stood at 726 passing tests across 42 suites.
The test number is a snapshot of one body of work, not a coverage figure.
Architecture overview
The web application is built in React and talks to a Node and Express REST API. PostgreSQL holds the data, queried through node-postgres with raw SQL, and Prisma Migrate manages schema changes.
Valkey provides caching and shared state. Background workers handle email and other jobs, and AWS hosts the production and staging environments in Docker behind Nginx.
RE/AMP at a glance. Prometheus, Grafana, and PostHog monitor the API.
Decisions worth explaining
Company-level data separation
Every company works inside its own boundary. Authorization checks happen on the server, so hiding a button in the interface is never the only protection.
The company ID on a request never comes from the payload. It comes from the signed-in user, who has already been checked as a member of that company.
That also makes changes at the company level easier, since the boundary lives in one place. I wrote it up in Multi-Tenant Data Boundaries in PostgreSQL.
Private document storage
Documents and images live in private S3 storage with public access blocked. The application decides who may see a file and then issues a short-lived pre-signed URL, so the storage layer is never the authorization system. I wrote more in Private S3 Storage With Pre-Signed URLs.
Email through IMAP
Email runs on self-hosted Mailcow with IMAP processing. The code handles message metadata, body retrieval jobs, caching, and polling. When authentication fails three times in a row, the connection is marked unusable and the user is asked to update their credentials, since their access has likely been revoked.
This was one of the hardest problems on the project. RE/AMP needed background workers polling IMAP for incoming mail and live API requests fetching mail for the person on the screen, across providers with different authentication rules and connection lifetimes. I explain how that shaped the design in Email Connections That Survive Auth Expiry.
Twilio messaging per company
Text messaging uses each company’s own Twilio credentials, with consent modeling, webhooks, and notifications. Webhooks carry the company’s ID in the query parameters, so each incoming event attaches to the right tenant.
Consent is enforced per phone number, in line with Twilio’s terms. More in Twilio Messaging With Per-Company Keys.
Stripe webhooks
I built the Stripe webhook handling. Webhooks can arrive late or more than once, so the handlers are written to tolerate repeats.
Client-side image processing
Photos are converted and resized in the browser before upload, behind an upload queue. In testing, 70+ photos were processed and live in under two minutes, with no image work on the server. The decision is covered in Moving Image Processing to the Browser.
Database changes that were hard to get right
Two schema changes stand out. One was the multi-tenant change, which added a company ID to everything. The other moved each property from a single workflow to multiple asynchronous workflows while keeping a core workflow.
For both, I wrote migration scripts and tested them over and over, ran dry runs on staging, and then tested on staging with live data before the production push. Both went through without a problem.
Release process
Releases go through a staging environment rebuilt from the latest production backup. The release is deployed and tested against real data shapes, and the restore process is exercised every time. I explain the reasoning in Rebuild Staging From Production Backups.
Monitoring and alerting
I run Prometheus and Grafana for application and system metrics, and PostHog for product and error visibility. Alerts fire when error rates cross a threshold, and they are coordinated across app instances so one problem does not send duplicate alerts.
I also fixed a Prometheus high-cardinality problem after unbounded labels inflated the metrics. The details are in Fixing Prometheus High-Cardinality Metrics.
How I build on it
New features start with requirements, edge cases, and the data model. Then come utilities and tests, endpoints, and frontend work, with each change reviewed before it is committed.
I use AI tools inside that structure. Existing, tested patterns in the codebase guide the output, and the architecture stays human-owned. See Why I Write Tests Before AI Writes Code.
What I would do differently
My biggest regret is in the early design. I modeled specific entities such as title companies and lenders one by one, when I only needed a general entity system with modules for contacts, accounting, and similar needs. Later I added that general system so companies could attach their own entities and items to properties and contracts.
The early specific models are now permanent tech debt.
What I would still improve
A project this size always has a list. Release steps will need more formality as the team grows beyond a small group, and the read replica is currently used for backup only.
Related
The custom web applications and code rescue services draw on this work. For my full background, see the engineering page.
RE/AMP facts
Verified RE/AMP figures. Keep the qualifiers.
- Commits
- 2,500+ of 5,000+
- Josh's commits on main out of the repository total. Other developers contribute.
- Codebase
- 180,000+ lines
- React, Node/Express, and SQL, excluding migrations, vendored and generated code.
- Tests
- 726 across 42 suites
- At one staging checkpoint. Not a coverage figure.
- Photo uploads
- 70+ in under 2 minutes
- In testing, with client-side image processing.