Build a Statuspage alternative

Build a Statuspage alternative as a public page, component states, and incident posts. Host it next to the product and skip the $29–$399 plan.

  • getting-started
  • vibe-coding
  • tonight

You can build a Statuspage alternative as a public board you host. Statuspage is a public board. The catalog line is “Green dots on good days. Honest updates on bad days.”

You pay $29–$399 a month for it. That is $348–$4,788 a year. The basic replacement is 3 hours: a public page, component states, and incident posts.

I would ship this if I still rented a status board. It is marked Tonight. The verdict on the card is “A perfect tiny app”.

Yesterday I wrote about how Linear works. That was a teardown. Today we are back on a regular backlog topic, a board you host next to the product it describes.

What you need to build a Statuspage alternative

The Statuspage page splits the work into three rows.

The first row is the one you want tonight. A public page, named components with a current state, and incidents you post by hand.

The second row is 8 hours. Maintenance windows, 90-day history, email and RSS subscriptions. Those can wait until someone has subscribed for real.

The third row is 2 weeks and up. SMS at scale, automation hooks, multi-tenant hosting. The table labels that row as the part you should skip.

If you tell an agent to “clone Statuspage”, it aims at the third row. Use the prompt on the page. It is written for a useful v1, and you still have to keep it on the first row.

Here is the shape of the data you are building. Nothing else has to exist at 3 hours.

const page = {
  domain: 'status.flaviocopes.com',
  title: 'flaviocopes.com status',
  timezone: 'Europe/Rome',
  components: [
    { id: 'api', name: 'API', state: 'operational' },
    { id: 'dashboard', name: 'Dashboard', state: 'operational' },
    { id: 'webhooks', name: 'Webhooks', state: 'operational' },
  ],
}

A visitor hits https://status.flaviocopes.com and sees those three rows. When something is wrong, you add an incident and the page explains it.

Who a Statuspage alternative is for

The audience line on the page is specific. A SaaS company communicating service health publicly.

The goal is to publish component status and a clear incident timeline on a branded page. Tonight the done state is one live public URL and one incident you can open and close by hand.

If you need SMS at scale, or a private page for enterprise customers, keep paying. That is the honest rebuild. The site puts it at 2 weeks on purpose.

The same v1 also replaces Instatus. One focused build covers whichever of those you currently pay for.

A ping that emails you when a URL dies is a different job. That is UptimeRobot, $24–$69 a month, about 6 hours. The Statuspage non-goal is explicit: do not build automated monitoring. You write the incident.

Read the non-goals before you copy anything

Open the guide. Read the audience and the non-goals before you touch the prompt.

The non-goal is the useful sentence: do not build automated monitoring, SMS, private pages, or multi-tenant reseller features.

The guardrail sits next to it. Keep an append-only incident history. Show all times with an explicit timezone, so last Tuesday cannot be quietly rewritten.

Then pick the stack you already write. Every prompt ships in four flavors: AHA (Astro, HTMX, Alpine.js), Next.js, Laravel, and Rails. The AHA version for this product uses Astro, D1, and Workers. This site is an Astro app on Cloudflare, so that is the path I default to. The scope matters more than the framework.

Copy the prompt. Before you run it, add one extra line at the top: ship the 3-hour row only. Public page, component states, incident posts. Stop.

Walk the first status-page journey

You create three components in the order customers care about: API, dashboard, webhooks. You add a logo and a support link, preview the public page, and publish it all-operational.

Later the API gets slow. You create an incident, mark the API as degraded, and post an investigating update. Twenty minutes later you post identified. You resolve it with a short summary of what you changed.

A reader with the URL sees the yellow state, the timeline, and the time in a labeled timezone.

The screens the prompt asks for are an admin overview, an incident editor, a maintenance editor, and the public page. For 3 hours you need the first two plus the public page. Maintenance waits. The overview is overall state, ordered components, active incidents, and a create-incident action. The editor is title, affected components, phase, the update body, and the append-only timeline so far. The public page shows the current summary, component states, and active timelines. 90-day history and the subscribe form belong to the 8-hour row.

The data model on the page is Component, Incident, IncidentUpdate, Maintenance, Subscriber. Tonight you need the first three.

One more rule from the prompt, and it is a good one. Publish an update and change affected component states in one transaction, so a refresh mid-publish never shows a red component with no incident, or an incident with stale green dots.

Code the append-only publish yourself

Agents will give you an incident form with an Edit button on every old update. Drop that button. A published outage cannot be softened after the fact.

The mini-prompt on the page is “The incident log you can’t quietly edit”. An incident is a sequence of timestamped updates: investigating, identified, monitoring, resolved. A correction is a new update that says what was wrong. No path mutates published text.

Order by server timestamp. Show the newest update first. Label every time with a timezone, 18:04 Europe/Rome, not a bare clock.

