Admin Panel
Admin dashboard for user management, subscription oversight, blog editing, and platform stats.
Overview
ScaleRocket includes an admin panel at apps/ops/. It provides a dashboard with platform stats, user management, subscription oversight, and a blog editor. Only whitelisted users can access it.
Architecture
apps/ops/ (Vite + React)
├── src/pages/
│ ├── dashboard.tsx # Stats overview
│ ├── users.tsx # User management
│ ├── subscriptions.tsx # Subscription management
│ └── blog/ # Blog editor
├── src/components/
└── src/lib/
└── supabase.ts # Admin Supabase clientThe admin panel uses the Supabase service role key to bypass RLS and access all data. It runs as a separate Vite app deployed to its own domain.
Security
Admin Whitelist
Access is restricted to a list of authorized email addresses:
// apps/ops/src/lib/auth.ts
const ADMIN_EMAILS = [
"admin@yourdomain.com",
// Add more admin emails here
];
export function isAdmin(email: string): boolean {
return ADMIN_EMAILS.includes(email);
}Route Protection
// apps/ops/src/components/admin-guard.tsx
export function AdminGuard({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user || !isAdmin(user.email)) {
return <Navigate to="/unauthorized" />;
}
return children;
}Service Role Client
The admin panel uses the service role key for unrestricted database access:
// apps/ops/src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
export const supabaseAdmin = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY
);Important: The service role key is only used in the admin app. Never expose it in apps/web or apps/app.
Dashboard Stats
The dashboard shows key metrics:
// apps/ops/src/pages/dashboard.tsx
const stats = [
{
label: "Total Users",
value: await supabaseAdmin.from("profiles").select("*", { count: "exact", head: true }),
},
{
label: "Active Subscriptions",
value: await supabaseAdmin
.from("subscriptions")
.select("*", { count: "exact", head: true })
.eq("status", "active"),
},
{
label: "MRR",
value: calculateMRR(subscriptions),
},
{
label: "Signups (30d)",
value: await supabaseAdmin
.from("profiles")
.select("*", { count: "exact", head: true })
.gte("created_at", thirtyDaysAgo),
},
];User Management
View, search, and manage all users:
// Fetch users with their subscription status
const { data: users } = await supabaseAdmin
.from("profiles")
.select(`
*,
subscriptions (status, plan_id, current_period_end),
credits (balance, monthly_allowance)
`)
.order("created_at", { ascending: false });Admin actions available:
- View user details: Profile, subscription, credit balance
- Update subscription: Change plan, extend period
- Grant credits: Manually add credits to a user
- Delete user: Remove user and all associated data
Subscription Management
View and filter all subscriptions:
const { data: subscriptions } = await supabaseAdmin
.from("subscriptions")
.select(`
*,
profiles (email, full_name)
`)
.order("created_at", { ascending: false });Filter by status (active, canceled, past_due) or plan.
Blog Editor
The admin panel includes a basic blog editor for publishing content to the marketing site:
apps/ops/src/pages/blog/
├── index.tsx # Blog post list
├── editor.tsx # Create/edit posts
└── preview.tsx # Preview before publishBlog posts are stored in a posts table:
CREATE TABLE posts (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title text NOT NULL,
slug text UNIQUE NOT NULL,
content text,
excerpt text,
cover_image text,
published boolean DEFAULT false,
author_id uuid REFERENCES profiles(id),
published_at timestamptz,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);The marketing site (apps/web) fetches published posts and renders them.
Running the Admin Panel
# From the monorepo root
pnpm --filter ops dev
# Or using Turborepo
pnpm turbo dev --filter=opsThe admin panel runs on http://localhost:5174 by default.
Done reading? Mark this page as complete.