Brand Studio docs
Engineering manualAPI route source

/api/admin/topup

Exact current route handler: hosted-app/app/api/admin/topup/route.js

Methods: POST. Input signals: JSON body. Literal response-status signals: none detected.

Source: hosted-app/app/api/admin/topup/route.js. Imports: ../../../../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

// POST /api/admin/topup — "+50 credits", by the owner, to a named account.
//
// THE ONE WRITE THE DASHBOARD HAS. It grants credits nobody paid for, to an
// account that is not the caller's, so it carries the same gate as the reads
// and one thing they do not: an idempotency key, minted by the confirm dialog
// and enforced by a unique index. A disabled button stops the second click on
// this machine; the key stops the second ROW, including the one a browser's
// own retry would cause after the first request already landed.
//
// The row it writes names the human: `actor = "admin:<email>"`, where the
// email is the one Supabase attached to the verified token — never one the
// client sent. See lib/credits.mjs's actors block.

import { NextResponse } from "next/server";
import { supabaseConfigFromEnv } from "../../../../lib/supabase-rest.mjs";
import { bearerTokenFromHeader } from "../../../../lib/comprehensive-pull.mjs";
import { resolveAdminContext, applyAdminTopup } from "../../../../lib/admin-api.mjs";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

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 });
  // Same 404 as the reads. A non-admin who POSTs here learns nothing about
  // whether the endpoint exists.
  if (!admin.ok) return NextResponse.json(admin.body, { status: admin.http_status });

  const body = await request.json().catch(() => ({}));
  const result = await applyAdminTopup({
    // The verified email, not body.actor. A route that let the client name the
    // actor would have an audit trail that says whatever the caller likes.
    adminEmail: admin.email,
    userId: String(body?.user_id || "").trim(),
    amount: Number(body?.amount ?? 50),
    requestKey: String(body?.request_key || "").trim(),
    config
  });
  return NextResponse.json(result.body, { status: result.http_status });
}