Setup documentation

Docs

Everything you need to connect a project to Status, expose a working /api/health endpoint, and surface the results on your own site.

Code examples

Reference implementations in the most common stacks. Each one exposes GET /api/health returning the full contract. Adapt to your routes, naming, and what you want to monitor.

Pattern: run your real sub-checks in parallel, time each one, never throw — always return a result per component so the poller has something to record.

Next.js (App Router)

// app/api/health/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

export const dynamic = 'force-dynamic'
export const revalidate = 0

async function check(label: string, fn: () => Promise<unknown>) {
  const start = Date.now()
  try {
    await fn()
    return { id: label, label, status: 'ok' as const, latencyMs: Date.now() - start }
  } catch (e) {
    return {
      id: label,
      label,
      status: 'down' as const,
      latencyMs: Date.now() - start,
      detail: { error: (e as Error).message },
    }
  }
}

export async function GET() {
  const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)
  const results = await Promise.all([
    check('site', async () => { return true }),
    check('db', async () => { const { error } = await supabase.from('products').select('id').limit(1); if (error) throw error }),
    check('storage', async () => { const { error } = await supabase.storage.from('product-images').list('', { limit: 1 }); if (error) throw error }),
  ])
  return NextResponse.json({ status: 'ok', results })
}

Express

// routes/health.js
import express from 'express'
import mongoose from 'mongoose'
import Redis from 'ioredis

const router = express.Router()
const redis = new Redis(process.env.REDIS_URL)

async function time(label, fn) {
  const start = Date.now()
  try {
    await fn()
    return { id: label, label, status: 'ok', latencyMs: Date.now() - start }
  } catch (e) {
    return { id: label, label, status: 'down', latencyMs: Date.now() - start, detail: { error: String(e) } }
  }
}

router.get('/api/health', async (_req, res) => {
  const [site, db, cache] = await Promise.all([
    time('site', async () => true),
    time('db', () => mongoose.connection.db.admin().ping()),
    time('cache', () => redis.ping()),
  ])
  res.json({ status: 'ok', results: [site, db, cache] })
})

export default router

Laravel

// routes/web.php or routes/api.php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Route;

Route::get('/api/health', function () {
    $results = [];
    $results[] = check('site', fn() => true);
    $results[] = check('db', fn() => DB::select('select 1'));
    $results[] = check('cache', fn() => Redis::ping());
    return response()->json([
        'status' => 'ok',
        'results' => $results,
    ]);
});

function check(string $id, callable $fn): array {
    $start = microtime(true);
    try {
        $fn();
        return ['id' => $id, 'label' => ucfirst($id), 'status' => 'ok', 'latencyMs' => (int) ((microtime(true) - $start) * 1000)];
    } catch (\Throwable $e) {
        return ['id' => $id, 'label' => ucfirst($id), 'status' => 'down', 'latencyMs' => (int) ((microtime(true) - $start) * 1000), 'detail' => ['error' => $e->getMessage()]];
    }
}

Static JSON (simple "site is up" check)

If you only need the page to be reachable, a static JSON file at /api/health works. It returns a single site component and cannot tell the poller about deeper issues, but it’s better than nothing.

// health.json — a static file. Useful only as a "site is up" signal.
{
  "status": "ok",
  "results": [
    { "id": "site", "label": "Marketing site", "status": "ok", "latencyMs": 0 }
  ]
}

Latency measurement tips

  • Use Date.now() for ms-precision timing — the schema stores integers.
  • Don’t include network round-trip time in latencyMs; the poller measures that separately.
  • Run sub-checks in parallel; total time is the slowest component, not the sum.
  • Treat retries as degraded rather than ok — the poller can spot the regression and your team can fix it.

Testing locally

From your terminal, with the dev server running:

curl -s http://localhost:3000/api/health | jq
# expect { "status": "ok", "results": [ { "id": "site", ... }, ... ] }

Force a degraded/down response to confirm the contract — change one result to degraded and call again, or stop the database connection and expect that component to flip to down.