Build a read-only dashboard for non-editors
Visualise Northwind Sheet data for stakeholders without giving them edit access.
Published Aug 20, 2025
Northwind’s stakeholders want to see the revenue numbers, but they do not need to edit the Sheet — and sharing the Sheet itself is a blunt instrument. View access still exposes every other tab, every formula, and every stray note, and one accidental edit from a “viewer” who was actually given edit rights causes a morning of cleanup.
This automation serves the data as a web app instead. A single page reads one range from the Sheet, draws it as a chart, and shows nothing else. Stakeholders get a clean dashboard URL; they never touch the spreadsheet. Because the script runs as you, viewers do not even need access to the underlying Sheet.
What you’ll need
- A revenue Sheet with two columns and a header row: a label (month, quarter) in column A and a number in column B. Copy the Sheet ID from its URL.
- An Apps Script project containing two files: a
Dashboard.htmlfile for the page and a.gsfile for the server code. - The project deployed as a web app, set to execute as you so viewers do not need access to the Sheet (see Deploy it).
The HTML
This is the Dashboard.html file. The <?!= ?> tag injects the data the server
prepared, and Chart.js (loaded from a CDN) draws it.
<!-- Dashboard.html — the read-only page stakeholders see. -->
<h1>Northwind dashboard</h1>
<canvas id="chart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// The server fills `series` in before sending the page.
const data = <?!= JSON.stringify(series) ?>;
new Chart(document.getElementById('chart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{ label: 'Revenue', data: data.values }],
},
});
</script>
The script
This is the server-side .gs file. doGet builds the page; revenueSeries
reads the Sheet into the shape the chart expects.
// The Sheet that holds the revenue figures. Copy the ID from its URL.
const REVENUE_SHEET_ID = '1abcRevenueId';
/**
* Serves the dashboard page. Reads the revenue data and injects it into
* the HTML template before sending it to the browser.
*/
function doGet() {
const template = HtmlService.createTemplateFromFile('Dashboard');
// Hand the data to the template; the <?!= ?> tag picks it up.
template.series = revenueSeries();
return template.evaluate()
.setTitle('Northwind dashboard');
}
/**
* Reads the revenue Sheet into the {labels, values} shape Chart.js wants.
*
* @returns {{labels: string[], values: number[]}} Data for the chart.
*/
function revenueSeries() {
const rows = SpreadsheetApp.openById(REVENUE_SHEET_ID)
.getSheets()[0]
.getDataRange()
.getValues();
// Drop the header row, then split into labels (col A) and values (col B).
const [, ...dataRows] = rows;
return {
labels: dataRows.map((r) => r[0]),
values: dataRows.map((r) => r[1]),
};
}
How it works
doGetruns when a stakeholder opens the web app URL. It loadsDashboard.htmlas a template — a template, not a plain file, so the<?!= ?>tag is evaluated.revenueSeriesopens the revenue Sheet and reads the whole used range withgetDataRange.- It drops the header row, then maps the rest into two parallel arrays:
labelsfrom column A andvaluesfrom column B — the exact shape Chart.js expects. doGetassigns that object totemplate.series. Whenevaluate()runs, the<?!= JSON.stringify(series) ?>tag inside the HTML is replaced with the real data.- The browser receives a finished page. Chart.js loads from the CDN and draws a
line chart from the injected
data. - Because the script executes as you, the viewer’s browser never touches the Sheet — they see only the chart.
Example run
The revenue Sheet holds:
| Month | Revenue |
|---|---|
| Jan | 42000 |
| Feb | 47500 |
| Mar | 51000 |
| Apr | 49000 |
A stakeholder opening the web app URL sees a single page titled Northwind dashboard with a line chart rising from 42,000 in January to 49,000 in April. They cannot see the Sheet, other tabs, or any formula — just the chart. When the Sheet is updated, the next page load reflects the new numbers automatically.
Deploy it
The dashboard is only reachable once the project is deployed as a web app:
- In the Apps Script editor, click Deploy then New deployment.
- Choose Web app as the type.
- Set Execute as to yourself — this is what lets viewers see the data without Sheet access.
- Set Who has access to Anyone for a public link, or Anyone within your organisation to keep it internal.
- Click Deploy, approve the authorisation prompt, and share the web app URL.
After any change to the code or HTML, deploy a new version of the same deployment, or viewers keep seeing the old build.
Watch out for
- Re-deploy after edits. Saving is not enough — publish a new version of the deployment, or the live URL serves the previous build.
- “Execute as me” means viewers see whatever you can. The script reads the Sheet with your permissions. Only surface ranges you are happy for every viewer to see, and never inject a whole sensitive tab into the page.
- The data is in the page source.
JSON.stringify(series)writes the numbers straight into the HTML. Anyone who views source sees the raw values — fine for figures you would share anyway, not for anything confidential. createTemplateFromFile, notcreateHtmlOutputFromFile. Only a template evaluates the<?!= ?>tag. The plain version would print the tag literally.- The page is read-only by design. It draws data and nothing else — there is
no write path. For an editable interface, build a form with
google.script.runinstead. - CDN dependency. Chart.js loads from jsDelivr. If that CDN is blocked on a viewer’s network the chart will not render. Pin a specific version in the URL, or host the library yourself, for a critical dashboard.
Related
Build a branded approval interface
Approve Northwind requests through a custom UI — clients click, decision is logged.
Updated Nov 8, 2025
Build an interactive quiz or assessment app
Run Northwind tests with scoring and feedback — questions in a Sheet, results in another.
Updated Nov 4, 2025
Build a multi-page web app with routing
Structure a real Northwind app across views — query-param routing, shared layout.
Updated Oct 31, 2025
Build a form-to-PDF web service
Convert Northwind form submissions to PDFs on the fly — POST in, PDF out.
Updated Oct 27, 2025
Build an expiring secure-download generator
Issue time-limited Northwind links via a web app — token in URL, server-side check.
Updated Oct 23, 2025