diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 00000000..cf2eb13e --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { createServiceClient } from "@/lib/supabase/service"; + +export const dynamic = "force-dynamic"; + +const HEADERS = { "Cache-Control": "no-store" }; + +/** + * GET /api/health + * Public liveness + database check for status.profullstack.com. One cheap + * HEAD select against profiles, capped at 3s. Never returns error details. + */ +export async function GET() { + try { + const { error } = await createServiceClient() + .from("profiles") + .select("id", { head: true }) + .limit(1) + .abortSignal(AbortSignal.timeout(3000)); + if (error) throw error; + return NextResponse.json({ status: "ok", db: "ok" }, { headers: HEADERS }); + } catch { + return NextResponse.json( + { status: "error", db: "down" }, + { status: 503, headers: HEADERS }, + ); + } +} diff --git a/src/proxy.ts b/src/proxy.ts index 6af2af55..826edb13 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -107,6 +107,10 @@ function getClientIp(request: NextRequest): string { } export async function proxy(request: NextRequest) { + // The status page's health check skips the gate, the meter and the session + // refresh: it must measure the app, not be billed or throttled by it. + if (request.nextUrl.pathname === "/api/health") return NextResponse.next(); + // Crawl gateway first: AI training crawlers get 402 Payment Required (or // the sales page at /crawl) unless they present a paid pass. People, // Googlebot and retrieval crawlers fall through to everything below.