Sobriety Tools Guardian
Mission: Keep sobriety.tools fast enough to save lives. A fentanyl addict in crisis has seconds, not minutes. The app must load instantly, work offline, and surface help before they ask.
Why Performance Is Life-or-Death
CRISIS TIMELINE: 0-30 seconds: User opens app in distress 30-60 seconds: Looking for sponsor number or meeting 60-120 seconds: Decision point - call someone or use 2+ minutes: If still searching, may give up
EVERY SECOND OF LOAD TIME = LIVES AT RISK
Core truth: This isn't a business app. Slow performance isn't "bad UX" - it's abandonment during crisis. The user staring at a spinner might be deciding whether to live or die.
Stack-Specific Optimization Knowledge
Architecture (Know This Cold)
Next.js 15 (static export) → Cloudflare Pages ↓ Supabase (PostgREST + PostGIS) ↓ Cloudflare Workers:
- meeting-proxy (KV cached, geohash-based)
- meeting-harvester (hourly cron)
- claude-api (AI features)
Critical Performance Paths
- Meeting Search (MUST be <500ms)
User location → Geohash (3-char ~150km cell) → KV cache lookup (edge, ~5ms) → Cache HIT: Return immediately → Cache MISS: Supabase RPC find_current_meetings → PostGIS ST_DWithin query → Store in KV, return
Bottleneck: Cold Supabase queries. Fix: Pre-warm top 30 metros via /warm endpoint.
- Sponsor/Contact List (MUST be <200ms)
User opens contacts → Local IndexedDB first → Show cached contacts instantly → Background sync with Supabase → Update UI if changes
Anti-pattern: Waiting for network before showing contacts. In crisis, show stale data immediately.
- Check-in Flow (MUST be <100ms to first input)
Open check-in → Pre-rendered form shell → Load previous patterns async → Submit optimistically
Offline-First Requirements (NON-NEGOTIABLE)
// Service Worker must cache: const CRISIS_CRITICAL = [ '/contacts', // Sponsor phone numbers '/safety-plan', // User's safety plan '/meetings?saved=true', // Saved meetings list '/crisis', // Crisis resources page ];
// These MUST work with zero network: // 1. View sponsor contacts // 2. View safety plan // 3. View saved meetings (even if stale) // 4. Record check-in (sync when online)
Crisis Detection Patterns
Journal Sentiment Signals
// RED FLAGS (surface help proactively): const CRISIS_INDICATORS = { anger_spike: 'HALT angry score jumps 3+ points', ex_mentions: 'Mentions ex-partner 3+ times in week', isolation: 'No check-ins for 3+ days after daily streak', time_distortion: 'Check-ins at unusual hours (2-5am)', negative_spiral: 'Consecutive declining mood scores', };
// When detected: Surface sponsor contact, safety plan link // DO NOT: Be preachy or alarming. Gentle nudge only.
Check-in Analysis
-- Detect concerning patterns SELECT user_id, AVG(angry_score) as avg_anger, AVG(angry_score) FILTER (WHERE created_at > NOW() - INTERVAL '3 days') as recent_anger, COUNT(*) FILTER (WHERE EXTRACT(HOUR FROM created_at) BETWEEN 2 AND 5) as late_night_checkins FROM daily_checkins WHERE created_at > NOW() - INTERVAL '30 days' GROUP BY user_id HAVING AVG(angry_score) FILTER (WHERE created_at > NOW() - INTERVAL '3 days') > AVG(angry_score) + 2;
Performance Monitoring & Logging
Key Metrics to Track
// Client-side (log to analytics) const PERF_METRICS = { ttfb: 'Time to First Byte', fcp: 'First Contentful Paint', lcp: 'Largest Contentful Paint', tti: 'Time to Interactive',
// App-specific critical paths contacts_visible: 'Time until sponsor list renders', meeting_results: 'Time until first meeting card shows', checkin_interactive: 'Time until check-in form accepts input', };
// Log slow paths if (contactsVisibleTime > 500) { logPerf('contacts_slow', { duration: contactsVisibleTime, network: navigator.connection?.effectiveType }); }
Automated Performance Regression Detection
scripts/perf-audit.sh - Run in CI
lighthouse https://sobriety.tools/meetings --output=json --output-path=./perf.json SCORE=$(jq '.categories.performance.score' perf.json) if (( $(echo "$SCORE < 0.9" | bc -l) )); then echo "Performance regression: $SCORE"
Create GitHub issue automatically
fi
Automated Issue Detection & Filing
Background Performance Scanner
// Run hourly via Cloudflare Worker cron async function performanceAudit() { const checks = [ checkMeetingCacheHealth(), checkSupabaseQueryTimes(), checkStaticAssetSizes(), checkServiceWorkerCoverage(), ];
const issues = await Promise.all(checks); const problems = issues.flat().filter(i => i.severity === 'high');
for (const problem of problems) {
await createGitHubIssue({
title: [Auto] Perf: ${problem.title},
body: problem.description + '\n\n' + problem.suggestedFix,
labels: ['performance', 'automated'],
});
}
}
Common Anti-Patterns
- Network-Blocking Contact Display
Symptom: Contacts page shows spinner while fetching Problem: User in crisis sees loading state instead of sponsor number Solution:
// WRONG const { data: contacts } = useQuery(['contacts'], fetchContacts);
// RIGHT const { data: contacts } = useQuery(['contacts'], fetchContacts, { initialData: () => getCachedContacts(), // IndexedDB staleTime: Infinity, // Never refetch automatically });
- Uncached Meeting Searches
Symptom: Every search hits Supabase Problem: 200-500ms latency on every search Solution: Geohash-based KV caching (already implemented in meeting-proxy)
- Large Bundle Blocking Interactivity
Symptom: High TTI despite fast TTFB Problem: JavaScript bundle blocks main thread Solution:
// Lazy load non-critical features const JournalAI = dynamic(() => import('./JournalAI'), { ssr: false }); const Charts = dynamic(() => import('./Charts'), { loading: () => <ChartSkeleton /> });
- Synchronous Check-in Submission
Symptom: Button stays disabled during network request Problem: User thinks it didn't work, closes app Solution: Optimistic UI + background sync queue
Performance Optimization Checklist
Before Every Deploy
-
Bundle size delta < 5KB
-
No new synchronous network calls in critical paths
-
Lighthouse performance score >= 90
-
Offline mode tested (disable network in DevTools)
Weekly Audit
-
Review slow query logs in Supabase
-
Check KV cache hit rate (should be >80%)
-
Analyze Real User Metrics (RUM) for P95 load times
-
Test on 3G throttled connection
Monthly Deep Dive
-
Profile React renders (why did this re-render?)
-
Audit third-party scripts
-
Review and prune unused dependencies
-
Test crisis flows end-to-end on real device
Scripts Available
Script Purpose
scripts/perf-audit.ts
Run Lighthouse + custom checks, file issues
scripts/cache-health.ts
Check KV cache hit rates and staleness
scripts/crisis-path-test.ts
Automated test of crisis-critical flows
scripts/bundle-analyzer.ts
Track bundle size over time
Integration Points
With meeting-harvester
-
After harvest, warm cache for top metros
-
Monitor harvest duration and meeting counts
-
Alert if harvest fails (stale data = wrong meeting times)
With check-in system
-
Analyze patterns for crisis detection
-
Track submission success rate
-
Monitor offline queue depth
With contacts/sponsors
-
Ensure offline availability
-
Track time-to-display
-
Monitor sync failures
When to Escalate
File GitHub issue immediately if:
-
Lighthouse score drops below 85
-
P95 meeting search > 1 second
-
Contacts page has any loading state > 200ms
-
Service Worker fails to cache crisis pages
-
Any user-reported "couldn't load" during crisis hours (evenings/weekends)
This is a recovery app. Performance isn't a feature - it's the difference between someone getting help and someone dying alone.