Engineering manualAPI route source
/api/admin
Exact current route handler: hosted-app/app/api/admin/route.js
Methods: GET, POST. Input signals: JSON body, query parameters. Literal response-status signals: 400.
Source: hosted-app/app/api/admin/route.js. Imports: ../../../lib/admin-access.mjs, ../../../lib/admin-api.mjs, ../../../lib/comprehensive-pull.mjs, ../../../lib/supabase-rest.mjs, next/server. Exact source is shown for inspection; its presence does not prove authentication, validation or live behavior.
All API routes · API and execution contract
// GET /api/admin?view=overview|accounts|pulls|providers — the owner's reads.
//
// ONE ROUTE, FOUR VIEWS, ONE GATE. Four separate endpoints would be four
// separate places for the gate to be written, and the fourth one is where it
// gets forgotten. The gate runs BEFORE the view is even looked at, so an
// unknown `view` from a stranger and a known one both answer 404 — the
// parameter cannot be used to probe what this surface can do.
//
// THE PROVIDERS VIEW IS THE ONE THAT LEAVES THE BUILDING. The other three read
// our own database; this one reaches four suppliers with the deployment's own
// credentials. Two consequences, both deliberate:
// 1. It sits behind the SAME gate, unchanged — a stranger who could reach it
// could make us call our providers on demand.
// 2. NO CREDENTIAL CROSSES THE WIRE BACK. The response carries balances,
// statuses, reasons and the last four characters of the key that answered
// — never a key, never the admin list, never a base URL with auth in it.
//
// Everything here is a thin wrapper. What is fetched and how it is folded is
// lib/admin-api.mjs and lib/provider-balances.mjs; who is allowed is
// lib/admin-access.mjs; who our suppliers ARE is lib/local/provider-registry.mjs.
import { NextResponse } from "next/server";
import { supabaseConfigFromEnv } from "../../../lib/supabase-rest.mjs";
import { bearerTokenFromHeader } from "../../../lib/comprehensive-pull.mjs";
import { adminPageFromQuery } from "../../../lib/admin-access.mjs";
import {
resolveAdminContext,
readAdminOverview,
readAdminAccounts,
readAdminPulls,
readAdminProviders,
readAdminPlan,
recordPlanDecision
} from "../../../lib/admin-api.mjs";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(request) {
const config = supabaseConfigFromEnv(process.env);
const token = bearerTokenFromHeader(request.headers.get("authorization"));
const admin = await resolveAdminContext({ token, config, env: process.env });
if (!admin.ok) return NextResponse.json(admin.body, { status: admin.http_status });
const url = new URL(request.url);
const view = url.searchParams.get("view") || "overview";
const page = adminPageFromQuery(url.searchParams.get("page"));
if (view === "accounts") {
const result = await readAdminAccounts({ config, page });
return NextResponse.json({ ...result.body, viewer: admin.email }, { status: result.http_status });
}
if (view === "pulls") {
const result = await readAdminPulls({ config, page });
return NextResponse.json({ ...result.body, viewer: admin.email }, { status: result.http_status });
}
if (view === "providers") {
// `config` added 2026-08-19 for the "spend by model / by use" fold —
// service-role, one small Supabase scan, same posture as every other
// view on this route (see readAdminProviders's own header).
const result = await readAdminProviders({ env: process.env, config });
return NextResponse.json({ ...result.body, viewer: admin.email }, { status: result.http_status });
}
if (view === "plan") {
const result = await readAdminPlan({ config });
return NextResponse.json({ ...result.body, viewer: admin.email }, { status: result.http_status });
}
if (view !== "overview") {
// A view this route does not have, asked for by someone who has already
// proved they are the owner. Named honestly rather than silently served
// the overview, so a typo in a bookmark is visible instead of looking
// like the dashboard has quietly changed.
return NextResponse.json({ status: "refused", reason: "unknown_view", detail: "Use overview, accounts, pulls, providers, or plan." }, { status: 400 });
}
const result = await readAdminOverview({ config });
return NextResponse.json({ ...result.body, viewer: admin.email }, { status: result.http_status });
}
// POST /api/admin?view=plan — the owner's decision on ONE proposal.
//
// SAME GATE, SAME 404. A write is placed here rather than in its own route for
// the reason the reads are: the gate is written once. The refusal for a stranger
// is identical to the read path's — 404, not 403 — so the existence of an
// approval surface is not confirmed to anyone who cannot use it.
//
// THE DECISION IS ALL THAT HAPPENS. No work is scheduled, no flag is flipped, no
// provider is called, and no pull's cost changes. That is the contract the owner
// asked for: see it all, approve it first, and only then start the plan.
//
// The ACTOR is taken from `admin.email` — the address the gate verified against
// Supabase's own /auth/v1/user — and never from the request body, so the audit
// trail records who it was rather than who the client said it was.
export async function POST(request) {
const config = supabaseConfigFromEnv(process.env);
const token = bearerTokenFromHeader(request.headers.get("authorization"));
const admin = await resolveAdminContext({ token, config, env: process.env });
if (!admin.ok) return NextResponse.json(admin.body, { status: admin.http_status });
const url = new URL(request.url);
if ((url.searchParams.get("view") || "") !== "plan") {
return NextResponse.json(
{ status: "refused", reason: "unknown_view", detail: "The only writable view is plan." },
{ status: 400 }
);
}
const payload = await request.json().catch(() => null);
if (!payload || typeof payload !== "object") {
return NextResponse.json({ status: "refused", reason: "invalid_body" }, { status: 400 });
}
const result = await recordPlanDecision({
adminEmail: admin.email,
proposalId: payload.proposal_id,
status: payload.status,
config
});
return NextResponse.json(result.body, { status: result.http_status });
}