GreenStack — Carbon & Cost Command Center
Inspiration
Every time I spun up a new EC2 instance or deployed a Lambda, I thought about cost and never carbon. But cloud infrastructure is one of the fastest-growing sources of energy consumption on the planet. Data centers consumed roughly 200–250 TWh of electricity globally in 2022, and that number is climbing.
The problem isn't that engineers don't care, it's that the feedback loop is invisible. AWS Console shows you dollars. It never shows you tons of CO₂.
I wanted to build a tool that makes the carbon cost of cloud infrastructure as tangible as the dollar cost. Something a developer could drop into their workflow today, without changing how they deploy or which cloud they use. That became GreenStack.
What I Built
GreenStack is a developer-focused dashboard that turns cloud billing exports and Terraform files into an actionable carbon report with AI-generated green refactoring suggestions and one-click Terraform remediation.
Core features:
- Drag-and-drop ingestion of
.json/.csvbilling exports (no live cloud connection needed) - Carbon math engine mapping 16 AWS regions to real-world grid intensity data
- 4 KPI cards + trend chart + service breakdown + region heatmap + carbon/latency scatter plot
- GreenScore — a letter grade (A–F) for your infrastructure's carbon efficiency
- What-If Simulator — toggle regions and Graviton instances to preview CO₂/cost savings before committing
- IaC Audit — upload or paste a
.tffile; Gemini AI extracts resources and computes their footprint - AI Green Advisor — Gemini 2.5 Flash analyzes your top emitters and returns categorized suggestions
- One-Click Remediation — each suggestion has a "Generate Fix" button that produces a ready-to-apply
.tffile
How I Built It
Stack
| Layer | Library |
|---|---|
| Framework | Next.js 14 (App Router) |
| Styling | Tailwind CSS |
| Charts | Recharts |
| Icons | Lucide React |
| CSV Parsing | PapaParse |
| AI | Google Gemini 2.5 Flash (@google/generative-ai) |
The Carbon Engine
The core of GreenStack is a static carbon intensity map, real gCO₂eq/kWh figures per AWS region, sourced from public grid data. Every resource's emissions are computed as:
$$ \text{CO}_2 \text{ (grams)} = \text{Usage (kWh)} \times \text{Grid Intensity (gCO}_2\text{eq/kWh)} $$
The GreenScore is derived from the usage-weighted average grid intensity across all resources, this is then mapped to a 0–100 score on a logarithmic scale anchored at known clean/dirty endpoints (Stockholm at 8 gCO₂eq/kWh ≈ 100, Sydney at 760 gCO₂eq/kWh ≈ 0), and bucketed into letter grades A–F.
$$ \text{Score} = \text{clamp}!\left(\frac{\ln(760) - \ln(\bar{I})}{\ln(760) - \ln(8)} \times 100,\ 0,\ 100\right) $$
The What-If Simulator
The simulator holds a parallel set of region overrides in React context.
When the user toggles Graviton or changes a region, the simulated footprint is recomputed in a useMemo:
simulated_kwh = original_kwh × 0.8 (for EC2 with Graviton enabled)
simulated_co2 = simulated_kwh × intensity(override_region)
No network calls, everything is instant, local, and reversible.
AI Integration (Gemini 2.5 Flash)
Three API routes talk to Gemini:
/api/iac-parse— sends raw Terraform HCL, gets back a structuredCloudResource[]JSON array/api/advisor— sends the top 5 highest-carbon resources, gets back 4–6 categorized suggestions/api/remediate— sends one suggestion + those top resources, gets back complete production-quality Terraform HCL implementing the fix
The remediation prompt is carefully engineered: it instructs Gemini to produce valid HCL only (no markdown fences, no prose), open with a comment block summarizing the savings, use actual resource IDs from the user's data, and follow Terraform style conventions.
The IaC parser also has a client-side regex fallback, if the Gemini API call fails or the key
isn't set, lib/terraform-parser.ts parses the HCL with pure regex, covering the four most common
resource types: aws_instance, aws_lambda_function, aws_db_instance, aws_s3_bucket.
Challenges
1. Regex vs. Multi-Line HCL
Terraform HCL blocks span multiple lines, and JavaScript's . doesn't match newlines by default.
The s (dotAll) flag would fix it, but it has inconsistent support. I used [\s\S]*? instead of
a deliberate choice that trades readability for compatibility.
const resourceRegex = /resource\s+"(\w+)"\s+"([^"]+)"\s*\{([\s\S]*?)\}/g;
Getting the lazy *? right to avoid matching across multiple resource blocks took several iterations.
2. Prompt Engineering for Structured Output
Gemini is a language model, it wants to explain itself. Getting it to return a bare JSON array with no surrounding text required explicit, repeated instruction in the system prompt:
"Respond ONLY with a valid JSON array. Do not add markdown formatting or explanation text."
Even then, the response sometimes arrived wrapped in `json fences. I added a regex extraction
step as a safety net: text.match(/\[[\s\S]*\]/) — extract the first JSON array found, regardless
of surrounding prose.
3. Next.js Static Analysis vs. Dynamic API Routes
Next.js tries to statically analyze API routes at build time. Any route that references
process.env in module scope (not inside a function) triggers a build error when the variable
isn't set. The fix was adding export const dynamic = "force-dynamic" to every API route,
which opts them out of static generation entirely.
4. Deduplication Across the Codebase
Early on, the carbon intensity map was copy-pasted into three places: lib/carbon.ts,
app/api/advisor/route.ts, and app/api/remediate/route.ts. Any correction to one region's
figure would need to be applied in three places. I centralized everything in lib/carbon.ts
with a single exported getRegionIntensity() function, and had both API routes import from it.
Small fix, meaningful discipline.
5. Keeping Charts Readable at Small Sizes
Recharts defaults are built for dashboards with lots of space. At the widths GreenStack targets, axis labels overlapped, tick marks cluttered the background, and tooltips clipped outside the chart container. I ended up:
- Removing all axis lines and only keeping the x-axis baseline
- Using
angle={-35}on x-axis tick labels with adyoffset - Replacing default tooltips with fully custom
<foreignObject>-based components - Setting
isAnimationActive={false}on initial render to prevent layout jank
What I Learned
Carbon data is surprisingly accessible. The grid intensity figures I used come from publicly available sources (Electricity Maps, EPA eGRID). The hard part isn't finding the data, it's surfacing it at the right moment in a developer's workflow.
Prompt engineering is real engineering. The difference between a Gemini response that's immediately usable and one that requires three post-processing steps comes down to how precisely you constrain the output format in the system prompt.
React context boundaries matter. Separating DataContext (billing state) from SimulatorContext
(what-if overrides) meant the simulator could derive its state from billing data without coupling
the two concerns. The useMemo in SimulatorContext recomputes the simulated footprint only when
overrides or the graviton flag change.
Tools that close the feedback loop change behavior. The most useful thing GreenStack does isn't
the AI suggestions — it's showing the number. Once you can see that us-east-1 emits
\( 3\times \) more CO₂ per kWh than us-west-2, moving a workload west becomes an obvious decision,
not a vague intention.
What's Next
- Support for GCP and Azure resource types in the IaC parser
- Live AWS Cost & Usage Report integration (direct S3 read)
- A CLI (
npx greenstack check) so the GreenScore can gate CI pipelines - Historical trend tracking: persist uploads and plot score over time
Built with Next.js 14, Tailwind CSS, Recharts, and Google Gemini 2.5 Flash.
Built With
- gemini
- next.js-14
- recharts
- tailwind-css
Log in or sign up for Devpost to join the conversation.