Complete system architecture, external services, accounts, API keys, and maintenance costs. Gitignored — not committed.
Fault Line has three user-facing surfaces — a mobile app, a Next.js web app, and a marketing site — all backed by a single Supabase project. Users submit reports from the phone or the web, the database stores them, edge functions run AI analysis and send emails, and a daily cron escalates to authorities. Here's every piece:
The app is built with React Native (makes one codebase work on both Android and iPhone) using Expo (a toolkit that handles building, testing, and publishing). Written in TypeScript.
Think of it like: React Native is the engine, Expo is the car body, TypeScript is the language the blueprints are written in.
Account needed: Expo account (free) at expo.dev/signup
Build command: npx eas build --platform android
Supabase is where ALL the data lives — every report, user profile, authority, cluster, vote, escalation log, AI cache, and feedback submission. It also handles user login (magic link emails), file storage (photos/videos), and real-time notifications.
Think of it as: the filing cabinet, the security guard, and the postal service all in one.
Account: Already created. Project URL: https://dzewklljiksyivsfpunt.supabase.co
These are small programs that run on Supabase's servers (not on the user's phone). They handle things that need secret keys — like sending emails and calling the AI.
Think of it as: a private office in the back that handles sensitive paperwork the public can't access.
A full-featured reporting interface in the browser. Shares the same Supabase backend as the mobile app — identical authentication, identical schema, identical RLS. Users can submit reports, browse the map, search, view dashboards, and manage their profile from any browser.
Think of it as: the second window into the same filing cabinet. Useful for desktop users, journalists, city officials, and anyone who doesn't want to install an app.
Powers all AI features: photo analysis, auto-descriptions, legal letter enhancement, escalation emails, notification copy, report summaries, photo comparison, and feedback triage.
Uses two models: Haiku 4.5 (fast/cheap) for descriptions and notifications, Sonnet 4.6 (accurate/quality) for legal letters and escalation emails.
Cost: Haiku ~$0.25/1M input tokens. Sonnet ~$3/1M input. Photo analysis ~$0.01/photo. Estimated $5-20/month at moderate usage.
Sends all emails: escalation reports to government authorities, individual report submissions, and demand letter deliveries.
Currently sending from: onboarding@resend.dev (Resend test sender). Replies route to: moonlit-social-labs@proton.me (via REPLY_TO header). Switch FROM_EMAIL to reports@yourdomain once you buy the domain and verify it in Resend.
Text-to-speech server powering the audio-guided reporting mode in the mobile app and any AI-generated voice content on the web. Modal runs a private deployment of the Kokoro TTS model on a T4 GPU that scales to zero when idle.
Shared across the user's projects — same server handles TTS for every Moonlit Social Labs app, not just Fault Line. The mobile app posts text to the endpoint, receives audio/wav back, caches it to disk, and plays it via expo-av. Next.js proxies through /api/tts to avoid browser CORS.
Cost: ~$0.001 per request. Scales to zero. First request after idle has a 10–15s cold start; subsequent requests are <1s.
Handles feedback/feature-request/bug-report form submissions from the marketing site, Next.js web app, and mobile app. All three surfaces POST to the same endpoint; submissions are also mirrored to Supabase so the public submission board keeps working.
Banner ads that generate revenue to keep the app free. Currently placeholder — app works without it. When ready, install react-native-google-mobile-ads and plug in IDs.
Catches app crashes and sends reports so you can fix bugs. Free tier: 5,000 errors/month. App works without it — you just won't see crashes.
Hosts the public website: landing, features (plain + technical), blog, about, privacy, terms, feedback. 10 static HTML pages sharing a single theme.css. Currently at fault-line.dev.
Repo layout: the public repo contains only the website/ files (served at root). The private repo contains the full project source: mobile app, Next.js web app pointer, Supabase migrations, docs. Two remotes — origin (private) is the default push target; github-public is pushed manually for website updates.
Hosts the Next.js fault-line-web project. Auto-deploys on every push to main. Edge middleware, SSR, image optimization, zero cold-start on the production URL.
Limits: Hobby tier gives 100 GB bandwidth/month, 1000 deployments/month. Plenty for pre-launch. Paid (Pro) is $20/month if traffic outpaces the free tier.
Serverless Redis used by the Next.js web app's rate limiter. Protects against abuse of the report-submission endpoint and other public mutation paths. Fails open in dev, fails closed in prod.
Status: account not yet created. Code references UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN env vars; without them the web app's rate limiter short-circuits to reject every request (by design).
Accepts community donations to support development. Button on all website pages.
| Service | Monthly | Annual | Notes |
|---|---|---|---|
| Supabase | $0 | $0 | Free tier (500MB DB, 1GB storage, 50K users, 500K edge invocations) |
| Vercel (Hobby) | $0 | $0 | Next.js web app. Free tier — 100GB bandwidth/mo |
| Upstash Redis | $0 | $0 | Free tier (10K req/day) — rate limiting |
| Resend | $0 | $0 | Free tier (3,000 emails/month) |
| Anthropic (Claude AI) | ~$5–20 | ~$60–240 | Pay per use. Scales with report volume |
| Modal (Kokoro TTS) | ~$0.50–3 | ~$6–36 | Pay per request (~$0.001/req). Shared across all MSL projects |
| GitHub Pages | $0 | $0 | Free for public repos (marketing site) |
| Formspree | $0 | $0 | Free tier (50 submissions/month) |
| Expo (EAS) | $0 | $0 | Free tier (30 builds/month) |
| Google Play | $0 | $0 | $25 one-time (already paid) |
| Apple Developer | ~$8.25 | $99 | Annual subscription (not yet enrolled) |
| Domain (fault-line.dev) | ~$1 | ~$12 | When purchased (not yet) |
| AdMob / AdSense | $0 | $0 | Free — generates revenue (not yet configured) |
| Sentry | $0 | $0 | Free tier (5K errors/month, not yet configured) |
| Ko-fi | $0 | $0 | Free — receives donations |
| TOTAL | ~$15–32/mo | ~$177–387/yr | Mostly AI + Apple Developer fee. Everything else is free tier. |
Break-even: With AdMob banner ads averaging $1-3 eCPM and even moderate daily active users (~500), ad revenue would cover all costs. Ko-fi donations are bonus.
| Trigger | What Happens | Cost |
|---|---|---|
| 500MB database | Upgrade Supabase to Pro | $25/mo |
| 1GB file storage | Included in Supabase Pro | — |
| 3,000 emails/month | Upgrade Resend | $20/mo (50K emails) |
| 5K crash reports | Upgrade Sentry | $26/mo |
| 30 builds/month | Upgrade EAS | $15/mo |
┌─────────────────────────────────────────────────────────┐ │ CLIENT (Mobile App) │ │ React Native 0.76 + Expo SDK 55 + TypeScript │ │ 73 source files | 11 screens | 10 components │ │ 43 service modules | 5 hooks | 3 stores │ ├─────────────────────────────────────────────────────────┤ │ ↕ HTTPS │ ├─────────────────────────────────────────────────────────┤ │ BACKEND (Supabase Cloud) │ │ PostgreSQL 15 + PostGIS 3.4 | Row Level Security │ │ 11 tables | 12 RPC functions | 6 triggers │ │ Auth (magic link) | Storage (report-media bucket) │ │ Realtime (WebSocket subscriptions) │ ├─────────────────────────────────────────────────────────┤ │ EDGE FUNCTIONS (Deno Runtime) │ │ 5 functions | Authenticated | Rate-limited │ │ escalate-clusters | analyze-photo | ai-generate │ │ ai-compare-photos | send-report-email │ ├─────────────────────────────────────────────────────────┤ │ EXTERNAL APIs │ │ Anthropic Claude (Haiku 4.5 + Sonnet 4.6) │ │ Resend (transactional email) │ │ Census Bureau Geocoder (election districts) │ │ OpenStreetMap (offline tile cache) │ │ Open311 / SeeClickFix (bidirectional 311 sync) │ └─────────────────────────────────────────────────────────┘
app.config.js — Expo config with dotenv. Reads .env at build time via require('dotenv').config()eas.json — EAS Build profiles: development (APK), preview (internal), production (auto-increment)com.faultline.app (both platforms)faultline://report/:reportId, faultline://dashboard@supabase/supabase-js — DB client with RLS-enforced queriesreact-native-maps — MapView with Markers, Callouts, draggable pinsexpo-camera — CameraView for AR overlayexpo-image-picker — photo/video capture and gallery selectionexpo-location — GPS, reverse geocoding, heading for ARexpo-sensors — Accelerometer for bump detectionexpo-speech-recognition — native STT for voice commandsexpo-speech — TTS for audio-guided reportingexpo-notifications — push notifications with 3 Android channelsexpo-haptics — tactile feedback on all interactions@expo/vector-icons — MaterialCommunityIcons throughout@sentry/react-native — crash reporting (disabled until DSN configured)i18n-js — 5 locales (en, es, pt, zh, ht)
reports (PostGIS)
├── id UUID PK
├── user_id UUID FK→profiles (nullable for anonymous)
├── category TEXT (24 enum values)
├── latitude/longitude DOUBLE PRECISION
├── location_point GEOGRAPHY(POINT,4326) — auto-set by trigger
├── address, city, state, zip TEXT
├── description TEXT
├── size_rating, hazard_level, urgency, condition_level TEXT
├── media JSONB[] — [{id, uri, uploadedUrl, thumbnailUrl, type}]
├── resolved_media JSONB[] — before/after photos
├── vehicle_damage JSONB
├── status TEXT (draft|submitted|acknowledged|in_progress|resolved|closed|rejected)
├── authority_id UUID FK→authorities
├── cluster_id UUID FK→report_clusters
├── upvote_count, confirm_count INTEGER
├── is_anonymous, sensor_detected, offline_queued, is_quick_report BOOLEAN
├── _hp TEXT — honeypot (bot detection)
└── created_at, updated_at, resolved_at TIMESTAMPTZ
profiles
├── id UUID PK FK→auth.users
├── display_name, avatar_url, push_token TEXT
├── total_reports, total_upvotes, total_confirms, points INTEGER
├── badges JSONB[]
└── created_at, updated_at TIMESTAMPTZ
authorities (42 seeded for MA/RI/NH)
├── id UUID PK
├── name, level (federal|state|county|city|town), state, city, county TEXT
├── submission_methods JSONB[] — [{method, endpoint, priority, notes}]
├── boundary_geojson JSONB
├── response_time_avg_days, fix_rate_percent REAL
└── is_active BOOLEAN
report_clusters
├── id UUID PK
├── category TEXT, centroid_lat/lng, centroid_point GEOGRAPHY
├── report_count, unique_reporters INTEGER
├── max_hazard_level, status TEXT
├── authority_id UUID FK→authorities
├── city, state, address TEXT
└── first_reported_at, last_reported_at, submitted_at, escalated_at TIMESTAMPTZ
report_votes (UNIQUE: report_id + user_id + vote_type)
escalation_log (cluster_id, method, recipient, subject, body, status)
feedback (type, name, email, subject, message, status, votes)
ai_cache (report_id, task_type, result JSONB, model)
submission_log (user_id, ip_address, user_agent)
cluster_reports (cluster_id, report_id — link table)
set_report_point — BEFORE INSERT/UPDATE: auto-creates PostGIS geography point from lat/lngauto_cluster_on_insert — AFTER INSERT: assigns report to nearest cluster (50m radius, same category) or creates new clusterenforce_rate_limit — BEFORE INSERT: rejects if user has 10+ reports in last hourreject_honeypot — BEFORE INSERT: rejects if _hp field is populated (bot detection)handle_new_user — AFTER INSERT on auth.users: auto-creates profile rowget_nearby_reports(lat, lng, radius_km) — PostGIS ST_DWithin queryget_nearby_clusters(lat, lng, radius_km) — same for clustersassign_report_to_cluster(report_id) — clustering algorithmget_clusters_ready_for_escalation() — confirmed + 10 reports + 30 daysget_cluster_summary(cluster_id) — full data for email generationfind_authority_by_point(lat, lng) — PostGIS boundary lookupaward_points(user_id, points) — gamification point systemincrement_upvote/increment_confirm(report_id) — atomic counter updatesuser_id IS NULL OR auth.uid() = user_idauth.uid() = user_idauth.uid() = id| Task | Model | Reason | ~Cost/call |
|---|---|---|---|
| Photo analysis | Haiku 4.5 (vision) | Fast, cheap, strong vision | $0.01 |
| Photo comparison | Haiku 4.5 (vision) | Pattern matching, 2 images | $0.02 |
| Descriptions | Haiku 4.5 | Short output, frequent | $0.001 |
| Notifications | Haiku 4.5 | Short, latency-sensitive | $0.001 |
| Summaries | Haiku 4.5 | Short, straightforward | $0.001 |
| Legal letters | Sonnet 4.6 | Legal accuracy critical | $0.02 |
| Escalation emails | Sonnet 4.6 | Quality affects outcomes | $0.015 |
| Feedback triage | Sonnet 4.6 | Nuanced deduplication | $0.02 |
System prompt provides full app context: 24 categories, 3 state statutes with notice periods, clustering thresholds, anti-hallucination rules.
supabase.auth.getUser()_hp column, BEFORE INSERT trigger rejects non-emptycreatePinnedFetch() wraps global.fetch, blocks non-whitelisted domains, enforces HTTPSsupabase/schema.sql — core tables, PostGIS, triggers, RLS, RPC functionssupabase/seed_authorities.sql — 42 MA/RI/NH authorities with real contact infosupabase/clustering.sql — cluster tables, auto-assign trigger, escalation functionssupabase/migration_001_add_missing_columns.sql — urgency, condition, quick report, cluster_id, resolved_media, push_tokensupabase/migration_002_missing_rpcs.sql — award_points, get_nearby_clusterssupabase/migration_003_security_fixes.sql — RLS: enforce user_id on insert, add DELETE policiessupabase/migration_004_server_rate_limit.sql — rate limit trigger, honeypot, submission_logsupabase/migration_005_feedback.sql — feedback table with RLSsupabase/migration_006_ai_cache.sql — AI result cache tablesupabase/migration_007_rpc_auth_guards.sql — increment_upvote / increment_confirm now require auth; prevent self-votingsupabase/migration_008_reports_insert_rls.sql — tightened INSERT policy so an attacker cannot forge reports under another user's IDsupabase/migration_009_schedule_escalation_cron.sql — enable pg_cron + pg_net, schedule escalate-clusters-daily to run at 14:00 UTC every day| Service | Free Tier | Monthly Cost | Annual Cost | Scale Trigger |
|---|---|---|---|---|
| Supabase | 500MB DB, 1GB storage, 50K MAU | $0 | $0 | $25/mo at 500MB+ |
| Anthropic | None (pay per use) | $5–20 | $60–240 | Scales linearly with volume |
| Resend | 3,000 emails/mo | $0 | $0 | $20/mo at 3K+ |
| Apple Developer | None | $8.25 | $99 | Fixed |
| Domain | None | $1 | $12 | Fixed |
| Expo EAS | 30 builds/mo | $0 | $0 | $15/mo at 30+ |
| GitHub Pages | Unlimited | $0 | $0 | Never |
| Sentry | 5K errors/mo | $0 | $0 | $26/mo at 5K+ |
| AdMob | N/A | Revenue | Revenue | Generates income |
| TOTAL | $14–29 | $171–351 |
One-time costs: Google Play $25. That's it.
Revenue potential: Banner ads at $1-3 eCPM. 500 DAU × 3 impressions = 1,500/day = 45K/month = $45-135/month in ad revenue. Covers all costs at ~300 DAU.