Voltstack
Documentation
What the product does today, written from the codebase rather than the pitch. If something below is wrong, it is out of date rather than aspirational: file it against this page.
Overview
Voltstack is a workspace for building analytics dashboards for European power and gas markets. A workspace is a grid of widgets: price charts, storage monitors, constraint tables, tickers. You add widgets from a palette, arrange them on a canvas, and the layout persists to your account.
The product is aimed at desks that would otherwise be pulling ENTSO-E, GIE, and Elexon data into spreadsheets by hand. Some of that pulling is done for you already; some widgets are still illustrative placeholders. The next section explains how to tell which is which, because that distinction matters more than anything else on this page.
Widgets and data
Widgets fall into three rough groups today:
- Live feeds: day-ahead power prices, cross-border flows, generation mix, gas storage (GIE AGSI+/ALSI+), gas pipeline flows (ENTSOG), GB balancing and system price (Elexon), weather (Open-Meteo), negative-price radar, and event/constraint feeds (JAO flow-based constraints, REMIT UMM messages, German redispatch). These read from a database that a background worker keeps refreshed from the real upstream APIs, not from the browser calling them live.
- Partially live: a few widgets bind some rows or series to real data and fill the rest with a modeled overlay (for example a historical storage band that isn't itself a live feed). Where that happens the widget says so in its own label.
- Illustrative: forward curves, hub basis spreads, LNG arbitrage, carbon spot, merit-order curves, and implied volatility have no free public feed behind them (they are licensed instruments from ICE, EEX, or Platts). These render generated numbers so you can see the widget shape, and they carry a badge so nobody mistakes them for real quotes.
For the exact, currently-registered feed list and how fresh each one is right now, see /status, a public page that reads its numbers from the same health check the app itself uses. That page is the source of truth; a fixed table here would go stale as feeds are added.
Data honesty
Every widget title bar carries a small dot that tells you what you're looking at:
- Connected, live data, fresh.
- Stale, a live feed whose last update is older than expected.
- Simulated, no live path, or a live feed that fell back; the widget also shows a violet SIM pill.
- Disconnected, a feed that should be live and isn't answering.
Live widgets also show a "Data age" line: how long ago the value you're looking at was actually fetched from the upstream source, not just when the page last redrew. Upstream publication schedules vary a lot (some feeds settle same-day, some lag by weeks), so a widget being a day old can be entirely normal rather than broken. The age is there so you can judge that for yourself instead of guessing.
Workspaces
A workspace is a saved arrangement of widgets. You can rename it, create as many as you want, and delete the ones you don't. Workspaces persist to your account (not just the browser you're on), and past versions are kept so you can restore an earlier layout if you rearrange something you regret.
Read-only share links
The Share button in the top bar copies a link that renders your current workspace layout at /view to anyone who opens it, signed in or not. It is a snapshot of the layout at the moment you copied it, not a live-editing session: the recipient sees the widgets and their data, but cannot rearrange or modify anything, and does not need an account.
CSV export
Every widget's overflow menu (the ⋮ in its title bar) has an Export CSV option that downloads whatever that widget is currently showing. For pulling more history than one widget displays, there is also a server-side export endpoint (/api/export/market) that returns CSV for a given feed. CSV only for now; there is no Excel export.
Feeds are stored in three shapes, chosen with &type=, and which filters apply depends on the shape. &from= and &to= narrow the date range on all three:
- type=points (the default, and the shape behind most widgets) also takes &seriesKey= and &category=.
- type=constraints also takes &seriesKey=, which selects the region column and is matched case-sensitively. This shape has no category.
- type=events takes the date range only, over the publication timestamp. This shape has neither a series nor a category.
Naming a filter a shape cannot honour returns 400 and says which one. So does any parameter this endpoint does not read at all, and so does giving any parameter twice, because only the first value would be read and the second would go unapplied. Between them that is why a filter you asked for is never quietly dropped from a response that otherwise looks complete. The eight it reads are format, feedId, type, seriesKey, category, from, to and limit; the names are case-sensitive, and each may be given at most once. An empty seriesKey or category is a filter rather than an omission: &category= selects the rows whose category is the empty string, which is a real value in this data.
The endpoint returns at most 5,000 rows by default and 20,000 with an explicit &limit=. A response that hit the cap says so rather than looking complete: X-VS-Truncated: true, alongside X-VS-Row-Count and X-VS-Row-Limit, and the downloaded file is named …-truncated.csv so the signal survives the download. To pull a deeper history than 20,000 rows, narrow the request with &from= and &to= and take the range in pieces. There is no page-continuation token: each shape is sorted on a single timestamp column that many rows share, so a cursor keyed on that column alone would either re-serve or skip a whole block of rows at the page boundary. The rows themselves are uniquely keyed; the sort order is not, and we have not built the composite cursor that would close the gap.
Getting started
Signing up creates your organization and a database provisioned for it automatically, no separate setup step. Two things are required for that to succeed:
- A company email address. Personal domains (Gmail, Outlook, Yahoo, iCloud) cannot start a new organization; onboarding will ask you to sign up again with your work address.
- Provisioning must complete. Onboarding sets up a dedicated database for your desk, and everything account-backed — saved workspaces, the Team panel and member invitations, alerts — depends on it. If provisioning can't finish, onboarding pauses on a "Setup didn't finish" screen with a retry button rather than dropping you into a desk where those features would fail. Retrying is safe; if it keeps failing, contact us and we'll finish setup for you.
From there you land in a default workspace with a starter set of European power widgets already on the canvas. A short guided tour runs the first time you open a workspace; you can run it again from Help in the sidebar.
Plan structure and trial terms are on the pricing page. For the live status of every data feed, see /status.