WaitingForPower — an Energy Project Tracker
Open source, MIT licensed — see LICENSE. Contributions,
forks, and issues welcome.
Tracks proposed U.S. energy projects — generation, transmission, storage, LNG, and pipelines, every fuel type — and how long each has been waiting for approval, and why.
The argument is structural, not partisan: solar, wind, storage, gas,
nuclear, hydro, LNG, pipelines, and transmission all get stuck in the same
handful of bottlenecks. Every tracked delay is mapped to one of seven named
cause categories (src/lib/data/causeCategories.ts).
The site's policy argument — six specific, bipartisan reform proposals, one
per structural bottleneck, each with a stated problem, proposal, strengths,
weaknesses, and bill links — lives on a single page,
/policies
(src/lib/data/policies.ts), deliberately kept
separate from the neutral cause-category data so "why a project is stuck"
and "what we're arguing should change" aren't the same object.
Quick start
npm install
cp .env.example .env # set DATABASE_URL to your own Postgres instance
npx prisma migrate deploy # applies the committed migrations to your database
npm run ingest:permitting-dashboard # populate from a live, no-key-needed source
npm run dev
Needs a real Postgres connection string in DATABASE_URL — there's no
bundled local database file. The deployed app and local dev both point at
the same hosted Postgres instance for this project (see "Architecture"
below for why); for your own fork, any Postgres works (Neon, Supabase,
Vercel/Prisma Postgres, RDS, local postgres via Docker, etc.).
Data & sourcing
All project data comes from live, re-runnable sources — no hand-curated
one-off research. An earlier version of this project shipped a small
hand-researched seed set; it was removed deliberately in favor of sources
that stay current on their own, rather than a snapshot that goes stale.
See src/lib/ingest/README.md for the full
per-source table, open questions, and how each is scheduled:
Every source above — 4 federal/national workbook sources plus the Permitting Dashboard's live API
plus all forty-one state docket modules — runs on Vercel Cron (vercel.json) with no manual
step, staggered by 30 minutes so no two sources' runs overlap. Cadence is matched to how often
each source actually publishes, not one uniform schedule: the 41 state modules and the Permitting
Dashboard run daily, since same-day filings are exactly what the homepage changes feed is
built to surface; the 4 sources whose underlying data only republishes monthly, quarterly, or
annually (EIA-860M, LBNL Queued Up, ORNL HydroSource, EIA's pipeline tracker) stay weekly,
since checking them daily wouldn't catch anything new. This moved off Hobby-plan-driven weekly
checks (see src/lib/ingest/README.md for the full cadence history)
once the site upgraded to Vercel Pro. Every ingestion run upserts by a stable per-source ID (see
the matchKey/manualOverrides.csv identity mechanism in open question #1), so a re-run updates
existing projects in place instead of duplicating them.
Every ingested project links back to its public source (see each project's
detail page). Where a date or figure wasn't confidently available, the
project is marked dateConfidence: "approximate" or carries a
dataQualityNote saying so, rather than presenting invented precision as
fact. Notably absent so far: sub-250MW generation/storage projects (see
each source's capacity floor / scope above — this includes the large
majority of hydropower relicensing dockets, which skew small), and any
cause-category assignment for automatically-ingested projects (none of the
five sources publishes why a project is delayed — see open question #4).
This site only tracks projects still waiting on approval — see
"RESOLVED_STAGES" in src/lib/ingest/README.md for what's deliberately
excluded and why.
Interconnection queue detail. lbnlQueuedUp.ts carries two extra fields
for LBNL-sourced projects, added 2026-08-21: interconnectionQueueStage
(the workbook's own study-phase label, e.g. "Feasibility Study") and
networkUpgradeCostUsd (reserved for a join against LBNL's separate,
irregularly-updated interconnection cost-analysis datasets — not yet
populated by any module). Per explicit product decision, a suspended
interconnection request is treated the same as withdrawn: not shown as
"waiting." Every ingestion run now also actively removes a previously-shown
project the moment a later edition reports it withdrawn, suspended, or
operational, rather than leaving it frozen in its last-known state — see
src/lib/ingest/README.md for the full detail and open question #9.
Open questions
Flagged deliberately rather than guessed at — see also
src/lib/ingest/README.md for the
per-data-source version of this list.
- Cross-source project identity matching is a real, ongoing problem,
not fully solved. EIA, the Permitting Dashboard, and now the growing
state-docket series each use their own name/ID for what might be the
same physical project.
src/lib/ingest/manualOverrides.ts+.csvlets a human declare two source records the same project via a sharedmatchKey— there's no automated fuzzy-matching, deliberately (name similarity + geographic proximity + capacity similarity is flagged as the highest-value follow-up engineering task, not attempted here). A real merge bug in this mechanism was found and fixed 2026-08-23:upsertNormalizedProject(common.ts) used to look up existing projects byslug, butslugis derived from a project's name, which differs between sources — so two records sharing a manually-declaredmatchKeystill produced two separate rows instead of merging into one, silently defeating the override file's entire purpose (caught by hand when Illinois's new CPCN docket for Grain Belt Express turned out to duplicate an existing Permitting Dashboard entry for the same line). Fixed by adding a realmatchKeycolumn toProject(prisma/schema. prisma) and keying the upsert on that instead, with a slug-based fallback lookup so every project ingested before this column existed self-heals (gets its real matchKey backfilled) the next time its own source naturally re-ingests it — no separate backfill migration needed.ProjectSourcerows are now upserted by(projectId, label)rather than deleted-and-recreated on every run too, so a merged project keeps both sources' links instead of the more-recently-run source's write wiping out the other's.manualOverrides.csvcurrently has one real entry (Grain Belt Express: Permitting Dashboard project_id 109441 + Illinois ICC docket 22-0499) — the two other originally-flagged duplicates, SouthCoast Wind and Ocean Wind 1, have no current overlap to merge (see the "NOTE on cross-source identity matching" comment inpermittingDashboard.ts), so nothing is declared for them yet. - Permitting Dashboard's Socrata dataset is a denormalized join, not one row per project — a single query can return dozens of byte-for-byte duplicate rows per project. The ingestion module dedupes before normalizing; watch for this if the row count from a fresh run ever looks far higher than expected.
- Permitting Dashboard has no public milestone/timeline or
application-filed-date field on the open Socrata dataset this project
used — that data likely exists behind the token-gated
/api/v1/project/{id}endpoint mentioned in the dashboard's own docs, which wasn't registered for in this pass.
Architecture
- Next.js (App Router) + TypeScript + Tailwind v4, single app.
- Prisma + Postgres (
prisma/schema.prisma) — one hosted instance used for both local dev and production; see open question #9 for why this project moved off SQLite. - MapLibre GL JS for the map — a free CARTO Voyager vector basemap (no
API token required), with projects rendered as plain DOM markers
(
maplibregl.Marker) sized by capacity, colored uniformly. An earlier version used a clustered GeoJSON source rendered as GL circle layers, but that pipeline (worker-built tiles + GL repaint) wasn't reliably rendering in production; DOM markers sidestep it entirely at the cost of native clustering — see git history onsrc/components/Map.tsx. - Filters live as React state in
src/components/Explorer.tsxand drive both the map and the sortable list/table view from one source of truth (src/lib/filters.ts), with live-updating aggregate stats (src/lib/stats.ts).
Project schema
See prisma/schema.prisma for the authoritative version. Key point: cause
categories (and the policies argued for at /policies) are not a
database table — they're fixed, small, code-reviewed sets in
src/lib/data/causeCategories.ts and src/lib/data/policies.ts. Projects
reference a cause by string slug, validated in app code, not a DB foreign
key — adding a new cause category or policy is meant to be a deliberate
product/policy decision, not something an ingestion script can do silently.