ServiceSnapp CRM is a client-side, AI-powered CRM application built for field service businesses. The entire application runs in the browser with no backend infrastructure.
- Browser-first: All logic executes client-side
- Zero infrastructure: No servers, databases, or hosting costs
- AI-augmented: Gemini AI handles lead discovery, analysis, and content generation
- Progressive enhancement: Core features work offline, AI features require internet
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ React Application (SPA) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ App.tsx │ │ LeadModal │ │ LeadFinder │ │ │
│ │ │ (Router & │ │ (Details & │ │ (AI Search) │ │ │
│ │ │ State) │ │ AI Tools) │ │ │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────────────┼──────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────────▼──────────────────────┐ │ │
│ │ │ Services Layer │ │ │
│ │ │ ┌────────────────┐ ┌──────────────────┐ │ │ │
│ │ │ │ crmService │ │ geminiService │ │ │ │
│ │ │ │ (CRUD ops) │ │ (AI ops) │ │ │ │
│ │ │ └────────┬───────┘ └────────┬─────────┘ │ │ │
│ │ └───────────┼──────────────────────┼─────────────┘ │ │
│ │ │ │ │ │
│ └──────────────┼──────────────────────┼──────────────────┘ │
│ │ │ │
│ ┌──────────────▼──────┐ ┌─────────▼────────────────┐ │
│ │ localStorage │ │ External APIs │ │
│ │ ├─ leads │ │ ├─ Google Gemini │ │
│ │ ├─ settings │ │ └─ Nominatim (OSM) │ │
│ │ └─ products │ │ │ │
│ └─────────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Responsibilities:
- Application routing (tab-based: Dashboard, Finder, Settings)
- Global state management (leads, settings, products)
- View mode switching (Kanban/List)
- Search and filter logic
- Lead selection and navigation
State:
activeTab: 'dashboard' | 'finder' | 'settings'
viewMode: 'kanban' | 'list'
leads: Lead[]
settings: AppSettings | null
products: Product[]
selectedLead: Lead | null
searchFilter: string
needsContactFilter: boolean
sourceFilter: 'ALL' | 'MANUAL' | 'AI'Sub-components (inline, should be extracted):
KanbanColumn- Status-based columnsListView- Table view of leads
Responsibilities:
- Display/edit lead information
- Notes management (add, view chronological notes)
- AI-powered actions:
- Lead analysis & scoring
- Call script generation
- Email generation (cold/follow-up)
- Smart prescription execution
- Product assignment
- Status management
- Keyboard navigation (arrow keys)
- Touch gestures (swipe)
Key Features:
- Full-screen modal overlay
- Real-time save on any change
- Copy-to-clipboard functionality
- Email client integration
- Responsive design
Responsibilities:
- Chat-style AI interaction
- Natural language lead search
- Display discovered leads with metadata
- Bulk lead import to CRM
Workflow:
- User enters query: "Find HVAC companies in Oklahoma"
- AI searches Google and structures results
- Display leads with preview cards
- User selects leads to save
- Leads added to CRM with
source: 'AI_FINDER'
Responsibilities:
- Company profile editing
- Product catalog management
- Custom status pipeline configuration
- AI-assisted product descriptions
Sections:
- Company Info: Name, description, sales persona
- Products: CRUD for service offerings
- Status Pipeline: Custom stages with colors and ordering
- Integrations (placeholder): Google Sheets URL
Responsibilities:
- Display leads on interactive map (Leaflet)
- Filter by leads with geocoded coordinates
- Cluster markers for multiple leads in same area
- Click marker to open lead details
Dependencies:
- Leaflet.js (external CDN)
- OpenStreetMap tiles
- Requires
latitudeandlongitudeon leads
Responsibilities:
- Top navigation bar
- Tab switching UI
- Consistent padding and structure
Pattern: Simulates async API calls with setTimeout delays
Functions:
| Function | Purpose | Storage Key |
|---|---|---|
fetchLeads() |
Load all leads, apply migrations | servicesnapp_crm_data |
saveLead(lead) |
Save/update lead, auto-geocode | servicesnapp_crm_data |
deleteLead(id) |
Remove lead | servicesnapp_crm_data |
addNoteToLead(id, content) |
Append note to lead | servicesnapp_crm_data |
fetchSettings() |
Load settings with defaults | servicesnapp_crm_settings |
saveSettings(settings) |
Update settings | servicesnapp_crm_settings |
fetchProducts() |
Load products catalog | servicesnapp_crm_products |
saveProduct(product) |
Save/update product | servicesnapp_crm_products |
deleteProduct(id) |
Remove product | servicesnapp_crm_products |
Geocoding Logic:
- Triggered when
lead.locationexists but coordinates are missing - Uses Nominatim API (OpenStreetMap)
- In-memory cache to prevent duplicate lookups
- Respects rate limits (1 req/sec by user behavior)
Data Migrations:
// Example: Adding productIds to old leads
return leads.map(l => ({
...l,
productIds: l.productIds || []
}));Initialization:
// Frontend calls Supabase Edge Function (ai-gateway).
// Gemini API key is stored server-side as an edge function secret.
const { data } = await supabase.functions.invoke('ai-gateway', { body: { action, payload } });AI Functions:
- Model:
gemini-3-pro-preview - Tool: Google Search grounding
- Output: Array of partial leads
- Use case: "Find plumbing companies in Texas"
- Model:
gemini-3-flash-preview - Output: Markdown-formatted call script
- Context: Company info, lead history, selected products
- Features: Personalized, objection handling
- Types:
COLD|FOLLOW_UP - Output:
{ subject: string, body: string } - Personalization: Uses sales persona, lead context
- Output:
{ score: number, reasoning: string, nextAction: string } - Score: 0-100 (likelihood to buy)
- Use case: Lead qualification
- Output:
{ type: 'email' | 'script', content: string, subject?: string } - Smart routing: Determines if email or script is needed
- Use case: "Execute the next action for this lead"
- Output: One-sentence product pitch
- Use case: Auto-generate descriptions for new products
Error Handling Pattern:
try {
const response = await ai.models.generateContent({...});
return JSON.parse(response.text);
} catch (error) {
console.error("Error:", error);
return fallbackValue; // Never throw to UI
}Keys:
servicesnapp_crm_data- Lead recordsservicesnapp_crm_settings- App configurationservicesnapp_crm_products- Service catalog
Capacity: ~5-10MB (browser-dependent)
Serialization: JSON.stringify/parse
Migration Strategy:
- Check for missing fields on load
- Apply defaults for new properties
- Never break existing data
interface Lead {
id: string;
companyName: string;
contactName?: string;
email?: string;
phone?: string;
website?: string;
status: LeadStatus; // Customizable via StatusConfig[]
notes: Note[];
description?: string;
location?: string;
latitude?: number; // Auto-geocoded
longitude?: number; // Auto-geocoded
source: 'MANUAL' | 'AI_FINDER';
productIds: string[]; // References Product.id
aiAnalysis?: LeadAIAnalysis;
createdAt: string; // ISO 8601
}interface AppSettings {
companyName: string;
companyDescription: string;
aboutMe: string; // Sales persona
sheetUrl?: string; // Placeholder for future export
statuses: StatusConfig[];
}interface StatusConfig {
id: string; // e.g., 'COLD', 'WARM'
label: string; // Display name
color: string; // Tailwind color (blue, green, etc.)
order: number; // Pipeline position
}- Purpose: All AI features
- Authentication: API key via environment variable
- Rate Limits: Per Google Cloud project quotas
- Models Used:
gemini-3-pro-preview- Search + reasoninggemini-3-flash-preview- Fast content generation
- Purpose: Geocoding locations to coordinates
- Rate Limit: 1 request/second (Usage Policy)
- Authentication: None (requires User-Agent header)
- Caching: In-memory during session
- React 19: View layer
- react-dom: DOM rendering
- TypeScript 5.8: Type safety
- lucide-react: Icon components
- TailwindCSS: Utility-first CSS (CDN)
- Leaflet.js: Map visualization (CDN)
- react-markdown: Render AI-generated content
- Vite 6: Dev server, bundler, HMR
- @vitejs/plugin-react: React Fast Refresh
npm install
# Create .env.local with:
# VITE_SUPABASE_URL
# VITE_SUPABASE_ANON_KEY
npm run dev # http://localhost:3000npm run build # Output: dist/
npm run preview # Test production build- Static hosting: Netlify, Vercel, GitHub Pages
- Requirements: Serve
index.htmlfor all routes (SPA) - Environment: Set
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEYfor frontend. - AI Secret: Set
GEMINI_API_KEYin Supabase Edge Function secrets.
User edits lead
↓
LeadModal.handleInputChange()
↓
setEditedLead({ ...lead, field: value })
↓
User clicks "Analyze Lead"
↓
LeadModal.handleAnalyzeLead()
↓
setAnalyzing(true)
↓
geminiService.analyzeLead(lead, settings)
↓
Google Gemini API call
↓
{ score: 75, reasoning: "...", nextAction: "..." }
↓
setEditedLead({ ...lead, aiAnalysis: result })
↓
saveLead(editedLead)
↓
crmService.saveLead()
↓
localStorage.setItem('servicesnapp_crm_data', JSON.stringify(leads))
↓
onUpdate(editedLead)
↓
App.handleLeadUpdate()
↓
setLeads(prev => prev.map(l => l.id === lead.id ? updatedLead : l))
↓
UI re-renders with new data
- ❌ API key exposed in browser (process.env)
- ❌ No authentication
- ❌ No authorization
- ❌ No input validation/sanitization
- ❌ XSS vulnerable (user-generated content)
- ❌ Data visible in localStorage (no encryption)
- ✅ Move API calls to backend proxy
- ✅ Implement user authentication (OAuth, JWT)
- ✅ Validate/sanitize all inputs
- ✅ Use Content Security Policy headers
- ✅ Encrypt sensitive data
- ✅ Rate limiting on API endpoints
- ✅ HTTPS only
- ⚡ Instant load: No API roundtrips for data
- ⚡ Fast filtering: In-memory JavaScript
- ⚡ No latency: All reads from localStorage
- 🐌 AI calls: 1-5 second response times
- 🐌 Geocoding: 1-2 second per location
- 🐌 Large datasets: localStorage read/parse becomes slow >1000 leads
- 🐌 No pagination: All leads loaded at once
- Debounce search input
- Virtual scrolling for large lists
- Lazy load AI features (code splitting)
- Cache geocoding results in localStorage
- Web Workers for heavy JSON parsing
| Resource | Limit | Impact |
|---|---|---|
| localStorage size | ~5-10MB | Max ~500-1000 leads |
| Browser memory | Device-dependent | Rendering performance degrades |
| API rate limits | Gemini quota | AI features may fail |
| Geocoding | 1 req/sec | Slow bulk imports |
- Phase 1: Add backend API to replace localStorage
- Phase 2: Implement pagination and incremental loading
- Phase 3: Server-side AI processing with caching
- Phase 4: Real-time sync (WebSockets) for team collaboration
- ❌ No unit tests
- ❌ No integration tests
- ❌ No E2E tests
- ❌ No type tests beyond TypeScript compiler
- Unit: Services layer (crmService, geminiService)
- Integration: Component + service interactions
- E2E: Critical user flows (Playwright/Cypress)
- Visual: Storybook for component library
- Type safety: Exhaustiveness checking for unions
Pros:
- Zero hosting costs
- Instant deployment (static files)
- No backend complexity
- Built-in privacy (data never leaves browser)
Cons:
- No multi-device sync
- No team collaboration
- Data loss if localStorage cleared
- API key security impossible
Verdict: Acceptable for demo/prototype, not production
Pros:
- Simpler API
- Synchronous reads (easier state management)
- JSON-friendly
Cons:
- Size limits
- No structured queries
- Slower for large datasets
Verdict: Good enough for POC, migrate to IndexedDB if scaling
Pros:
- Google Search integration (grounding)
- Structured output (JSON schemas)
- Multimodal (future: image analysis)
- Competitive pricing
Cons:
- Vendor lock-in
- Rate limits
- Variable response times
Verdict: Best fit for lead discovery + content generation
┌─────────────────────────────────────────────┐
│ Frontend (React SPA) │
│ ├─ Offline-first design │
│ ├─ Service Worker caching │
│ └─ IndexedDB for local state │
└──────────────┬──────────────────────────────┘
│ HTTPS/WSS
┌──────────────▼──────────────────────────────┐
│ Backend (Node.js/Python) │
│ ├─ REST API (CRUD operations) │
│ ├─ WebSocket (real-time updates) │
│ └─ Job Queue (background AI processing) │
└──────────────┬──────────────────────────────┘
│
┌──────────────▼──────────────────────────────┐
│ Persistence Layer │
│ ├─ PostgreSQL (relational data) │
│ ├─ Redis (caching, sessions) │
│ └─ S3 (file storage) │
└─────────────────────────────────────────────┘
- Dual-write period: Keep localStorage + add API calls
- Feature flag: Gradually shift users to backend
- Export/import: Allow data portability
- Deprecation: Remove localStorage after full migration
This architecture prioritizes rapid prototyping and AI experimentation over production readiness. It's an excellent foundation for:
- Validating product-market fit
- Demoing to potential customers
- Iterating on AI features quickly
Before production launch, address:
- Backend API integration
- Security hardening
- Scalability improvements
- Test coverage
- Error monitoring