Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

How to Build a SaaS Status Page With Next.js -- Communicate Downtime and Keep Customer Trust

August 4, 2026
nextjsdrizzle-ormsaasresendboilerplate

The moment your SaaS goes down, every customer who notices does the same thing: they open a new tab, try to find a status page, and when there is not one, they file a support ticket that says "is it just me?"

Multiply that by every affected user and you end up managing customer anxiety at the same time you are trying to fix the actual problem. A status page does not prevent outages. It prevents the chaos around them.

What a Status Page Actually Needs

Before you reach for Statuspage.io ($99/month) or Instatus ($20/month), consider what a status page really does: it shows users what is broken, when it started, and when it will be fixed. That is three database rows and a public Next.js page.

The four things your status page needs to handle:

  • A list of your services (API, Dashboard, Payments, Email)
  • The current health of each service (operational, degraded, outage)
  • Incident history with timeline updates
  • Optional email alerts so subscribers hear from you before they notice the problem

All of that fits in your existing Next.js SaaS boilerplate without a third-party subscription.

The Data Model

You need two tables: one for services and one for incidents.

-- services: API, Dashboard, Payments, etc.
CREATE TABLE services (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'operational', -- operational | degraded | outage
  display_order INTEGER NOT NULL DEFAULT 0
);
 
-- incidents: one per event, with timeline updates stored as JSONB
CREATE TABLE incidents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  service_id UUID REFERENCES services(id),
  title TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'investigating', -- investigating | identified | monitoring | resolved
  updates JSONB NOT NULL DEFAULT '[]', -- [{message, createdAt}]
  resolved_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Storing timeline updates as JSONB on the incident row avoids a third table while keeping the append pattern simple: each update is pushed to the array via a Drizzle sql expression. This is the same approach used in the onboarding checklist pattern -- track structured state per record without a join table.

The Public Status Page

The status page is a Server Component that fetches services and open incidents in parallel:

// app/(public)/status/page.tsx
export default async function StatusPage() {
  const [services, incidents] = await Promise.all([
    serviceRepo.findAll(),
    incidentRepo.findOpen(),
  ]);
 
  return (
    <main>
      <ServiceGrid services={services} />
      {incidents.length > 0 && <IncidentList incidents={incidents} />}
    </main>
  );
}

The ServiceGrid renders a colored badge per service: green for operational, yellow for degraded, red for outage. The IncidentList shows open incidents with their timeline updates newest-first.

Because it is a server component, the page always reflects the current database state. No caching layer to invalidate, no polling on the client.

The Incident Management Flow

You need an internal admin route (behind your existing JWT auth) to create and update incidents. The flow is:

  1. Something breaks -- you create an incident, choosing the affected service and initial message
  2. Your service layer sets the service status to match the incident severity
  3. You add timeline updates as you investigate ("we have identified the cause")
  4. You mark it resolved -- the service resets to "operational" and resolved_at is stamped

The admin form lives at /admin/incidents behind your existing role check. The pattern is the same as any other protected admin route -- validate the request with getUserFromRequest(), check for the admin role, call the service.

Email Alerts to Subscribers

If you want to notify customers proactively, add a status_subscribers table with an email column and a confirmation token. When an incident is created or updated, your service calls emailService.sendEmail() to each confirmed subscriber.

The Resend transactional email setup you already have handles this without any new infrastructure -- the same emailService.sendEmail() call used for password resets works here too.

A three-line email works better than a long one:

  • What is affected
  • Current status and what you know so far
  • Where they can follow updates (link to the status page)

Customers who get that email before filing a ticket trust you more, not less. The outage is the same -- your communication is the variable.

Build It or Buy It -- A Quick Framework

Build the status page yourself if:

  • You want it on your own domain (trust signal)
  • You already have Resend for email alerts
  • You have fewer than 10 services to track
  • You do not need external uptime monitoring (ping checks, SLA reports)

Pay for an external tool if:

  • Your infrastructure goes fully down and takes your app with it -- an external tool stays up when your servers do not
  • You need automated detection rather than manual incident creation
  • Your customers have SLAs that require third-party verification

Most early-stage SaaS products benefit from owning the status page. It is not your ops dashboard -- it is a customer-facing trust signal, and owning the domain matters.

Get Started

The schema above drops cleanly into the boilerplate's DDD-lite module structure: two schema files, one relations file each, a repo and service layer. Run npm run db:generate && npm run db:migrate, wire the public /status route and the admin form, and you have a working status page in a few hours.

If you want auth, payments, email, and the database foundation already in place, the Next.js SaaS boilerplate has it ready. Build the status page on top and ship it this weekend.