A public status page builds trust with users. Here is how to create one with health checks and incident history.
Step 1: Data Types
// types/status.ts
export type ServiceStatus = "operational" | "degraded" | "partial_outage" | "major_outage" | "maintenance";
export interface Service {
id: string;
name: string;
description: string;
status: ServiceStatus;
uptimePercentage: number;
responseTime: number; // ms
lastChecked: string;
}
export interface Incident {
id: string;
title: string;
status: "investigating" | "identified" | "monitoring" | "resolved";
severity: "minor" | "major" | "critical";
createdAt: string;
resolvedAt: string | null;
updates: IncidentUpdate[];
affectedServices: string[];
}
export interface IncidentUpdate {
id: string;
message: string;
status: Incident["status"];
createdAt: string;
}
export interface UptimeDay {
date: string;
status: ServiceStatus;
uptimePercentage: number;
}
Step 2: Status Badge Component
import type { ServiceStatus } from "@/types/status";
const STATUS_CONFIG: Record<ServiceStatus, { label: string; color: string; dot: string }> = {
operational: { label: "Operational", color: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", dot: "bg-green-500" },
degraded: { label: "Degraded Performance", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", dot: "bg-yellow-500" },
partial_outage: { label: "Partial Outage", color: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400", dot: "bg-orange-500" },
major_outage: { label: "Major Outage", color: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", dot: "bg-red-500" },
maintenance: { label: "Maintenance", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", dot: "bg-blue-500" },
};
export function StatusBadge({ status }: { status: ServiceStatus }) {
const config = STATUS_CONFIG[status];
return (
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${config.color}`}>
<span className={`h-1.5 w-1.5 rounded-full ${config.dot}`} />
{config.label}
</span>
);
}
Step 3: Uptime Bar Visualization
import type { UptimeDay, ServiceStatus } from "@/types/status";
const DAY_COLORS: Record<ServiceStatus, string> = {
operational: "bg-green-500",
degraded: "bg-yellow-500",
partial_outage: "bg-orange-500",
major_outage: "bg-red-500",
maintenance: "bg-blue-500",
};
interface UptimeBarProps {
days: UptimeDay[];
label: string;
}
export function UptimeBar({ days, label }: UptimeBarProps) {
const avgUptime =
days.reduce((sum, d) => sum + d.uptimePercentage, 0) / days.length;
return (
<div>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium">{label}</span>
<span className="text-sm text-gray-500">{avgUptime.toFixed(2)}% uptime</span>
</div>
<div className="flex gap-[2px]">
{days.map((day) => (
<div
key={day.date}
className={`h-8 flex-1 rounded-sm ${DAY_COLORS[day.status]} transition-all hover:opacity-80`}
title={`${day.date}: ${day.uptimePercentage}%`}
/>
))}
</div>
<div className="mt-1 flex justify-between text-xs text-gray-400">
<span>{days.length} days ago</span>
<span>Today</span>
</div>
</div>
);
}
Step 4: Service List Component
import type { Service } from "@/types/status";
import { StatusBadge } from "./StatusBadge";
export function ServiceList({ services }: { services: Service[] }) {
const allOperational = services.every((s) => s.status === "operational");
return (
<div className="space-y-4">
{/* Overall status banner */}
<div
className={`rounded-xl p-4 text-center text-sm font-medium ${
allOperational
? "bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400"
: "bg-yellow-50 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400"
}`}
>
{allOperational
? "All systems operational"
: "Some systems are experiencing issues"}
</div>
{/* Service rows */}
<div className="divide-y rounded-xl border dark:border-gray-700 dark:divide-gray-700">
{services.map((service) => (
<div
key={service.id}
className="flex items-center justify-between px-4 py-3"
>
<div>
<p className="text-sm font-medium">{service.name}</p>
<p className="text-xs text-gray-400">{service.description}</p>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400">
{service.responseTime}ms
</span>
<StatusBadge status={service.status} />
</div>
</div>
))}
</div>
</div>
);
}
Step 5: Incident Timeline
import type { Incident } from "@/types/status";
const SEVERITY_COLORS = {
minor: "border-yellow-500",
major: "border-orange-500",
critical: "border-red-500",
};
const STATUS_LABELS = {
investigating: "Investigating",
identified: "Identified",
monitoring: "Monitoring",
resolved: "Resolved",
};
export function IncidentTimeline({ incidents }: { incidents: Incident[] }) {
if (incidents.length === 0) {
return (
<p className="rounded-xl border p-6 text-center text-sm text-gray-400 dark:border-gray-700">
No incidents reported in the last 90 days
</p>
);
}
return (
<div className="space-y-4">
{incidents.map((incident) => (
<div
key={incident.id}
className={`rounded-xl border-l-4 bg-white p-4 shadow-sm dark:bg-gray-900 ${SEVERITY_COLORS[incident.severity]}`}
>
<div className="flex items-start justify-between">
<h3 className="text-sm font-semibold">{incident.title}</h3>
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
incident.status === "resolved"
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
}`}
>
{STATUS_LABELS[incident.status]}
</span>
</div>
<div className="mt-3 space-y-2 border-l-2 border-gray-200 pl-4 dark:border-gray-700">
{incident.updates.map((update) => (
<div key={update.id}>
<p className="text-xs font-medium text-gray-500">
{STATUS_LABELS[update.status]} —{" "}
{new Date(update.createdAt).toLocaleString()}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{update.message}
</p>
</div>
))}
</div>
</div>
))}
</div>
);
}
Step 6: Status Page Route
// app/status/page.tsx
import { ServiceList } from "@/components/status/ServiceList";
import { UptimeBar } from "@/components/status/UptimeBar";
import { IncidentTimeline } from "@/components/status/IncidentTimeline";
async function getStatusData() {
// Fetch from your monitoring API
const res = await fetch(`${process.env.API_URL}/status`, {
next: { revalidate: 60 },
});
return res.json();
}
export default async function StatusPage() {
const { services, uptimeHistory, incidents } = await getStatusData();
return (
<div className="mx-auto max-w-3xl px-4 py-12">
<h1 className="mb-8 text-2xl font-bold">System Status</h1>
<ServiceList services={services} />
<section className="mt-12 space-y-6">
<h2 className="text-lg font-semibold">Uptime (90 days)</h2>
{Object.entries(uptimeHistory).map(([serviceId, days]) => {
const service = services.find((s: any) => s.id === serviceId);
return (
<UptimeBar
key={serviceId}
days={days as any}
label={service?.name ?? serviceId}
/>
);
})}
</section>
<section className="mt-12">
<h2 className="mb-4 text-lg font-semibold">Recent Incidents</h2>
<IncidentTimeline incidents={incidents} />
</section>
</div>
);
}
Step 7: Health Check API
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const checks = await Promise.allSettled([
checkDatabase(),
checkRedis(),
checkExternalAPI(),
]);
const results = {
database: checks[0].status === "fulfilled" ? "healthy" : "unhealthy",
redis: checks[1].status === "fulfilled" ? "healthy" : "unhealthy",
api: checks[2].status === "fulfilled" ? "healthy" : "unhealthy",
};
const allHealthy = Object.values(results).every((v) => v === "healthy");
return NextResponse.json(
{ status: allHealthy ? "healthy" : "degraded", checks: results },
{ status: allHealthy ? 200 : 503 }
);
}
async function checkDatabase() {
// Run a simple query
const start = Date.now();
// await db.execute(sql`SELECT 1`);
return { responseTime: Date.now() - start };
}
async function checkRedis() {
const start = Date.now();
// await redis.ping();
return { responseTime: Date.now() - start };
}
async function checkExternalAPI() {
const start = Date.now();
const res = await fetch("https://api.example.com/health");
if (!res.ok) throw new Error("API unhealthy");
return { responseTime: Date.now() - start };
}
Summary
- Visual uptime bars show 90-day history at a glance
- Service-level status with response times
- Incident timeline with update history
- Health check API for automated monitoring
- Revalidate every 60 seconds for near-real-time data
Need Monitoring Solutions?
We build custom monitoring and status page systems for SaaS products. Contact us to discuss your project.