Skip to main content

Overview

Status pages allow you to share your uptime monitoring data publicly with your users. They display real-time status and historical uptime data for your selected monitors.
Status pages can be published on custom domains or accessed via their slug URL.

Creating Status Pages

Create a New Status Page

Create a status page with selected monitors:
const statusPage = await trpc.statusPage.create.mutate({
  name: "Production Services",
  slug: "production-status",
  monitorIds: ["website-1", "website-2", "website-3"],
  isPublished: true,
});
The creation process:
packages/api/src/routes/status-page.ts
create: protectedProcedure
  .input(createStatusPageInput)
  .output(statusPageOutput)
  .mutation(async ({ ctx, input }) => {
    const userId = ctx.user.userId;
    const { name, slug, monitorIds, isPublished } = input;

    // Validate monitors belong to user
    const websites = await prismaClient.website.findMany({
      where: {
        id: {
          in: monitorIds,
        },
        userId,
        isActive: true,
      },
      select: {
        id: true,
      },
    });

    if (websites.length !== monitorIds.length) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: "One or more selected monitors are invalid",
      });
    }

    const statusPage = await prismaClient.statusPage.create({
      data: {
        name,
        slug,
        userId,
        isPublished: isPublished ?? true,
        monitors: {
          createMany: {
            data: monitorIds.map((websiteId) => ({
              websiteId,
            })),
          },
        },
      },
      include: {
        monitors: {
          select: { websiteId: true },
        },
        domain: true,
      },
    });

    return statusPage;
  }),
The slug must be unique across all status pages. Duplicate slugs will return a CONFLICT error.

Input Parameters

name
string
required
Display name for your status page
slug
string
required
URL-friendly identifier (e.g., “production-status”)
monitorIds
string[]
required
Array of website IDs to display on the status page
isPublished
boolean
default:"true"
Whether the status page is publicly accessible

Managing Status Pages

List Status Pages

Get all status pages for the current user:
const { statusPages } = await trpc.statusPage.list.query();

statusPages.forEach((page) => {
  console.log(`${page.name}: ${page.monitorCount} monitors`);
});
Implementation:
packages/api/src/routes/status-page.ts
list: protectedProcedure
  .output(statusPageListOutput)
  .query(async ({ ctx }) => {
    const statusPages = await prismaClient.statusPage.findMany({
      where: {
        userId: ctx.user.userId,
      },
      include: {
        monitors: {
          select: { websiteId: true },
        },
        domain: true,
      },
      orderBy: {
        createdAt: "desc",
      },
    });

    return {
      statusPages: statusPages.map(toStatusPageOutput),
    };
  }),

Update Status Page

Update status page properties and monitors:
const updated = await trpc.statusPage.update.mutate({
  id: "status-page-id",
  name: "Updated Name",
  monitorIds: ["website-1", "website-4"], // Replace monitors
  isPublished: false, // Unpublish
});
The update transaction:
packages/api/src/routes/status-page.ts
const updated = await prismaClient.$transaction(async (tx) => {
  const updatedStatusPage = await tx.statusPage.update({
    where: { id },
    data: updates,
  });

  if (monitorIds) {
    // Replace all monitors atomically
    await tx.statusPageMonitor.deleteMany({
      where: { statusPageId: id },
    });
    await tx.statusPageMonitor.createMany({
      data: monitorIds.map((websiteId) => ({
        statusPageId: id,
        websiteId,
      })),
    });
  }

  return tx.statusPage.findUniqueOrThrow({
    where: { id: updatedStatusPage.id },
    include: {
      monitors: { select: { websiteId: true } },
      domain: true,
    },
  });
});
Monitor updates are atomic - all monitors are replaced in a single transaction.

Delete Status Page

Permanently delete a status page:
await trpc.statusPage.delete.mutate({
  id: "status-page-id",
});

Adding Monitors to Status Pages

Monitors are linked to status pages through the StatusPageMonitor relationship.

Monitor Validation

Before adding monitors, Better Uptime validates:
  1. All monitor IDs exist in the database
  2. All monitors belong to the authenticated user
  3. All monitors are active (isActive: true)
packages/api/src/routes/status-page.ts
if (monitorIds) {
  const websites = await prismaClient.website.findMany({
    where: {
      id: { in: monitorIds },
      userId,
      isActive: true,
    },
    select: {
      id: true,
    },
  });

  if (websites.length !== monitorIds.length) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "One or more selected monitors are invalid",
    });
  }
}

Monitor Status Aggregation

Status pages fetch and aggregate monitor data from ClickHouse:
packages/api/src/routes/status-page.ts
function mapWebsiteStatuses(
  websites: Array<{
    id: string;
    name: string | null;
    url: string;
  }>,
  statusEvents: WebsiteStatusEvent[],
): WebsiteStatusSummary[] {
  const statusByWebsite = new Map();

  for (const event of statusEvents) {
    if (!statusByWebsite.has(event.website_id)) {
      statusByWebsite.set(event.website_id, {
        statusPoints: [],
        currentStatus: {
          status: event.status,
          checkedAt: new Date(event.checked_at),
          responseTimeMs: event.response_time_ms,
          httpStatusCode: event.http_status_code,
          regionId: event.region_id,
        },
      });
    }

    statusByWebsite.get(event.website_id)!.statusPoints.push({
      status: event.status,
      checkedAt: new Date(event.checked_at),
      responseTimeMs: event.response_time_ms,
      httpStatusCode: event.http_status_code,
    });
  }

  return websites.map((website) => {
    const statusData = statusByWebsite.get(website.id);
    return {
      websiteId: website.id,
      websiteName: website.name,
      websiteUrl: website.url,
      statusPoints: statusData?.statusPoints || [],
      currentStatus: statusData?.currentStatus || null,
    };
  });
}

Public vs Private Pages

Published Status Pages

Published status pages are publicly accessible without authentication. Access Requirements:
  • Status page must have isPublished: true
  • Associated custom domain must be verified (if using custom domain)
  • At least one active monitor must be attached

Unpublished Status Pages

Unpublished status pages (isPublished: false) are only visible to the owner and not accessible publicly.
const statusPage = await trpc.statusPage.update.mutate({
  id: "status-page-id",
  isPublished: false, // Make private
});

Public Status Page API

Retrieve a public status page by its custom domain:
const statusPage = await trpc.statusPage.publicByHost.query({
  hostname: "status.example.com",
  viewMode: "per-check",
});
The public endpoint implementation:
packages/api/src/routes/status-page.ts
publicByHost: publicProcedure
  .input(publicStatusPageByHostInput)
  .output(statusPagePublicOutput)
  .query(async ({ input }) => {
    const { hostname, viewMode } = input;

    // Find verified domain with published status page
    const domain = await prismaClient.statusPageDomain.findFirst({
      where: {
        hostname,
        verificationStatus: "VERIFIED",
        statusPage: {
          isPublished: true,
        },
      },
      include: {
        statusPage: {
          include: {
            monitors: {
              include: {
                website: true,
              },
            },
          },
        },
      },
    });

    if (!domain) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Status page not found for this hostname",
      });
    }

    // Filter active websites only
    const activeWebsites = domain.statusPage.monitors
      .map((monitor) => monitor.website)
      .filter((website) => website.isActive);

    // Fetch status events from ClickHouse
    const websiteIds = activeWebsites.map((website) => website.id);
    const statusEvents = await getStatusEventsByWebsiteIds(
      websiteIds,
      viewMode,
    );
    
    // Map to response format
    const websites = mapWebsiteStatuses(
      activeWebsites,
      statusEvents,
    );

    return {
      statusPage: {
        id: domain.statusPage.id,
        name: domain.statusPage.name,
        slug: domain.statusPage.slug,
        hostname: domain.hostname,
        websites,
      },
    };
  }),
The public endpoint only returns data for domains with VERIFIED status and published status pages.

Status Data Views

Status pages support two view modes:

Per-Check View

Shows the most recent N individual checks (default: 90):
const statusEvents = await getRecentStatusEvents(
  websiteIds,
  STATUS_EVENT_QUERY_CONFIG.PER_CHECK_LIMIT, // 90
);

Per-Day View

Shows aggregated daily data for longer time periods (default: 31 days):
const statusEvents = await getStatusEventsForLookbackHours(
  websiteIds,
  STATUS_EVENT_QUERY_CONFIG.PER_DAY_LOOKBACK_DAYS * 24, // 31 days
);

Status Page Output

The status page output includes:
interface StatusPageOutput {
  id: string;
  name: string;
  slug: string;
  isPublished: boolean;
  userId: string;
  monitorCount: number;
  domain: {
    id: string;
    hostname: string;
    verificationStatus: "PENDING" | "VERIFIED" | "FAILED";
    verifiedAt: Date | null;
  } | null;
  createdAt: Date;
  updatedAt: Date;
}

Best Practices

Use descriptive, URL-friendly slugs like:
  • production-status
  • api-status
  • platform-health
Avoid special characters and spaces.
Group related monitors on the same status page:
  • Separate internal and external services
  • Create region-specific status pages
  • Group by customer tier (e.g., enterprise-status)
Keep status pages unpublished during testing:
isPublished: false // Test before making public

Custom Domains

Configure custom domains for status pages

Uptime Monitoring

Learn about the monitoring system

Build docs developers (and LLMs) love