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.
Display name for your status page
URL-friendly identifier (e.g., “production-status”)
Array of website IDs to display on the status page
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:
All monitor IDs exist in the database
All monitors belong to the authenticated user
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