Multi-tenant Real Estate SaaS
"Zero to production, solo — an AI-powered multi-tenant SaaS built end-to-end for real-estate professionals."
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
useMediaQueryto 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.