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
- Copy
admin.tsfrom the block detail page and paste it intolib/admin.tsin your project. - Install peer dependencies:
npm install @supabase/supabase-js zod4. Environment
| Variable | Required | Default | Purpose |
|---|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | yes | — | Your Supabase project URL |
| SUPABASE_SERVICE_ROLE_KEY | yes | — | Service 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
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
- Call
listUsers({ page: 1, pageSize: 10 })from a test route. Confirm it returns your user rows from Supabase. - Create a feature flag via
createFeatureFlag({ key: 'test', enabled: true, rolloutPct: 100 })and confirm the row appears infeature_flags. - Call
getRevenueSummary()and confirm it returns aggregated data. - 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_logwith 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.