From e2e3745f0effe869cb26cc26240108b4b20defd1 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 6 Oct 2026 18:39:45 +0000 Subject: [PATCH] feat: GET /api/health with a database check Returns 200 {"status":"ok","db":"ok"} after one PostgREST HEAD query (3s timeout) through the shared service-role client, or 503 {"status":"error","db":"down"} without error details. For the status.profullstack.com Gatus check; the middleware lets it past the per-IP API rate limiter. Co-Authored-By: Claude Opus 5.5 --- src/app/api/health/route.js | 24 ++++++++++++++++++++++++ src/middleware.js | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 src/app/api/health/route.js diff --git a/src/app/api/health/route.js b/src/app/api/health/route.js new file mode 100644 index 00000000..9610fd5c --- /dev/null +++ b/src/app/api/health/route.js @@ -0,0 +1,24 @@ +import { NextResponse } from 'next/server'; +import { getServiceRoleClient } from '@/lib/supabase/service-role.js'; + +export const dynamic = 'force-dynamic'; + +const headers = { 'Cache-Control': 'no-store' }; + +/** + * Health check for status.profullstack.com: one cheap PostgREST HEAD query, + * capped at 3s. Never echoes the error, so nothing about the setup leaks. + */ +export async function GET() { + try { + const { error } = await getServiceRoleClient() + .from('users') + .select('id', { head: true }) + .limit(1) + .abortSignal(AbortSignal.timeout(3000)); + if (error) throw error; + return NextResponse.json({ status: 'ok', db: 'ok' }, { headers }); + } catch { + return NextResponse.json({ status: 'error', db: 'down' }, { status: 503, headers }); + } +} diff --git a/src/middleware.js b/src/middleware.js index 7666693d..7efc23a1 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -23,6 +23,9 @@ export function middleware(request) { return NextResponse.redirect(url, 301); } + // Health checks (status.profullstack.com) are never rate limited. + if (pathname === '/api/health') return NextResponse.next(); + const ip = getClientIp(request); // Rate limiting