Methodology

sfpulse fetches public data from SF government sources on a fixed schedule, caches the results locally, and renders them as-is. Nothing is summarized, reworded, or inferred. The text you read came directly from the source.

How data gets to you

A set of Python scripts runs in the background, each targeting a single data source. Each script accepts a district number and a lookback window (in days), queries the source, filters the results to the district, and returns structured JSON. The Go server stores that JSON in SQLite and serves it to the page. When cached data is older than its refresh threshold, the script reruns in the background while the old data stays visible.

No data transformation happens between script output and what you see. If a permit shows "HIGH" tier, that label came directly from the source data. If a 311 category is flagged as a spike, that flag was computed by the script comparing the current week to the prior week using the same Socrata dataset the city publishes.

Sources and refresh schedule

SectionSourceHowRefreshed every
Building permitsSF Open Data i98e-djp9Socrata API, filtered by supervisor district2 hours
Housing pipelineSF Planning qvu5-m3a2 & acdm-wktnSocrata API, filtered by district4 hours
311 activitySF Open Data vw6y-z8j6Socrata API; compares current 7-day window to prior 7-day window per category to detect spikes1 hour
Eviction noticesSF Open Data 5cei-gny5Socrata API, filtered by district; Ellis Act and owner move-in flagged separately6 hours
Board of SupervisorsLegistar RSSRSS feed parsed for upcoming and recent meetings; items filtered to district supervisor2 hours
Planning Commissionsfplanning.orgHTML scrape of published hearing calendars; PDFs parsed for item addresses cross-referenced to district streets3 hours
SFMTA engineering hearingssfmta.comHTML scrape; items filtered by district street list1 hour
Board of AppealsSF.gov Wagtail APIREST/JSON endpoint2 hours
Ethics & lobbyingSF Ethics CommissionLobbyist contact filings filtered to supervisor; campaign contribution data via Socrata & RSS4 hours
Local newsMission Local, SF Standard, Streetsblog SF, SF YIMBY, 48 Hills, Indybay (RSS)RSS feeds deduplicated by title similarity; articles tagged to district by street/neighborhood name matching30 minutes
SFMTA Boardsfmta.comHTML scrape of meeting calendar6 hours
Rent BoardSF Rent Board (SF.gov)Wagtail REST/JSON endpoint12 hours
Rec & ParkSF Rec & Park (Granicus)REST/JSON endpoint4 hours
SFUSD BoardSFUSD Board (BoardDocs)BoardDocs API + public Google Sheets calendar12 hours
BART BoardBART Legistar APIREST/JSON; BART advisory bodies included12 hours
SFCTAsfcta.orgHTML scrape of meeting calendar12 hours
Civic actionsmobilize.us + Indybay RSSmobilize.us event search for SF; Indybay RSS filtered for civic events30 minutes
Volunteer cleanupsRefuse Refuse SF + SF DPWHTML scrape + DPW volunteer calendar4 hours

District assignment

Each supervisorial district is pre-configured with a list of neighborhoods and streets. Scripts that query address-level data (permits, evictions, planning) filter by supervisor district number directly in the API query where the dataset supports it. Scripts that scrape text (planning PDFs, news articles) match against the district's street and neighborhood lists.

The 11 district boundaries follow the official supervisorial district map. Neighborhood-to-district assignments are manually curated and updated when the Board redraws district lines.

Tier classification

Permits are classified HIGH, MEDIUM, or LOW by the building permit script based on permit type and estimated cost. HIGH covers major new construction, large additions, and demolitions. MEDIUM covers significant alterations. LOW covers minor work, mechanical permits, and routine maintenance. These thresholds are defined in the script and applied to the raw permit data. They are not adjusted per district or per time period.

The "This week" digest on each district page counts items in each section and flags anything above these thresholds. The counts reflect exactly what is in the cached data at render time.

Limitations