Skip to main content

Overview

The Admin API provides authenticated endpoints for managing and analyzing event registrations. All endpoints require authentication with an @nj.sgadi.us domain email address.
All admin endpoints verify user authentication and domain. Requests without proper credentials will return 401 (Unauthorized) or 403 (Forbidden) responses.

Authentication

Every admin endpoint follows this authentication pattern:
import { createClient } from "@/utils/supabase/server"
import { isAdminDomainUser } from "@/lib/admin-auth"

const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
  return NextResponse.json(
    { error: "Unauthorized", message: "Sign in required" },
    { status: 401 }
  )
}

if (!isAdminDomainUser(user)) {
  return NextResponse.json(
    { error: "Forbidden", message: "Admin domain (@nj.sgadi.us) required" },
    { status: 403 }
  )
}

Get Registration Statistics

Retrieve aggregated statistics about registrations within a date range.

Endpoint

GET /api/admin/stats

Query Parameters

start_date
string
default:"2026-07-23"
Start date (inclusive) in ISO format (YYYY-MM-DD)
end_date
string
default:"2026-08-08"
End date (inclusive) in ISO format (YYYY-MM-DD)

Response

success
boolean
Indicates successful operation
stats
object
Statistics object returned by the get_registrations_stats RPC function

Implementation

import { REGISTRATION_DATE_RANGE } from "@/lib/registration-date-range"

export async function GET(request: Request) {
  const headers = new Headers()
  headers.set("Cache-Control", "no-store, max-age=0")

  // ... authentication checks ...

  const { searchParams } = new URL(request.url)
  const startDate = searchParams.get("start_date") ?? REGISTRATION_DATE_RANGE.start
  const endDate = searchParams.get("end_date") ?? REGISTRATION_DATE_RANGE.end

  const { data, error } = await supabase.rpc("get_registrations_stats", {
    p_start_date: startDate,
    p_end_date: endDate,
  })

  if (error) {
    return NextResponse.json(
      { error: "Stats query failed", details: error.message },
      { status: 500, headers }
    )
  }

  return NextResponse.json(
    {
      success: true,
      stats: data,
    },
    { status: 200, headers }
  )
}
The stats endpoint calls the get_registrations_stats RPC function in Supabase, which performs read-only aggregations. No table writes occur.

Get Paginated Registrations

Retrieve paginated registration records with advanced filtering and search capabilities.

Endpoint

GET /api/admin/registrations

Pagination Parameters

page_size
integer
default:"25"
Number of records per page. Allowed values: 25, 50, 100
cursor
integer
ID of the last record from the previous page (for keyset pagination)
direction
string
default:"next"
Pagination direction: next or prev

Filter Parameters

ghaam
string
Filter by exact ghaam name
mandal
string
Filter by exact mandal name
country
string
Filter by exact country
age
integer
Filter by exact age
age_min
integer
Minimum age (inclusive)
age_max
integer
Maximum age (inclusive)
arrival_from
string
Earliest arrival date (YYYY-MM-DD, inclusive)
arrival_to
string
Latest arrival date (YYYY-MM-DD, inclusive)
departure_from
string
Earliest departure date (YYYY-MM-DD, inclusive)
departure_to
string
Latest departure date (YYYY-MM-DD, inclusive)
Text search across first name, last name, email, and mobile number. Minimum 2 characters per word.

Response

success
boolean
Indicates successful operation
rows
array
Array of registration records
pageSize
integer
Number of records in current page
nextCursor
integer | null
Cursor for the next page (null if no more pages)
prevCursor
integer | null
Cursor for the previous page (null if on first page)
hasMore
boolean
Whether more records exist after current page
hasPrev
boolean
Whether records exist before current page

Implementation

const PAGE_SIZES = [25, 50, 100] as const

