2025GridIron Construction
GridIron CRM
Real estate deal execution platform
- Node.js
- Express
- SQLite / Turso
- REST API
- HTML/CSS/JS
- gated pipeline stages
- 7
- before a stalled deal is flagged
- 14 days
At a glance
- Full-stack internal CRM tracking real estate deals from sourcing through exit across a 7-stage gated pipeline.
- Enforced forward-only status transitions with per-stage checklist gates, real-time time-in-status timers, and status history audit trails.
- Automated stalled-deal alert engine flags properties inactive beyond 14 days with severity codes, dashboard banners, and per-property resolve tracking.
- Investor assignment and budget modules: ownership percentages, categorized budget vs. spent line items, balance calculations, and visual progress bars.
- Document management with multipart/form-data upload, server-side file storage, and per-property download links in a tabbed single-page UI.
An internal deal-execution CRM for a real estate construction firm: a single source of truth that mirrors how the team actually works deal by deal, rather than a generic sales CRM bent into the shape of construction.
Purpose
GridIron Construction CRM is an internal deal-execution tracker built for a real estate construction firm that needed visibility into properties as they move from sourcing to exit. Off-the-shelf CRMs are tuned for sales pipelines (leads, contacts, opportunities) and don't model the operational reality of a construction-and-investment shop: a property sitting in “Under Contract” has different obligations than one in “Execution,” and skipping steps causes real financial and legal exposure.
I built this to solve three concrete problems the team was hitting with spreadsheets:
- 1. Properties getting stuck without anyone noticing. A deal could sit in the same status for weeks because there was no system-of-record flagging it. The CRM auto-generates alerts when a property has been in the same stage for more than 14 days.
- 2. Inconsistent execution between deals. Each status (Sourced → Underwritten → Offer/Negotiation → Under Contract → Owned → Execution → Exit) has a required checklist. The pipeline is gated: you cannot advance a property until every required item for its current stage is completed, which enforces process discipline at the database level rather than relying on memory.
- 3. Operational data scattered across tools. Documents lived in Drive, budgets in Excel, permits in email threads, investor splits in another spreadsheet. The CRM consolidates documents, budgets (with budget-vs-spent tracking), permits, investor assignments with ownership percentages, and per-checklist-item notes against a single property record.
The goal was a single source of truth that mirrors how the team actually works deal by deal, not a generic CRM bent into the shape of construction.
Development process
- Stack & shape. TypeScript + Express on the backend, vanilla HTML/CSS/JS for the UI served as static files, and SQLite via libSQL/Turso for the database. I deliberately kept the frontend dependency-free (no React, no build step on the client) because the user base is small and internal, and a single-binary Express server with static assets is dramatically easier to deploy and maintain than a full SPA pipeline.
- Schema-first design. I started with the data model rather than the UI. The pipeline's invariants (linear progression, single current status, full status history with durations) are encoded directly in the schema and enforced via triggers. When a property's
current_status_idchanges, a trigger automatically closes the previousproperty_status_historyrow (stampingexited_atand computingduration_seconds) and opens a new one. This means the timer logic is correct even if a developer forgets to call a service method, because the database itself owns the invariant. - Layered backend. The server is organized as routes → services → query layer → connection. The service layer (e.g.
StatusTransitionService,ChecklistService) holds the business rules (“can this property advance?”, “are all required checklist items complete?”) so the HTTP routes stay thin and the same logic is reachable from scripts or future clients. Status transitions are wrapped in transactions to keep the history table and the property row consistent. - Iterative feature build-out. Once the core pipeline + checklist gating worked end-to-end, I layered features on as separate, optional schema extensions (
schema-extensions.sql) so existing deployments could opt in without re-migrating: per-checklist-item notes, document uploads (Multer + localuploads/directory), investor assignment with ownership percentages, budget tracking with budget-vs-spent progress, permit status tracking, and the stalled-deal alert system. Each feature got its own table, its own route file, and its own tab in the property detail UI, which kept blast radius small when iterating. - Database portability. The project initially targeted Postgres, then I migrated to libSQL/Turso for easier deployment and a serverless-friendly footprint. I kept both schema files (
schema.sqlandschema-turso.sql) in the repo as documentation of the migration path, and the connection layer abstracts the client so the rest of the codebase didn't need to change. - Auth added later, deliberately. The first version had no user model: the team wanted to validate the pipeline mechanics before adding accounts. Once the core was stable, I layered in bcrypt-based auth, roles, cookie sessions, and per-user tracking on properties (
schema-auth-*.sql,schema-property-user-tracking.sql) as additive migrations rather than a rewrite.