Live: SoberMark, the live directory
One of two products I build and run solo under my LLC, Culmination Digital. Source is private.
Read the build story: Building SoberMark
SoberMark is a public directory of addiction-treatment providers, searchable by location, level of care, insurance, and a dozen other filters. 10,900+ providers across all 50 states, assembled from public federal and state datasets and kept current by a scheduled Python pipeline. It ships in English and Spanish.
Why it exists
A person looking for treatment usually starts at a search engine and lands on a directory. The useful facts (where a provider is, what level of care it offers, what it accepts) already exist in public federal data, but they are scattered across sources that disagree with each other. SoberMark is the work of pulling those sources into one clean, searchable record per provider.
How it's built
Next.js 16 App Router on Vercel, TypeScript strict, in a pnpm monorepo with a Python ingest app beside the web app. Postgres with PostGIS does the heavy lifting.
Ingest. 20 source connectors cover SAMHSA, NPPES, federal exclusion and debarment lists, CMS, and state licensing boards. Addresses are geocoded through the Census batch API. A monthly scheduled job refreshes the core federal sources against a throwaway database first, then production, then refreshes the search views and runs data validation: the run fails if the provider count or state coverage drops below a safe floor, and it also checks geocoding and how recently each source was ingested.
Combining sources. The same facility appears in several datasets with different names, phone numbers, and address formats. Records are matched in a fixed order: exact NPI first, then phone and state only when the city and name or street and ZIP also agree, then a normalized name, city, and state. That middle rule exists because an early version merged providers across cities on a shared phone number. The higher-confidence source wins each field, and every record keeps the list of sources it came from.
Search. Radius search runs on PostGIS geography. Text search uses Postgres full-text ranking and falls back to trigram similarity when someone misspells a name or a city. Results come from a materialized view so the request path stays fast, with filters for level of care, insurance, telehealth, language, medication-assisted treatment, and more.
Data management. 239 forward-only migrations, row-level security on the tables, and pre-merge scripts that validate pending migrations and check environment parity. Claude is used in a few back-office paths, such as summarizing listings and assisting with ownership-claim reviews; the claim assistant is built so it can never approve or reject a claim on its own.
The trade-offs
Building from public data means the directory is broad before it is deep. A listing can be right about address and services and still know nothing about today's waitlist. Curating by hand does not scale to ten thousand providers for one engineer, so I chose breadth with honest labeling, plus a path for providers to correct or enrich their own record.
What I learned
How much of a data product is deciding which source to believe. The search is the visible part; the matching rules, the validation floors, and the migrations underneath are where the real work went. The full story is in the build post.