Inspiration
Houston has no zoning. That means a concrete plant, a landfill, or an industrial site can end up near homes, schools, and parks, and the only formal moment residents can weigh in is while a permit is under review.
The information about those permits is public. But when we tried to answer a simple question, "What's permitted near my home?", we found it scattered across state and federal agencies, written for engineers and lawyers, and organized by permit number instead of by place. You can't search for a permit you don't know exists.
It matters for two reasons:
- The environment. Permits decide what's allowed into our air and water. Across the Houston-Galveston region, 42% of stream miles are impaired by bacteria (H-GAC 2020 Basin Highlights Report), and wastewater discharge permits are the rules that govern what goes into those bayous.
- People's homes. A study of 1,600 industrial plant openings and closings found that home values within one mile fell about 1.5% when a plant opened, with most of the effect within half a mile (Currie et al., American Economic Review, 2015).
So we built Permit Watch: the data was always public; we wanted to make it visible.
What it does
Enter any Houston-area address or ZIP code, choose a radius from 1 to 10 miles, and Permit Watch shows the environmental permits nearby on one map:
- Four categories: concrete batch plants, industrial air permits, wastewater discharge permits, and solid waste permits, each with its own marker shape
- Filters by category and by status (pending, effective, issued, expired, and more)
- Permit details: facility, status, distance from you, and the facts listed in the record
- A link to the official TCEQ or EPA source for every record, so residents never have to rely only on our interpretation
- Editable drafts: a public comment for permits where that fits, or a concerns / information-request letter for older or unknown records, plus the official TCEQ complaint link
- English and Spanish, light and dark mode, and shareable URLs that preserve your search
The current dataset contains 807 real, validated permits. It's a source-scoped snapshot of the Houston region, not every environmental permit in the city.
How we built it
Data pipeline. The hard part was the data. We wrote TypeScript collectors (plus a small Python step for one waste spreadsheet) that pull from TCEQ air permit records and notices, EPA ECHO / ICIS-NPDES wastewater data, and TCEQ landfill GIS and authorization files. Each source has its own format, IDs, and status vocabulary, so we normalized them all into one shared permit model.
Validation. Every record is checked with Zod: stable IDs, authorization numbers, categories, coordinates, dates, and official HTTPS source links. Invalid rows are rejected and logged with a reason, never silently fixed. For wastewater alone, we downloaded 1,513 rows and kept 559; we excluded rows with coarse coordinates, non-individual permit IDs, duplicates, and locations outside the region.
Backend. Node.js, Express, and TypeScript, with SQLite (better-sqlite3). The search API filters permits by location, radius, category, and status, and calculates each permit's distance from the searched address.
Frontend. React, TypeScript, Vite, and Tailwind CSS, with MapLibre GL for the map, Framer Motion for transitions, and React Three Fiber for the landing globe. When two permits share the exact same coordinates, the map offsets their markers on screen so both stay clickable, without changing the real coordinates or distances.
Testing. 210 automated tests (Vitest and Supertest), 13 Playwright browser scenarios, a secret scan, and a local stress test of 1,480 requests with zero failures.
No AI required. Summaries and drafts are built directly from dataset fields and the user's selected concerns, so nothing is invented.
Challenges we ran into
- Four sources, four languages. Air, water, and waste data use different IDs, status terms, and location formats. Mapping them into one model without losing meaning took most of our time.
- Being precise about what a permit means. A pending permit isn't an approved one, an approved permit doesn't prove a facility is operating, and permitted limits aren't measured pollution. We built those distinctions into the data model, the UI copy, and the drafts.
- Missing data. Many records lack operators, emissions, or deadlines. We decided never to guess: missing fields say "Not listed in source," and the app only shows a comment deadline when the official record supports it.
- Location quality. Some sources only have approximate or mailing-address coordinates, so we excluded imprecise rows rather than put misleading pins on the map.
- Cutting features. Early versions used AI for summaries and routing. We removed them so every word on screen traces back to an official record.
Accomplishments that we're proud of
- 807 real permits across four categories, every one with coordinates and an official source link
- A validation pipeline that logs exactly why each rejected row was excluded
- A careful, honest product: no health claims, no invented deadlines, no guesswork
- A polished, bilingual app with solid test coverage, built in a weekend
What we learned
Public data isn't the same as usable data. Most of the real work was cleaning, validating, and explaining, not displaying. We also learned how much wording matters in civic tech: "permitted," "pending," and "operating" mean very different things, and residents deserve to know which is which.
What's next for Permit Watch
- Expanding from the Houston area to all of Texas
- Adding more permit types
- Alerts when a new permit is proposed near a saved address
- Shareable neighborhood links and QR codes for community groups
- Tracking official comment windows wherever the source confirms them
Built With
- better-sqlite3
- epa-echo
- express.js
- framer-motion
- github
- javascript
- maplibre-gl
- node.js
- openfreemap
- playwright
- python
- react
- react-three-fiber
- render
- sqlite
- supertest
- tailwindcss
- tceq
- three.js
- typescript
- us-census-geocoder
- vite
- vitest
- zod

Log in or sign up for Devpost to join the conversation.