Running SQL in the browser means your dashboard has no backend to break
How in-browser SQL engines actually work, what they can load, where they fall over — and paste-ready queries that run entirely client-side.
The usual way to query a CSV is to send it somewhere. Upload it to a database, point a BI tool at the database, wait for a chart to render over the network. Every one of those hops is a thing that can break, cost money, or leak the file. The interesting fact of the last few years is that you no longer need any of them. A full analytical SQL engine now runs inside the browser tab, and the CSV never leaves your machine.
This post is the literal how-to: how the engine works, what file formats you can load, how big the data can get before the tab starts to struggle, and a set of queries you can paste and run today. No warehouse, no server, no account.
What "SQL in the browser" actually means
The piece that makes this work is DuckDB compiled to WebAssembly — usually written duckdb-wasm. DuckDB is a columnar analytical database (think the query engine of a warehouse, not the transaction engine of Postgres). WASM is a portable binary format that browsers run at near-native speed in a sandbox. Put them together and you get a real OLAP engine executing as a .wasm module on the same JavaScript runtime that draws your page.
Two consequences fall out of that, and they are the whole reason this matters:
- The data never leaves the tab. When you load a CSV, the bytes are read by the WASM module in page memory. There is no upload. Your
revenue.csvdoes not touch a server you don't control, because there is no server in the loop at all. - There is no backend to break. The query planner, the execution engine, and the storage layer are all the same downloaded module. Nothing to provision, nothing to keep running, nothing that 500s at 2am.
The engine binary is a few megabytes, fetched once and cached. The cold start — instantiating the WASM module — is on the order of a couple hundred milliseconds. After that, queries against in-memory data return in single-digit milliseconds. That latency profile is why this feels less like "a database in a webpage" and more like a spreadsheet that happens to speak SQL.
What you can load
An in-browser DuckDB reads the same file formats it reads on a server, registered as virtual tables:
- CSV / TSV — the common case. Headers become column names; types are sniffed automatically. A few hundred thousand rows is comfortable.
- Parquet — columnar and compressed. This is the format to reach for when CSV gets slow: a Parquet file is often five to ten times smaller than the equivalent CSV and scans far faster because the engine only reads the columns your query touches.
- JSON / NDJSON — newline-delimited JSON loads cleanly; deeply nested JSON works but you'll spend query lines unnesting it.
You can register several files at once and join across them. Acustomers.csv and an orders.csv become two tables, and a join is exactly the join you'd write against any relational database.
Where the ceiling is
Be honest about the limits, because this is where people get burned. The engine runs in the tab, so it shares the tab's memory budget. The practical envelope:
- Hundreds of megabytes is fine. A 50–200 MB CSV, or a Parquet file representing several million rows, queries smoothly on a normal laptop.
- Multiple gigabytes is where it hurts. Once the working set approaches the tab's memory ceiling, you'll see slow loads and occasional crashes. This is a single-tab analytics engine, not a billion-row OLAP cluster.
- It is not for live production data. You query the file you loaded. There's no streaming connection to your product database. If you need "as of thirty seconds ago," this is the wrong tool.
The mental model: this replaces the reporting layer for small-to-medium data, not the warehouse. We made the same argument from the other direction in building dashboards without a data warehouse — most internal dashboards are a few thousand rows with a point, and a few thousand rows do not need Snowflake.
Queries you can actually paste
Here is the part that's usually missing from "run SQL in the browser" articles: real queries, not SELECT 1. Assume two CSVs are registered as tables orders and customers. Everything below runs client-side, in the engine described above.
1. A join across two CSVs. Treat each file as a table and join on a key, exactly as you would in any database:
SELECT c.region, COUNT(*) AS order_count, SUM(o.amount) AS revenue FROM orders o JOIN customers c ON c.id = o.customer_id GROUP BY c.region ORDER BY revenue DESC;
2. Date bucketing. The single most common analytical query is "this metric, by month." date_trunccollapses timestamps into buckets so a daily transaction log becomes a monthly trend:
SELECT
date_trunc('month', o.created_at) AS month,
SUM(o.amount) AS mrr
FROM orders o
GROUP BY 1
ORDER BY 1;3. A window function. This is where a real engine earns its keep. Here we compute month-over-month growth without a self-join, using LAG to look at the previous row:
WITH monthly AS (
SELECT
date_trunc('month', created_at) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY 1
)
SELECT
month,
revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS delta,
ROUND(
100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
/ NULLIF(LAG(revenue) OVER (ORDER BY month), 0), 1
) AS pct_growth
FROM monthly
ORDER BY month;None of this is special browser SQL. It's standard analytical SQL, and that's the point — the queries you already know transfer unchanged. Window functions, CTEs, QUALIFY, multiple joins, nested aggregates: they all run, because the engine in the tab is the same engine you'd run on a server.
The security upside is not a footnote
"The data never leaves the tab" reads like a performance detail. It's actually the strongest reason to do this. When you query a CSV in the browser, your finance team's revenue.csv, a customer export with PII, or an unreleased cap table is never transmitted anywhere. There is no upload log, no vendor with a copy, no third-party breach surface, no data-residency form to fill out. The bytes are read into page memory and discarded when you close the tab.
For a lot of analysis that's the difference between "I'll just look at this quickly" and "I need to file a ticket with security first." The most sensitive numbers in a company are exactly the ones nobody wants to upload to a SaaS dashboard. An engine that runs locally sidesteps the entire question.
From a SQL block to a live panel
Running a query is one thing; turning the result into something you can hand to a colleague is another. This is where the in-browser engine stops being a trick and becomes a document format.
In Plain, a dashboard is Markdown. You write a fenced block of data and a panel that declares a SQL query against it. The query runs in the browser using exactly the engine described above, and the result renders as a chart in place — no server round-trip:
```csv orders
month,amount
2026-01,42000
2026-02,47500
2026-03,51200
2026-04,49800
2026-05,58300
```
panel(variant: sql, id: trend)
source: orders
query: |
SELECT month, SUM(amount) AS revenue
FROM orders
GROUP BY month
ORDER BY month
panel(variant: line-chart, data: trend)
title: Monthly revenue
x: month
y: revenueThat Markdown renders to a web page with a live chart. The SQL executed in the reader's browser; the data sat inside the document. Because the artifact is a page at a URL rather than a file behind a login, you share a link and the recipient sees the same panel you did — this is the same idea as the document is a link: the dashboard isn't a .xlsx you email around, it's a static page that happens to contain a query engine.
Two properties follow from that. First, the dashboard has no backend to break — there's nothing to deploy, nothing to keep running, and it works offline because the engine and the data both ship inside the page. Second, it's diff-able and reviewable like any text, since the whole thing is Markdown plus a query rather than a binary blob.
When to reach for this — and when not to
Use an in-browser SQL engine when the data fits in memory, the refresh cadence is "whenever I re-export the file," and you'd rather not stand up infrastructure to answer a question. That covers a surprising share of real analysis: weekly metrics, a one-off investigation, a board chart, a numbers-backed explanation you want to send as a link.
Don't reach for it when you need live data from production, when the dataset is genuinely large (billions of rows, multi-gigabyte scans), or when a team of analysts needs governed, shared models. Those are warehouse problems, and a warehouse is the right answer.
But for the everyday case — a CSV, a few questions, a chart you want to show someone — the engine in your browser is enough. No upload, no server, no backend to break. Just SQL, where the data already is.