export async function GET(request: Request) {
  // ... authentication checks ...

  const { searchParams } = new URL(request.url)
  const pageSize = parsePageSize(searchParams.get("page_size"))
  const cursor = cursorRaw ? parseInt(cursorRaw, 10) : null
  const direction = searchParams.get("direction") === "prev" ? "prev" : "next"

  // Parse filters
  const ghaam = searchParams.get("ghaam")?.trim() || null
  const mandal = searchParams.get("mandal")?.trim() || null
  const country = searchParams.get("country")?.trim() || null
  const age = parseOptionalInt(searchParams.get("age"))
  const ageMin = parseOptionalInt(searchParams.get("age_min"))
  const ageMax = parseOptionalInt(searchParams.get("age_max"))
  const search = searchRaw && searchRaw.length >= 2 ? searchRaw : null

  if (ageMin != null && ageMax != null && ageMin > ageMax) {
    return NextResponse.json(
      { error: "age_min must be <= age_max" },
      { status: 400, headers }
    )
  }

  const { data, error } = await supabase.rpc("get_registrations_filtered", {
    p_page_size: pageSize,
    p_cursor: cursor,
    p_direction: direction,
    p_ghaam: ghaam,
    p_mandal: mandal,
    p_country: country,
    p_age: age,
    p_age_min: ageMin,
    p_age_max: ageMax,
    p_arrival_from: arrivalFrom,
    p_arrival_to: arrivalTo,
    p_departure_from: departureFrom,
    p_departure_to: departureTo,
    p_search: search,
  })

  return NextResponse.json({
    success: true,
    rows: result.rows ?? [],
    pageSize: result.pageSize ?? pageSize,
    nextCursor: result.nextCursor ?? null,
    prevCursor: result.prevCursor ?? null,
    hasMore: result.hasMore ?? false,
    hasPrev: result.hasPrev ?? false,
  })
}
Keyset pagination is more efficient than offset-based pagination for large datasets. Always use the returned cursors for navigation.

Get Distinct Values

Retrieve distinct values for filter dropdowns (ghaam, mandal, country).

Endpoint

GET /api/admin/registrations/distinct

Response

success
boolean
Indicates successful operation
ghaams
array
Array of unique ghaam values
mandals
array
Array of unique mandal values
countries
array
Array of unique country values

Implementation

export async function GET() {
  // ... authentication checks ...

  const { data, error } = await supabase.rpc("get_registrations_distinct_values")

  if (error) {
    return NextResponse.json(
      { error: "Query failed", details: error.message },
      { status: 500, headers }
    )
  }

  return NextResponse.json(
    { success: true, ...(data as object) },
    { status: 200, headers }
  )
}

Test Read Endpoint

Test Supabase connectivity and RLS policies by reading a single registration record.

Endpoint

GET /api/admin/test-read

Description

Read-only test endpoint that returns the most recent registration record. Useful for verifying:
  • Supabase connection
  • Authentication flow
  • Row Level Security (RLS) policies
This endpoint does not write to any tables.

Response

success
boolean
Indicates successful operation
message
string
Description of the test result
row
object | null
Single registration record (most recent)
rowCount
integer
Number of rows returned (0 or 1)

Implementation

export async function GET() {
  // ... authentication checks ...

  const { data, error } = await supabase
    .from("registrations")
    .select("id, first_name, last_name, email, ghaam, mandal, arrival_date, departure_date")
    .order("id", { ascending: false })
    .limit(1)
    .maybeSingle()

  if (error) {
    return NextResponse.json(
      { error: "Query failed", details: error.message },
      { status: 500, headers }
    )
  }

  return NextResponse.json(
    {
      success: true,
      message: "Read-only test: 1 row from registrations",
      row: data,
      rowCount: data ? 1 : 0,
    },
    { status: 200, headers }
  )
}

Error Responses

All admin endpoints return consistent error responses:
{
  "error": "Unauthorized",
  "message": "Sign in required"
}

Cache Control

All admin endpoints set strict no-cache headers to ensure fresh data:
const headers = new Headers()
headers.set("Cache-Control", "no-store, max-age=0")

Best Practices

Pagination

  1. Always use keyset pagination with cursors for large datasets
  2. Store cursors from responses for navigation
  3. Use appropriate page sizes based on UI needs

Filtering

  1. Combine multiple filters for precise queries
  2. Use text search with minimum 2 characters
  3. Validate age ranges before submission

Performance

  1. Cache distinct values for filter dropdowns
  2. Use date range filters to limit result sets
  3. Monitor query performance with larger datasets

Security

  1. Never expose Supabase keys in client code
  2. Always verify admin domain on server side
  3. Log failed authentication attempts for monitoring

Build docs developers (and LLMs) love