
Milo – Charleston County Real Estate Assistant
Milo is a cross-platform AI chatbot for real estate in the Charleston tri-county area (Charleston, Dorchester, Berkeley counties). The system consists of milo-backend—a Node.js API server with native node:http (no Express), custom routing, and CORS—and milo-app—an Expo (React Native) app for iOS, Android, and Web. The backend uses Anthropic Claude (Messages API with streaming) as the main agent, with Zod-defined tools for GIS lookups (address, parcel ID, coordinates, address search) against Charleston, Dorchester, and Berkeley ArcGIS REST APIs. When GIS returns no results, an OpenAI Responses API (web search) fallback is used. Voice is handled via OpenAI Whisper (STT) and OpenAI TTS, proxied through the backend. Auth and persistence use Supabase: the app uses Supabase Auth (email/password, OTP, magic link) with session storage (e.g. AsyncStorage); the backend validates JWT with the service role and manages chat sessions and messages in Supabase tables (chat-sessions, chat-sessions-messages). The app communicates with the backend via POST /api/chat (streaming SSE) and POST /api/voice for voice input. End-to-end flow: user signs in → chat/voice in app → backend validates JWT, creates or reuses session, runs Milo (Claude + GIS + OpenAI research) → SSE stream back to app → reply stored in Supabase and shown in UI.
The Story
The Problem
Real estate professionals and residents in the Charleston tri-county area needed a single, conversational way to look up property data (address, parcel ID, coordinates, deeds/registry) across Charleston, Dorchester, and Berkeley counties without switching between multiple county GIS sites or tools. They also wanted voice input and streaming answers.
The Solution
Built a two-package system: a lightweight Node.js backend (no Express) with Claude as the core agent, Zod-based tool definitions, and direct integration with all three counties' ArcGIS REST APIs for property lookups. Added OpenAI for voice (Whisper STT, TTS) and for web-search fallback when GIS returns no results. Delivered a cross-platform Expo app (iOS, Android, Web) with Supabase Auth, Expo Router, and an API client that consumes streaming SSE from the backend. Chat sessions and messages are persisted in Supabase and linked to the authenticated user.
My Approach
Custom Node HTTP server with CORS, request logging, and route dispatch; Claude Messages API with streaming and structured tool calls (Zod + betaZodTool); separate gis.mjs clients per county; Supabase for auth and chat persistence; Expo app with file-based routing, Supabase Auth, and SSE for real-time chat streaming.
Technology Stack
Frontend
- •Expo SDK ~54, React 19, React Native 0.81
- •Expo Router (app/ directory)
- •React Navigation (Stack, Tabs, Drawer)
- •lucide-react-native, expo-vector-icons, react-native-markdown
- •StyleSheet, theme (light/dark), zod for forms/env
Backend & Infrastructure
- •Node.js (ES modules, *.mjs)
- •Native node:http (no Express)
- •Custom router (lib/router.mjs) – GET/POST by path
- •Anthropic Claude Messages API (streaming)
- •Zod schemas + betaZodTool for tool calls
- •OpenAI Whisper (STT), OpenAI TTS, OpenAI Responses API (research fallback)
- •Supabase (service role) – auth validation, chat-sessions, chat-sessions-messages
- •dotenv – .env in milo-backend/
Payments & Integrations
- •Charleston, Dorchester, Berkeley ArcGIS REST APIs (gis.mjs)
- •Supabase Auth (email/password, OTP, magic link)
- •EAS (Expo Application Services) for builds
Core Platform Modules
A. Auth and entry
User signs in via Supabase Auth; session stored (e.g. AsyncStorage). App routes to sign-in/sign-up/forgot-password/verify-otp or to (tabs): Home, Chats, Folders, Account.
- •Supabase Auth (email/password, OTP, magic link)
- •Expo Router screens: index, sign-in, sign-up, forgot-password, verify-otp
- •AuthProvider context, JWT sent to backend on each request
B. Chat and voice in the app
User types or speaks; app calls miloChat/miloChatStream or voice endpoints; backend responds with SSE (session, text, tool_start, etc.).
- •POST /api/chat with messages, optional sessionId, type chat|voice
- •Authorization: Bearer <supabase_access_token>
- •SSE via react-native-sse for chat stream
- •Voice: record → POST /api/voice (base64 audio) → STT/TTS via backend
C. Milo (Claude) and tools
Backend validates JWT, creates or reuses chat session in Supabase, sends conversation to Milo. Claude uses system prompt and Zod tools: GIS lookups (address, PID, coordinates, address search) and internet_address_search when GIS returns no results.
- •gis_lookup_property (address + state + county)
- •gis_lookup_by_pid (parcel ID; backend tries all three counties)
- •gis_lookup_by_coordinates (lat/lon, optional county)
- •gis_search_addresses (partial text; county required)
- •internet_address_search (OpenAI web search fallback)
- •registry_search / document_search for deed/title docs
D. GIS and tri-county data
ArcGIS REST clients for Charleston (gisccapps.charlestoncounty.org), Dorchester (gisportal.dorchestercounty.net), Berkeley (gis.berkeleycountysc.gov). Tool results streamed back; full reply stored in Supabase.
- •County-specific address lookup and PID lookup
- •Point-in-parcel by coordinates
- •Address search by partial text per county
- •Correct county attribution in replies
Advanced System Capabilities
- •Property lookup by street address (state + county required)
- •Lookup by parcel ID (PID/TMS) across all three counties
- •Lookup by lat/lon (point-in-parcel)
- •Address search by partial text per county
- •Web search fallback when GIS returns no results
- •Deed/registry and document search when requested
- •Voice input (Whisper) and TTS output
- •Streaming chat with session persistence in Supabase
- •Cross-platform: iOS, Android, Web via Expo
System Architecture Principles
- •milo-backend: index.mjs (HTTP server, CORS, route dispatch) → config.mjs, lib/, middleware/, routes/ (health, chat, voice), milo.mjs (Claude agent, tools), gis.mjs (ArcGIS), openai.mjs (research)
- •milo-app: app/ (Expo Router), components/, contexts/ (AuthProvider), services/ (supabase, milo-api), hooks/
- •Flow: App → Supabase Auth → POST /api/chat or /api/voice → Backend JWT validation → Supabase session + messages → Milo (Claude + GIS + OpenAI) → SSE → App → store reply in Supabase, show in UI