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.
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:
All of that fits in your existing Next.js SaaS boilerplate without a third-party subscription.
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 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.
You need an internal admin route (behind your existing JWT auth) to create and update incidents. The flow is:
resolved_at is stampedThe 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.
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:
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 the status page yourself if:
Pay for an external tool if:
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.
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.