ScaleRocket/Web

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 client

The 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 publish

Blog 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=ops

The admin panel runs on http://localhost:5174 by default.

Done reading? Mark this page as complete.

On this page