cd ../projects

Multi-tenant Real Estate SaaS

"Zero to production, solo — an AI-powered multi-tenant SaaS built end-to-end for real-estate professionals."

Abstract dashboard mockup representing a multi-tenant SaaS platform: sidebar navigation, a map view with connected data points, and a card panel

1. The Problem

Real-estate agencies run on relationships and timing — but their tooling rarely reflects that. The client came with a clear frustration: existing CRMs are either generic (built for sales teams, not agents), bloated with features nobody uses, or completely disconnected from the day-to-day reality of prospecting, matching buyers to properties, and coordinating across agencies.

The ask was to build something purpose-built: a CRM designed specifically for real-estate professionals, with AI woven into the core workflows rather than bolted on as an afterthought — and built to scale across multiple agencies from day one.

2. Full Lifecycle, Solo

This was a true end-to-end engagement. Starting from a blank repo and a set of client interviews, I handled the full product lifecycle:

  • Requirements gathering — iterative sessions with the client to define user roles, core workflows, and priority features.
  • Architecture design — monorepo (pnpm + Turborepo), multi-tenant data model, API design, auth strategy.
  • Development — full-stack implementation across API and frontend, feature by feature.
  • Security hardening — dedicated audit pass covering privilege escalation vectors, tenant isolation, rate limiting, and data exposure risks.
  • Cloud deployment — migration from Vercel (serverless Prisma engine conflicts) to Railway, with persistent containers and Cloudflare DNS.
  • Ongoing maintenance — iterative feature delivery post-launch, with a structured roadmap.

3. Architecture

The stack is a pnpm + Turborepo monorepo with two apps: a Hono API (Node.js) and a Next.js 14 frontend. Data lives in Supabase/PostgreSQL via Prisma, with Row-Level Security enforcing tenant isolation at the database level. Every agency operates in a fully isolated data silo — enforced at three layers: middleware, Prisma queries, and RLS policies.

Authentication is JWT-based with agency_slug in the app metadata. The agencyId always comes from the JWT, never from the client — a non-negotiable invariant throughout the codebase. Tenant isolation is enforced at three independent layers:


// 1. Middleware — agencyId always from JWT, never from the client
const agencyId = c.get("jwtPayload").agencyId;

// 2. Prisma — every query scoped to the agency
const contacts = await prisma.contact.findMany({
  where: { agencyId },   // injected from JWT, not user input
});

// 3. PostgreSQL RLS — last line of defense
-- policy on contacts table
CREATE POLICY agency_isolation ON contacts
  USING (agency_id = current_setting('app.agency_id')::uuid);
Three independent layers means a bug in one doesn't expose another agency's data. Belt, suspenders, and a safety pin.

Deployment runs on Railway with two persistent services (API via Nixpacks, frontend via a custom Dockerfile), behind Cloudflare. The database uses Supabase's session pooler to stay compatible with Railway's IPv4 infrastructure.

4. AI Integration

AI isn't a feature here — it's infrastructure. Anthropic's Claude (Haiku for ~80% of calls, Sonnet for complex reasoning) powers several core workflows:

  • Collective memory enrichment — agents automatically enrich contact and property notes with structured context, debounced to avoid redundant calls.
  • Buyer–property matching — re-matching triggered automatically on criteria or property updates, with anti-loop guards and a 5-minute debounce.
  • Proactive suggestions — deterministic SQL signals surface actionable tasks for agents (capped at 3 per session, with a 7/30-day anti-harassment system).
  • Conversational assistant — a 3-branch classifier routes user queries to either action execution, data lookup, or usage guidance, dispatching to 6 JWT-scoped tools. The LLM never picks tool parameters directly — dispatch is deterministic TypeScript.
  • Voice capture via Gladia (European GDPR-compliant provider), replacing browser Web Speech API with a universal MediaRecorder pipeline. Audio is never retained.
  • Spec-sync — an ultra-conservative divergence detector (Haiku, confidence ≥ 0.9) flags when the live product drifts from the functional spec, routing suggestions through a manager approval flow before any update.

All AI usage runs through a usage-based credit wallet: atomic debits via raw SQL, a floor of zero (never negative), configurable rates per action, and quota alerts at 80% and 95% with a manager recovery UI. The key invariant: the balance is checked before the API call, and debited after — the LLM is never called if the wallet can't cover it.


// Check balance BEFORE calling the LLM — never debit on a failed call
const wallet = await prisma.$queryRaw`
  SELECT balance FROM credit_wallets
  WHERE agency_id = ${agencyId}
  FOR UPDATE
`;

if (wallet.balance < cost) throw new InsufficientCreditsError();

// Call the LLM
const result = await anthropic.messages.create({ ... });

// Atomic debit AFTER — floor at 0, never negative
await prisma.$executeRaw`
  UPDATE credit_wallets
  SET balance = GREATEST(0, balance - ${actualCost})
  WHERE agency_id = ${agencyId}
`;
No surprises on the invoice. If the balance is too low, the call never happens.

5. Selected Features

  • Real-time property map — Mapbox with turf.js polygon drawing, BAN geocoding, exact pins for own-agency properties and grid-snapped blurred pins for cross-agency listings.
  • Cross-agency contact system — agents from different agencies can initiate contact via HMAC-signed tokens, with a three-state consent flow (pending / accepted / refused) that keeps identities hidden until mutual acceptance.
  • Universal capture funnel — a 3-phase intake flow (classify → resolve → confirm) handles any inbound contact or property, with anti-hallucination guards and deduplication logic.
  • Independent agent plan — a solo-agent signup flow reuses the full multi-tenant infrastructure with isIndependent: true, requiring zero refactoring of the core data model.
  • Mobile-responsive shell — sidebar becomes an off-canvas drawer on mobile (CSS-only, no useMediaQuery to avoid SSR flash), with bottom sheets for capture flows and a full-screen map toggle.

6. Security

Before opening the platform to a second agency, a dedicated security audit covered the full attack surface. Key findings and fixes:

  • Privilege escalation — Server Actions lacked role checks on agent creation, deletion, and role changes. Fixed with a dedicated guard layer, tested against 12 real-DB replay scenarios.
  • Rate limiting — AI routes capped at 20 req/min/agent and 100 req/min/agency; transcription at 10/50; password reset at 5/h/IP. Wallet is never debited on a 429.
  • Verified clean — tenant isolation, LLM tool dispatch (deterministic, never user-controlled), HMAC map token (fail-closed), AI logs (metadata only, no PII), atomic wallet debits, no SQL injection vectors.

Need a product built end-to-end?

Let's Talk