Integration guide

Admin Dashboard

Production admin backend: user management, revenue analytics, feature flags with rollout percentages, audit log, and CSV export. One file, Supabase-native.

Read the Getting Access guideif you haven't yet.

1. Get the file

Sign in at marrowstack.dev, open the Admin Dashboard block, and click Copy all files. Paste lib/admin.ts (~510 lines) into your project. The SQL migration is embedded in the MIGRATION constant.

This block is server-side only. It exports typed async functions (no React components). Call them from your API routes or Server Actions and build your own UI on top.

2. Prerequisites

  • Next.js 14 or 15, App Router
  • Supabase project with the Auth block already running (admin.ts reads the profiles table)
  • An admin-role check in your auth layer (this block does not implement its own auth gate)

3. Install

  1. Copy admin.ts from the block detail page and paste it into lib/admin.ts in your project.
  2. Install peer dependencies:
bash
npm install @supabase/supabase-js zod

4. Environment

VariableRequiredDefaultPurpose
NEXT_PUBLIC_SUPABASE_URLyesYour Supabase project URL
SUPABASE_SERVICE_ROLE_KEYyesService role key — server-side only

5. Database

Run the MIGRATION constant from admin.ts in Supabase SQL Editor. Creates: feature_flags, admin_audit_log, and views over the profiles/orders tables. Requires the profiles table from the Auth block.

6. Wire it in

app/api/admin/users/route.ts
import { listUsers, banUser } from '@/lib/admin'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { NextRequest, NextResponse } from 'next/server'

export async function GET() {
  const session = await getServerSession(authOptions)
  if (session?.user?.role !== 'admin') return new NextResponse('Forbidden', { status: 403 })
  const users = await listUsers({ page: 1, pageSize: 50 })
  return NextResponse.json(users)
}

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions)
  if (session?.user?.role !== 'admin') return new NextResponse('Forbidden', { status: 403 })
  const { userId } = await req.json()
  await banUser(userId, session.user.id)
  return NextResponse.json({ ok: true })
}

7. Verify it works

  1. Call listUsers({ page: 1, pageSize: 10 }) from a test route. Confirm it returns your user rows from Supabase.
  2. Create a feature flag via createFeatureFlag({ key: 'test', enabled: true, rolloutPct: 100 }) and confirm the row appears in feature_flags.
  3. Call getRevenueSummary() and confirm it returns aggregated data.
  4. Call exportUsersCsv() and confirm the returned string is valid CSV.

8. Failure modes & fixes

relation 'feature_flags' does not exist

Cause: The migration has not been run.

Fix: Run the MIGRATION constant SQL in Supabase SQL Editor.

row-level security policy violation

Cause: Using the anon key instead of the service role key.

Fix: Set SUPABASE_SERVICE_ROLE_KEY. The admin client must use this key.

column 'role' does not exist on profiles

Cause: The Auth block's profiles table is missing the role column, or the Auth migration has not been run.

Fix: Run the Auth block's MIGRATION first. Admin depends on it.

Cannot read properties of undefined (reading 'rows')

Cause: Supabase query returned null — table does not exist or RLS blocked it.

Fix: Check that the migration ran and that the service role key is correct.

9. Security responsibilities

What this block handles:

  • All operations are audited to admin_audit_log with actor ID and timestamp.
  • Ban/unban and role-change operations require an actor_id (must be an admin user).
  • All table access goes through the service role key — RLS blocks direct client access.

What you must ensure:

  • Verify the caller is an admin in every API route before calling any admin function. This block does not enforce auth — that is your layer.
  • The admin API routes must never be publicly accessible without an auth gate.
  • Keep SUPABASE_SERVICE_ROLE_KEY server-side only. Exposing it client-side gives full database access to anyone.