Sitemap
Daniel Line

Daniel Line

Build a Dead-Simple Service Status Page in 10 Minutes

7 min readMar 18, 2026

--

Every website goes down eventually. Servers hiccup, APIs misbehave, databases get slow. When that happens, the worst thing you can do is leave your users in the dark — wondering if the problem is on their end, refreshing the page, filing a support ticket that says “is this broken for everyone or just me?”

A status page fixes that. It sets expectations, shows you’re aware of the issue, and saves your support inbox from a flood of “is everything okay?” messages.

The problem is that most status page solutions are either expensive, require a separate service to run, or need a database to store the current state of each component. For a side project, a small SaaS, or a lean team, that’s a lot of infrastructure for something that’s fundamentally just a few fields of data.

In this article, we’ll build a clean, functional service status page from scratch — one you can update from your terminal in seconds, with nothing but a hosted JSON file and some vanilla JavaScript.

Press enter or click to view image in full size

What We’re Building

A status page with:

  • A list of services (API, Website, Database, etc.)
  • Color-coded status indicators: Operational, Degraded, or Outage
  • Optional notes for each service (“Investigating slow response times”)
  • A “last updated” timestamp
  • Zero backend — all state lives in a single JSON file

Here’s what the end result looks like in use:

All Systems — Last updated: 2 minutes ago

🟢 Website — Operational
🟢 API — Operational
🟡 Authentication — Degraded —
Slower response times than usual
🔴 File Storage — Outage —
Investigating storage connectivity issues

Step 1: Design the JSON Structure

The entire state of your status page lives in a single JSON document. Here’s the structure we’ll use:

{
"last_updated": "2025-03-18T10:00:00Z",
"services": [
{
"name": "Website",
"status": "operational",
"note": ""
},
{
"name": "API",
"status": "operational",
"note": ""
},
{
"name": "Authentication",
"status": "degraded",
"note": "Slower response times than usual"
},
{
"name": "File Storage",
"status": "outage",
"note": "Investigating storage connectivity issues"
}
]
}

Three status values cover everything you’ll realistically need:

ValueMeaningoperationalEverything is working normallydegradedThe service works but is slow or partially impairedoutageThe service is down or completely unavailable

The note field is optional context — leave it empty when everything is fine, fill it in when something needs explaining.

Step 2: Host the JSON File

You need a place to store this document that:

  • Serves it publicly with a GET request (no auth required for your users)
  • Lets you update specific fields remotely with a simple API call
  • Doesn’t require you to run anything

JSONhost.com is a good fit here. It’s a JSON hosting service built exactly for this kind of use case — you upload a JSON file, get a public URL back, and can read or write to it via a REST API.

Get Daniel Line’s stories in your inbox

Join Medium for free to get updates from this writer.

Create your JSON file:

  1. Sign up and go to your dashboard
  2. Create a new JSON file and paste in the structure above
  3. Note down the file’s ID and your API Authorization token (from the file’s Settings tab)

Your status data is now live at:

GET https://jsonhost.com/json/YOUR_FILE_ID

No authentication needed to read it — which is exactly what you want, since your status page needs to fetch it without credentials.

Step 3: Build the Status Page

Here’s a complete, self-contained HTML file. Drop it anywhere — a GitHub Pages site, Netlify, your existing domain, or even just open it locally.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>System Status</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
color: #1a1a2e;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 60px 20px;
}
.container { width: 100%; max-width: 640px; } h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 4px; } .meta {
font-size: 0.85rem;
color: #6b7280;
margin-bottom: 32px;
}
.overall {
display: flex;
align-items: center;
gap: 10px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 16px 20px;
margin-bottom: 16px;
font-weight: 600;
font-size: 1rem;
}
.services { display: flex; flex-direction: column; gap: 10px; } .service {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 14px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.operational { background: #22c55e; }
.dot.degraded { background: #f59e0b; }
.dot.outage { background: #ef4444; }
.service-info { flex: 1; }
.service-name { font-weight: 600; font-size: 0.95rem; }
.service-note {
font-size: 0.82rem;
color: #6b7280;
margin-top: 2px;
}
.badge {
font-size: 0.75rem;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
text-transform: capitalize;
}
.badge.operational { background: #dcfce7; color: #16a34a; }
.badge.degraded { background: #fef3c7; color: #b45309; }
.badge.outage { background: #fee2e2; color: #dc2626; }
.error {
text-align: center;
padding: 40px;
color: #6b7280;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<h1>System Status</h1>
<p class="meta" id="meta">Loading…</p>
<div id="overall" class="overall" style="display:none"></div>
<div class="services" id="services"></div>
</div>
<script>
const STATUS_URL = 'https://jsonhost.com/json/YOUR_FILE_ID';
const statusIcon = {
operational: '🟢',
degraded: '🟡',
outage: '🔴',
};
function overallStatus(services) {
if (services.some(s => s.status === 'outage')) return 'outage';
if (services.some(s => s.status === 'degraded')) return 'degraded';
return 'operational';
}
function timeAgo(iso) {
const diff = Math.floor((Date.now() - new Date(iso)) / 1000);
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)} minute${Math.floor(diff / 60) !== 1 ? 's' : ''} ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)} hour${Math.floor(diff / 3600) !== 1 ? 's' : ''} ago`;
return `${Math.floor(diff / 86400)} day${Math.floor(diff / 86400) !== 1 ? 's' : ''} ago`;
}
async function render() {
try {
const res = await fetch(STATUS_URL);
const data = await res.json();
document.getElementById('meta').textContent =
`Last updated: ${timeAgo(data.last_updated)}`;
const overall = overallStatus(data.services);
const overallEl = document.getElementById('overall');
overallEl.style.display = 'flex';
overallEl.innerHTML = `
<span class="dot ${overall}"></span>
${overall === 'operational'
? 'All Systems Operational'
: overall === 'degraded'
? 'Partial System Degradation'
: 'Service Disruption in Progress'}
`;
const container = document.getElementById('services');
container.innerHTML = data.services.map(s => `
<div class="service">
<span class="dot ${s.status}"></span>
<div class="service-info">
<div class="service-name">${s.name}</div>
${s.note ? `<div class="service-note">${s.note}</div>` : ''}
</div>
<span class="badge ${s.status}">${s.status}</span>
</div>
`).join('');
} catch (e) {
document.getElementById('services').innerHTML =
'<div class="error">Unable to load status. Please try again shortly.</div>';
}
}
render();
setInterval(render, 60_000); // refresh every minute
</script>
</body>
</html>

Replace YOUR_FILE_ID with the ID from JSONhost, and that's it. The page fetches live status on load and refreshes automatically every 60 seconds.

Step 4: Update a Service Status

This is where things get satisfying. When something goes wrong, one command is all it takes.

Mark the API as down:

curl -X PATCH "https://jsonhost.com/json/YOUR_FILE_ID" \
-H "Authorization: YOUR_API_TOKEN" \
-d '[
{"op": "replace", "path": "/services/1/status", "value": "outage"},
{"op": "replace", "path": "/services/1/note", "value": "API is down — we are investigating now"},
{"op": "replace", "path": "/last_updated", "value": "2025-03-18T14:30:00Z"}
]'

Mark it as recovering:

curl -X PATCH "https://jsonhost.com/json/YOUR_FILE_ID" \
-H "Authorization: YOUR_API_TOKEN" \
-d '[
{"op": "replace", "path": "/services/1/status", "value": "degraded"},
{"op": "replace", "path": "/services/1/note", "value": "Service is recovering — monitoring closely"},
{"op": "replace", "path": "/last_updated", "value": "2025-03-18T15:00:00Z"}
]'

Clear an incident:

curl -X PATCH "https://jsonhost.com/json/YOUR_FILE_ID" \
-H "Authorization: YOUR_API_TOKEN" \
-d '[
{"op": "replace", "path": "/services/1/status", "value": "operational"},
{"op": "replace", "path": "/services/1/note", "value": ""},
{"op": "replace", "path": "/last_updated", "value": "2025-03-18T15:45:00Z"}
]'

The PATCH endpoint uses JSON Patch (RFC 6902), which lets you update multiple fields atomically in a single request. The array index in the path (e.g. /services/1/status) corresponds to the position of the service in your JSON array — 0 is Website, 1 is API, and so on.

If you prefer targeting by name, you can reorganize your JSON structure so each service is a key instead of an array item:

{
"last_updated": "2025-03-18T10:00:00Z",
"services": {
"website": { "status": "operational", "note": "" },
"api": { "status": "operational", "note": "" },
"authentication": { "status": "degraded", "note": "Slower response times" }
}
}

Then your patch path becomes /services/api/status — no index math required.

Step 5: Wrap It in a Shell Script (Optional)

If you’re updating status during an incident, typing that curl command under pressure is error-prone. A small shell script makes it faster and safer:

#!/bin/bash
# update-status.sh
# Usage: ./update-status.sh <service-index> <status> "<note>"
ID="YOUR_FILE_ID"
TOKEN="YOUR_API_TOKEN"
INDEX=$1
STATUS=$2
NOTE=$3
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
curl -s -X PATCH "https://jsonhost.com/json/$ID" \
-H "Authorization: $TOKEN" \
-d "[
{\"op\": \"replace\", \"path\": \"/services/$INDEX/status\", \"value\": \"$STATUS\"},
{\"op\": \"replace\", \"path\": \"/services/$INDEX/note\", \"value\": \"$NOTE\"},
{\"op\": \"replace\", \"path\": \"/last_updated\", \"value\": \"$NOW\"}
]" && echo "Status updated."

Now updating is just:

./update-status.sh 1 outage "API is down — investigating now"
./update-status.sh 1 operational ""

Going Further

A few directions you might take this:

Add an incident history. Extend the JSON to include a history array and use the PATCH add operation to append new entries without overwriting old ones. Your status page can then show a timeline of past incidents.

Automate the timestamp. If you’re using a CI/CD pipeline or a monitoring tool, you can call the API automatically when a health check fails — no manual intervention needed.

Embed it in your app. The same JSON endpoint you use for the status page can also be consumed by your app itself. Show a small in-app banner when status is not operational, without any server-side logic.

Wrapping Up

Building a status page doesn’t have to mean provisioning infrastructure. For most projects, the “database” is just a few fields of JSON — and there are much simpler ways to host and update that than spinning up a server.

The approach here — a public JSON file, a simple fetch on the frontend, and a curl command when something breaks — is intentionally minimal. It covers the common case well: your users know what’s up, you know they know, and you can update it from your phone if you have to.

JSONhost.com handles the hosting and write API, so the file is always accessible, always up to date, and never your problem to maintain.

--

--