skills/sobriety-tools-guardian/SKILL.md
Performance optimization and continuous improvement for sobriety.tools recovery app. Use for load time optimization, offline capability, crisis detection, performance monitoring, automated issue detection. Activate on "sobriety.tools", "recovery app perf", "crisis detection", "offline meetings", "HALT check-in", "sponsor contacts". NOT for general Next.js help, unrelated Cloudflare Workers, or non-recovery apps.
npx skillsauth add curiositech/windags-skills sobriety-tools-guardianInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
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)
1. 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.
2. 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.
3. Check-in Flow (MUST be <100ms to first input)
Open check-in → Pre-rendered form shell
→ Load previous patterns async
→ Submit optimistically
// 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)
// 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.
-- 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;
// 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 });
}
# 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
// 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'],
});
}
}
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
});
Symptom: Every search hits Supabase Problem: 200-500ms latency on every search Solution: Geohash-based KV caching (already implemented in meeting-proxy)
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 /> });
Symptom: Button stays disabled during network request Problem: User thinks it didn't work, closes app Solution: Optimistic UI + background sync queue
| 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 |
File GitHub issue immediately if:
This is a recovery app. Performance isn't a feature - it's the difference between someone getting help and someone dying alone.
tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.