async function publishUpdate(db, incidentId, { phase, body, components }) {
  const update = {
    incidentId,
    phase,
    body,
    publishedAt: new Date().toISOString(),
  }

  await db.transaction(async (tx) => {
    await tx.insertUpdate(update)
    await tx.setIncidentPhase(incidentId, phase)
    await tx.recomputeComponentStates(components)
  })

  await purgePublicCache('status.flaviocopes.com')
  return update
}

await publishUpdate(db, 'inc-41', {
  phase: 'identified',
  body: 'API latency is up in eu-west. Dashboard is fine.',
  components: [{ id: 'api', severity: 'degraded' }],
})

Cache purges fire after the commit. If you purge first and the write fails, the public page can flash the new state and then bounce back.

A failed save should leave the draft body in the editor. Do not wipe twenty minutes of writing because you forgot to pick a component.

Two incidents can share a component

The other failure agents miss is treating component state as a field you type.

The mini-prompt is “Two outages, one component, honest math”. Each incident declares affected components with a severity: degraded, partial outage, or major outage. The displayed state is derived. It is the worst severity across every active incident that touches that component.

Two incidents both hit the API. One is a major outage. One is degraded. You resolve the major one. The API must drop to degraded, not to operational, because something is still wrong. Only when zero active incidents touch a component does it return to operational.

const SEVERITY = {
  operational: 0,
  degraded: 1,
  'partial outage': 2,
  'major outage': 3,
}

function worstState(activeIncidents, componentId) {
  let worst = 'operational'
  for (const incident of activeIncidents) {
    const hit = incident.components.find((item) => item.id === componentId)
    if (!hit) continue
    if (SEVERITY[hit.severity] > SEVERITY[worst]) {
      worst = hit.severity
    }
  }
  return worst
}

const active = [
  {
    id: 'inc-41',
    components: [{ id: 'api', severity: 'major outage' }],
  },
  {
    id: 'inc-42',
    components: [{ id: 'api', severity: 'degraded' }],
  },
]

worstState(active, 'api')
//major outage

worstState(active.filter((item) => item.id !== 'inc-41'), 'api')
//degraded

Recompute on create, on update, and on resolve. Do not let the admin hand-edit the live state of a component that an incident still owns.

What not to build in a Statuspage clone

Leave out automated monitoring, SMS, a private customer page, and anything that looks like multi-tenant reseller hosting.

Scheduled maintenance is on the feature list. Skip it until you have posted a real incident by hand. Email and RSS wait until people already open the page.

If the agent starts a probe network or an on-call rotation, stop it. Paste the non-goal again. UptimeRobot is the card for pings.

The 8-hour row, after the page is public

Use the status page as the real URL for a week. Then look at the second estimate.

Maintenance windows with a start and an end. The mini-prompt is “Maintenance windows that survive the clock change”. Store the local times the admin typed, the named IANA timezone they typed them in (Europe/Rome, not a plus-two offset), and the computed UTC instants. End must be after start in absolute terms. A local time that never happens on the spring-forward night should fail with a clear error.

const window = {
  startLocal: '2026-10-18T02:00',
  endLocal: '2026-10-18T04:00',
  timeZone: 'Europe/Rome',
  startUtc: '2026-10-18T00:00:00.000Z',
  endUtc: '2026-10-18T02:00:00.000Z',
}

Show the window in that zone on the public page, and offer the reader’s local time next to it. Reminder and start emails are idempotent jobs keyed on (maintenance, notification type), so a reschedule cannot double-send.

90-day daily history. One bar per day, colored by the worst state that day, using the same math as the live view.

Email and RSS subscriptions. The mini-prompt is “Status alerts nobody gets twice”. Double opt-in. A signed unsubscribe link that works without a login. One email per subscriber per update, even if the admin double-clicks publish. RSS entries get a stable GUID per update.

That is the solid v1. The catalog says 8 hours. Stop there.

The week-long row is SMS at scale, automation hooks, and multi-tenant hosting. You do not need that.

Host it next to the product, then cancel

The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.

Put the board on a subdomain you already own, status.flaviocopes.com. Link it from the footer of the product it describes, and from the support page. Then replace the old Statuspage URL in emails you still send by hand.

Use it for a week as the real tool, not as a demo. If you miss a maintenance window or an RSS feed, add them from the 8-hour row.

Then cancel Statuspage, or Instatus, whichever one you pay for.

A short URL is a different job. That is Bitly, $35–$300 a month, about 90 minutes. You already have that post if redirects are the rent.

If you want another Tonight card after this one, the audit is still the index. TinyPNG Pro is about 60 minutes and stays in the browser.

Start now

Go to Build your own Statuspage. Copy the AHA prompt, or the stack you already write.

Tell the agent the first estimate only: public page, component states, incident posts. Three hours.

Publish the page. Point the product footer at it tonight.