Skip to content

Repository files navigation

ChurchMap

Find your church, rated on what actually matters.

ChurchMap is a church discovery app for searching churches by city/state, viewing them on a map, and reading or leaving reviews across six fit dimensions: worship energy, community warmth, sermon depth, children's programs, theological openness, and facilities.

The repo is named church_map (formerly holyhub); the product UI is branded as ChurchMap.

What Is Actually Used

Area Used in this repo
Backend FastAPI (async), Uvicorn, psycopg 3 + AsyncConnectionPool, Pydantic, HTTPX
Database Supabase Postgres + pgvector, transaction pooler on port 6543
Frontend React 18, Vite, React Router, Leaflet, React Leaflet
Styling Plain CSS in frontend/src/index.css, Fraunces (serif) + Plus Jakarta Sans
Auth Google Identity Services in the browser, Google tokeninfo verification in FastAPI
Enrichment Google Places API (lazy, capped at 10,800 calls/month)
Location ipapi.co for first-visit city/state detection
Hosting Vercel (frontend) + Render Web Service Starter (backend)
Migrations Numbered migrations/*.sql + backend/db/migrate.py runner
Tests pytest (17 active, parity tests gated on DATABASE_URL)

Features

  • Search churches by city + state, or by zip_code through the API
  • See results in a list and on a Leaflet/OpenStreetMap map
  • Sort by nearest, rating, or review count
  • Filter by computed tags, language, and cultural background when data is available
  • Open an inline church detail panel from search results
  • View a full church detail page at /church/:id
  • Sign in with Google to submit reviews
  • Rate churches on the six fit dimensions
  • See aggregate dimension bars and computed tags once enough review data exists
  • Find similar churches using Euclidean distance across the six review dimensions
  • Lazily enrich church details from Google Places when GOOGLE_PLACES_KEY is configured

Local Setup

Prerequisites:

  • Python 3.11+
  • Node 18+
  • npm
  • A Supabase Postgres URL (free tier is fine for local dev). See the Phase A migration plan for provisioning details.
git clone https://github.com/zhou100/church_map.git
cd church_map

# Backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt

# Frontend
cd frontend && npm install && cd ..

Set environment variables (see next section), then apply schema migrations:

export DATABASE_URL='postgresql://postgres.xxx:PWD@aws-0-...:6543/postgres'
python -m backend.db.migrate
# applies migrations/0001_initial.sql, 0002_pgvector.sql, 0003_indexes.sql

The data load is a one-shot from a SQLite snapshot (used during the Phase A cutover; for fresh local dev you start with an empty schema and seed via the API).

Environment Variables

Backend:

DATABASE_URL=postgresql://postgres.xxx:PWD@aws-0-us-west-1.pooler.supabase.com:6543/postgres
GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_PLACES_KEY=your-google-places-api-key
ENV=development                  # set to "production" on Render
READ_ONLY=0                      # flip to 1 during cutover windows
DB_POOL_MAX=10                   # psycopg pool max size

Frontend (.env.local or Vercel project env):

VITE_API_URL=http://localhost:8000
VITE_GOOGLE_CLIENT_ID=your-google-oauth-client-id

Notes:

  • DATABASE_URL must use the transaction pooler port (6543), not the direct port (5432). Direct exhausts the 60-conn limit under FastAPI workers.
  • GOOGLE_CLIENT_ID is optional on the backend, but when set it verifies that Google ID tokens were issued for the expected client.
  • VITE_GOOGLE_CLIENT_ID is required for the review form to show Google Sign-In.
  • GOOGLE_PLACES_KEY is optional. Without it, enrichment endpoints return cached data if present or skip safely.

Running Locally

# Terminal 1: backend at http://localhost:8000
uvicorn backend.main:app --reload

# Terminal 2: frontend at http://localhost:5173
cd frontend && npm run dev

Open http://localhost:5173. Try Brooklyn, NY if location detection does not find local results.

Tests

pytest -q                                    # unit + static checks (17 active)
DATABASE_URL=... pytest tests/test_parity.py # endpoint parity against Postgres

tests/test_no_sqlite_in_routes.py is a CI grep that fails if anything reintroduces sqlite3.connect, ? placeholders, or .lastrowid into runtime route code.

API

GET  /api/health
GET  /api/stats
GET  /api/churches?city=Brooklyn&state=NY&limit=50&offset=0
GET  /api/churches?city=Brooklyn&state=NY&language=Spanish&worship_style=liturgical&stance=traditional
GET  /api/churches?zip_code=11201&limit=50&offset=0
GET  /api/churches/{church_id}
GET  /api/churches/{church_id}/similar
POST /api/churches/{church_id}/enrich
GET  /api/reviews/{church_id}
POST /api/reviews
POST /api/auth/verify

GET /api/stats reports corpus and crawl-pipeline health — how many churches have a website, how many are extracted, which prompt version and model they were extracted under, why extraction attempts ended, and how recently each crawl stage did any work. Aggregates only, so it needs no token, and cached in-process for 5 minutes.

Read crawl.pipeline_ok and crawl.stages[*].stale, not crawl.runs.error. A stage that dies before it can write its crawl_runs row — a database it can't reach, say — contributes no error at all, so an error count of zero is not evidence of health. Age since the last progress is the signal an absent row can't fake. A stage that has never made progress reports null and counts as stale rather than being omitted; the crawl going quiet is the failure this is meant to make visible (it was auto-disabled for 8 days in July 2026 and nothing said so).

Progress, not perfection: a batch is status='ok' only when zero rows failed, so one bad row makes it partial. Staleness therefore keys on stages[*].last_progress (a run that finished and got at least one row through), while last_success reports the last flawless run alongside it. Keying on the strict one meant a backfill with any error rate reported its stage as dead while it worked normally.

extraction.attempts breaks down why attempts ended, and it is the first thing to read when coverage stops moving. churches.extracted counts extracted_at IS NOT NULL, which failures stamp too — so attempts.ok is the honest success count and attempts sums to churches.extracted. The buckets that matter: no_html means R2 holds no page behind the artifact, so those churches need re-fetching and re-queueing them achieves nothing; no_text and error are terminal and correctly so; transient retries itself. Watch extraction.awaiting_queue against extraction.stale during a backfill — falling together is progress, awaiting_queue hitting zero while stale sits still means the backfill has failed to a halt, which otherwise looks exactly like completion.

language, worship_style and stance filter on extracted_tags — what the crawler read off each church's own website, not review-derived tags. They combine with AND. worship_style and stance are validated against the extraction schema and return 400 on an unknown value, because zero results for a typo is indistinguishable from "no churches like that near you". language is open-ended, since the extractor emits whatever the site says.

A church with no extraction cannot match any of these filters — we don't know its service language until something has read its site. So their usefulness tracks extraction coverage: check extraction.stale on /api/stats.

POST /api/reviews requires an Authorization: Bearer <google_id_token> header. Reviews are tied to a Google-authenticated user record and store reviewer display metadata.

When READ_ONLY=1, all POST/PUT/PATCH/DELETE requests outside the exempt list (/api/health, /api/auth/verify) return 503 with a Retry-After header. Used during cutover windows to backstop the frontend banner.

Review Dimensions

Each review can rate:

  • Worship energy: lively to contemplative
  • Community warmth: how welcoming the community feels
  • Sermon depth: how substantive the teaching feels
  • Children's programs: quality of kids ministry
  • Theological openness: traditional to progressive
  • Facilities: building, amenities, and accessibility signals

Tags such as Vibrant worship, Deep sermons, Progressive, or Traditional are computed from aggregate dimension scores once a church has at least 3 reviews.

Project Structure

backend/
  main.py               FastAPI app: lifespan, READ_ONLY middleware, CORS
  auth.py               Consolidated GSI tokeninfo verification
  enrichment.py         Google Places enrichment (sync psycopg)
  db/
    pool.py             AsyncConnectionPool factory (transaction pooler)
    migrate.py          Migration runner
    repository.py       ChurchRepository, ReviewRepository, UserRepository
  routers/              FastAPI route handlers
  scrapers/             FROZEN — see backend/scrapers/README.md (Phase B rewrite)

migrations/
  0001_initial.sql      Lowercase tables, JSONB, TIMESTAMPTZ
  0002_pgvector.sql     CREATE EXTENSION vector + church_embeddings.vector
  0003_indexes.sql      Indexes ported from SQLite

scripts/
  migrate_data.py       SQLite -> Postgres one-shot (used during cutover)
  docker-entrypoint.sh  Run migrations, then exec uvicorn

tests/                  pytest suite
frontend/               React + Vite SPA
Dockerfile              Render Web Service image
render.yaml             Render Blueprint
frontend/vercel.json    SPA rewrite config for Vercel

Deployment

Backend (Render):

render.yaml defines a single Docker Web Service on the Starter plan. Auto-deploys from main on every push. Set the four sync: false env vars (DATABASE_URL, GOOGLE_CLIENT_ID, GOOGLE_PLACES_KEY) in the Render dashboard.

The container entrypoint runs python -m backend.db.migrate before starting Uvicorn. New migrations land automatically on the next deploy.

Frontend (Vercel):

Vite SPA. frontend/vercel.json rewrites all routes to index.html so /church/:id works on refresh. Auto-deploys from main.

Database (Supabase):

Free-tier Postgres is sufficient at current scale (~80 MB). Use the transaction pooler URL (port 6543), not the direct connection (5432). pgvector ships preinstalled and is enabled by migrations/0002_pgvector.sql.

More Docs

About

ChurchMap is a location based church discovery app for searching churches by city/state, viewing them on a map, and reading or leaving reviews

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages