A unified healthcheck and status toolkit for web applications — Watchtower keeps a vigilant eye on your site's heartbeat, ensuring every critical path and service stays up.
- 🏥 Comprehensive Health Checks - Monitor Algolia search, web pages, HTTP endpoints, and build integrity
- ⚡ High Performance - Parallel execution with configurable timeouts and caching
- 🔒 Security First - Multiple sanitization strategies for production environments
- 🔧 Framework Agnostic - Next.js adapter included, Express support coming soon
- 📦 Zero Runtime Dependencies - Lightweight package with peer dependencies only
- 🎯 TypeScript Ready - Full TypeScript support with comprehensive type definitions
# npm
npm install @last-rev/watchtower
# pnpm
pnpm add @last-rev/watchtower
# yarn
yarn add @last-rev/watchtowerPeer Dependencies:
algoliasearch@^5.0.0(required for Algolia checks)next@^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0(required for Next.js adapter)
Ready-to-use examples are available in the examples/ directory:
- Next.js Pages Router - Complete setup with Contentful and Algolia
- Next.js App Router - Modern Next.js 13+ App Router integration
- Manual Configuration - Custom configuration without templates
- Minimal Setup - Simple configuration with essential checks only
See the examples README for detailed setup instructions.
- Create your healthcheck configuration:
// healthcheck.config.ts
import {
createAlgoliaCheck,
createPagesCheck,
createBuildCheck,
type RunnerConfig
} from '@last-rev/watchtower';
const config: RunnerConfig = {
budgetMs: 20000,
cacheMs: 30000,
auth: {
token: process.env.HEALTHCHECK_TOKEN
// In production, auth is required by default
// Query params disabled by default (use headers for better security)
},
sanitize: process.env.NODE_ENV === 'production' ? 'counts-only' : 'none',
checks: [
createBuildCheck({
criticalEnv: ['CONTENTFUL_SPACE_ID', 'ALGOLIA_ADMIN_API_KEY']
}),
createAlgoliaCheck({
indexName: 'contentful',
apiKey: process.env.ALGOLIA_ADMIN_API_KEY,
useSearchKey: false,
thresholds: {
totalRecords: { critical: 50, warning: 100 },
categories: {
'In the News': {
critical: 5,
warning: 20,
actualField: 'postType',
actualValue: 'In the News'
}
}
}
}),
createPagesCheck({
critical: [{ path: '/', name: 'Homepage' }],
timeout: 5000,
retries: 2
})
]
};
export default config;- Create your API route:
For Pages Router:
// pages/api/healthcheck/[[...slug]].ts (optional catch-all route for test page support)
// OR pages/api/healthcheck.ts (simple route, no test page)
import { createNextHandler } from '@last-rev/watchtower';
import config from '../../../healthcheck.config';
export default createNextHandler(config);For App Router:
// app/api/healthcheck/[[...slug]]/route.ts (optional catch-all route for test page support)
// OR app/api/healthcheck/route.ts (simple route, no test page)
import { createNextHandler } from '@last-rev/watchtower';
import config from '../../../../healthcheck.config';
export const GET = createNextHandler(config);Note: Use an optional catch-all route ([[...slug]] with double brackets) if you want to enable the test page feature. This allows the route to match both /api/healthcheck (base path) and /api/healthcheck/test (test page). A regular catch-all route ([...slug]) won't match the base path. If you don't need the test page, a simple route file works fine.
- Test your healthcheck:
# Development
curl http://localhost:3000/api/healthcheck
# Production (with authentication)
curl -H "Authorization: Bearer YOUR_TOKEN" https://example.com/api/healthcheck
# Browser testing (if enableTestPage is enabled)
# Visit: http://localhost:3000/api/healthcheck/test
# Enter your token in the secure test page interfaceFor quick setup, you can use pre-configured templates:
// healthcheck.config.ts
import { contentfulSiteTemplate } from '@last-rev/watchtower';
export default contentfulSiteTemplate({
algolia: {
indexName: 'contentful',
thresholds: {
totalRecords: { critical: 50, warning: 100 }
}
}
});Then use it in your API route:
// pages/api/healthcheck.ts
import { createNextHandler } from '@last-rev/watchtower';
import config from '../../healthcheck.config';
export default createNextHandler(config);Monitor your Algolia search indices with customizable thresholds.
createAlgoliaCheck({
indexName: 'contentful',
applicationId: process.env.ALGOLIA_APPLICATION_ID,
apiKey: process.env.ALGOLIA_ADMIN_API_KEY,
useSearchKey: false, // Use admin key for comprehensive monitoring
thresholds: {
totalRecords: {
critical: process.env.NODE_ENV === 'production' ? 100 : 50,
warning: process.env.NODE_ENV === 'production' ? 125 : 100
},
categories: {
'In the News': {
critical: 5,
warning: 20,
actualField: 'postType',
actualValue: 'In the News'
},
'Blogs': {
critical: 3,
warning: 10,
actualField: 'postType',
actualValue: 'Blog Post'
},
},
},
skipHeavyFacets: false, // Enable full facet checking for comprehensive monitoring
});Verify critical web pages are accessible and responding correctly.
createPagesCheck({
critical: [
{ path: '/', name: 'Homepage' },
{ path: '/robots.txt', name: 'Robots.txt' },
{ path: '/sitemap.xml', name: 'Sitemap' },
],
important: [
{ path: '/about', name: 'About Page' },
{ path: '/blog', name: 'Blog Index' },
],
timeout: 5000,
retries: 2,
});Test custom HTTP endpoints with full control over requests.
createHttpCheck({
endpoints: [
{
path: '/api/graphql',
name: 'GraphQL API',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: { query: '{ __typename }' },
expectedStatus: 200,
},
],
timeout: 3000,
retries: 1,
});Validate build integrity and environment variables.
createBuildCheck({
criticalEnv: [
'CONTENTFUL_SPACE_ID',
'CONTENTFUL_DELIVERY_TOKEN',
'NODE_ENV',
],
optionalEnv: [
'NEXT_PUBLIC_SITE_URL',
'ALGOLIA_APPLICATION_ID',
],
});Watchtower provides pre-configured templates for common use cases:
Optimized for Contentful-powered websites with Algolia search.
import { contentfulSiteTemplate } from '@last-rev/watchtower';
export default contentfulSiteTemplate({
algolia: {
indexName: 'contentful',
thresholds: {
totalRecords: { critical: 100, warning: 200 },
categories: {
'Blog Posts': {
critical: 5,
warning: 20,
actualField: 'contentType',
actualValue: 'blogPost'
},
},
},
},
// Optional: Don't pass contentful config to avoid API checks
// contentful: { ... }
});Note: The contentfulSiteTemplate includes Build integrity and Algolia checks. To add Contentful API checks, pass the contentful configuration option.
A balanced configuration suitable for most web applications. No configuration needed.
import { defaultTemplate } from '@last-rev/watchtower';
// Returns config with:
// - Pages check (/, /robots.txt, /sitemap.xml, /favicon.ico)
// - Build check (NODE_ENV, NEXT_PUBLIC_SITE_URL)
export default defaultTemplate();Lightweight setup with only essential checks. No configuration needed.
import { minimalTemplate } from '@last-rev/watchtower';
// Returns config with:
// - Build check (package.json, Node version)
export default minimalTemplate();Secure your healthcheck endpoints with token-based authentication. In production, authentication is required by default to prevent information leakage.
const config = {
// ... checks
auth: {
token: process.env.HEALTHCHECK_TOKEN,
// In production, auth is required by default
// Query params disabled by default (use headers for better security)
// allowQueryToken: false, // Enable for browser testing (not recommended for production)
// strictMode: true, // Minimal error responses (default: true in production)
customValidator: (req) => {
// Custom validation logic (takes precedence over token)
return req.headers['x-internal-token'] === process.env.INTERNAL_TOKEN;
},
onAuthFailure: (req, reason) => {
// Optional callback for logging/alerting
console.warn('Healthcheck auth failed:', reason);
},
},
};Token can be provided via:
Authorization: Bearer <token>header (recommended, works with monitoring tools)X-Healthcheck-Token: <token>header- Query parameter
?token=<token>(ifallowQueryToken: trueis enabled)
Security defaults:
- Production: Auth required by default
- Query params: Disabled by default (can appear in logs)
- Strict mode: Enabled in production (minimal error responses)
Enable an interactive browser-based test page for manual healthcheck testing:
const config = {
// ... checks
enableTestPage: true, // Access at /api/healthcheck/test
// Or custom path:
// enableTestPage: '/test-page', // Access at /api/healthcheck/test-page
auth: {
token: process.env.HEALTHCHECK_TOKEN
}
};Features:
- Secure token input (stored in sessionStorage, not localStorage)
- Token sent via
Authorizationheader (never in URL) - Beautiful, responsive UI with formatted JSON output
- Status badges and error handling
- Token must be entered to run healthcheck (no bypass)
Security:
- Test page prompts for token - cannot run checks without it
- Token validated against
HEALTHCHECK_TOKENenvironment variable - Token never appears in URL or browser history
- Token cleared when browser tab closes (sessionStorage)
- Healthcheck endpoint still requires authentication (same token validation)
Control what information is exposed in healthcheck responses:
sanitize: 'none' // Full details exposedsanitize: 'redact-values' // Environment variables and URLs partially maskedsanitize: 'counts-only' // Only show counts, no sensitive detailsConfigure performance constraints to ensure healthchecks don't impact your application:
const config = {
budgetMs: 20000, // Global timeout (8 seconds for comprehensive checks)
cacheMs: 30000, // Cache results for 30 seconds (balance between freshness and performance)
aggregationPrecedence: ['Down', 'Partial', 'Unknown', 'Up'],
};Expensive operations like Algolia facet queries are automatically cached:
const config: RunnerConfig = {
cacheMs: 30000, // Cache results for 30 seconds
checks: [
createAlgoliaCheck({
indexName: 'contentful',
skipHeavyFacets: false // Enable full facet checking (cached)
})
]
};Healthcheck responses follow a standardized format:
{
"id": "site_healthcheck",
"name": "Site Health",
"status": "Up",
"message": "All systems operational",
"timestamp": 1703123456789,
"performance": {
"totalCheckTime": 234,
"checksCompleted": 4,
"checksFailed": 0
},
"services": [
{
"id": "algolia",
"name": "Algolia Search",
"status": "Up",
"message": "Algolia Search: All systems operational",
"timestamp": 1703123456789,
"services": [
{
"id": "record_count",
"name": "Record Count",
"status": "Up",
"message": "1250 total records (234ms)",
"timestamp": 1703123456789,
"metadata": {
"duration": 234,
"totalRecords": 1250,
"productionRecords": 1100,
"previewRecords": 150
}
}
]
}
]
}The API returns appropriate HTTP status codes based on overall health:
200- Up or Partial (service still functional)503- Down or Unknown (service unavailable)
NEXT_PUBLIC_SITE_URL- Base URL for page checks (e.g.,https://example.com)ALGOLIA_APPLICATION_ID- Algolia application IDALGOLIA_ADMIN_API_KEY- Algolia admin API key (recommended for comprehensive monitoring)CONTENTFUL_SPACE_ID- Contentful space IDCONTENTFUL_ENV- Contentful environment (e.g.,master)
HEALTHCHECK_TOKEN- Authentication token for healthcheck endpoint (required in production)NODE_ENV- Environment mode (affects sanitization and thresholds)
ALGOLIA_SEARCH_API_KEY- Algolia search-only API key (alternative to admin key)CONTENTFUL_DELIVERY_TOKEN- Contentful delivery API tokenCONTENTFUL_PREVIEW_TOKEN- Contentful preview API tokenSITE_URL- Alternative site URL configurationDOMAIN- Domain name for URL constructionVERCEL_URL- Vercel deployment URL (auto-detected)DATABASE_URL- Database connection stringREDIS_URL- Redis connection string
Create your own health checks by implementing the Check interface:
import type { Check, StatusNode } from '@last-rev/watchtower';
import { createStatusNode } from '@last-rev/watchtower';
export function createCustomCheck(): Check {
return {
id: 'custom',
name: 'Custom Service',
async run(): Promise<StatusNode> {
try {
// Your custom health check logic
const isHealthy = await checkCustomService();
return createStatusNode(
'custom',
'Custom Service',
isHealthy ? 'Up' : 'Down',
isHealthy ? 'Service operational' : 'Service unavailable'
);
} catch (error) {
return createStatusNode(
'custom',
'Custom Service',
'Unknown',
`Check failed: ${(error as Error).message}`
);
}
},
};
}// pages/api/healthcheck/[[...slug]].ts (optional catch-all for test page support)
// OR pages/api/healthcheck.ts (simple route)
import { createNextHandler } from '@last-rev/watchtower';
export default createNextHandler(config);// app/api/healthcheck/[[...slug]]/route.ts (optional catch-all for test page support)
// OR app/api/healthcheck/route.ts (simple route)
import { createNextHandler } from '@last-rev/watchtower';
export const GET = createNextHandler(config);- Replace your existing healthcheck with Watchtower configuration
- Update environment variables to match Watchtower expectations
- Test thoroughly in development before deploying
- Update monitoring dashboards to handle new response format
| Legacy | Watchtower |
|---|---|
SITE_URL |
NEXT_PUBLIC_SITE_URL |
ALGOLIA_APP_ID |
ALGOLIA_APPLICATION_ID |
ALGOLIA_API_KEY |
ALGOLIA_ADMIN_API_KEY |
Healthcheck returns 503 but site works fine
- Check authentication configuration
- Verify all required environment variables are set
- Review individual service status in response details
Algolia checks failing
- Verify Algolia credentials and permissions
- Check if index exists and is populated
- Review network connectivity to Algolia API
Page checks failing
- Ensure
NEXT_PUBLIC_SITE_URLis correctly set - Check if pages actually exist at specified paths
- Verify site is accessible from healthcheck location
Build failing
- Check TypeScript compilation errors
- Verify all peer dependencies are installed
- Review build configuration in
tsup.config.ts
Enable detailed logging in development:
const config = {
// ... configuration
sanitize: 'none', // Show full details in development
};- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Create new check in
src/checks/ - Add exports to
src/checks/index.ts - Update types in
src/core/types.ts - Add documentation and examples
- Update tests
This project is licensed under the MIT License - see the LICENSE file for details.
For maintainers publishing new versions, see PUBLISHING.md for the complete publishing workflow.
For support and questions:
Made with ❤️ by LastRev