xtablo-source/docs/superpowers/plans/2026-04-24-supabase-admin-dashboard.md
2026-04-24 14:22:22 +02:00

32 KiB

Supabase Admin Dashboard Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build an internal-only apps/admin operations console for production Supabase data with privileged token-gated access, safe row editing, curated analytics, and audited admin actions.

Architecture: Add a standalone React app in apps/admin, but keep privileged trust on the server by introducing admin-only API routes, short-lived admin sessions, and registry-driven table, dataset, and action definitions. Deliver the product in slices: access foundation first, then explorer and auditability, then operations home, analytics, and custom actions.

Tech Stack: React 19, Vite, React Router, TanStack Query, Tailwind, @xtablo/ui, Hono, Supabase, Zod, AG Grid, Recharts, Vitest, React Testing Library


File Map

New app

  • Create: apps/admin/package.json
  • Create: apps/admin/tsconfig.json
  • Create: apps/admin/vite.config.ts
  • Create: apps/admin/wrangler.toml
  • Create: apps/admin/index.html
  • Create: apps/admin/worker/index.ts
  • Create: apps/admin/src/main.tsx
  • Create: apps/admin/src/App.tsx
  • Create: apps/admin/src/routes.tsx
  • Create: apps/admin/src/main.css
  • Create: apps/admin/src/lib/api.ts
  • Create: apps/admin/src/lib/adminSession.ts
  • Create: apps/admin/src/hooks/useAdminSession.ts
  • Create: apps/admin/src/hooks/useAdminTables.ts
  • Create: apps/admin/src/hooks/useAdminDatasets.ts
  • Create: apps/admin/src/hooks/useAdminActions.ts
  • Create: apps/admin/src/components/AdminLayout.tsx
  • Create: apps/admin/src/components/AdminNavigation.tsx
  • Create: apps/admin/src/components/ProductionBadge.tsx
  • Create: apps/admin/src/components/PrivilegedGate.tsx
  • Create: apps/admin/src/components/data-explorer/AdminGrid.tsx
  • Create: apps/admin/src/components/data-explorer/RowDetailDrawer.tsx
  • Create: apps/admin/src/components/data-explorer/RowEditForm.tsx
  • Create: apps/admin/src/components/analytics/ChartBuilder.tsx
  • Create: apps/admin/src/components/analytics/SavedDashboardList.tsx
  • Create: apps/admin/src/components/actions/ActionRunner.tsx
  • Create: apps/admin/src/pages/OperationsHomePage.tsx
  • Create: apps/admin/src/pages/DataExplorerPage.tsx
  • Create: apps/admin/src/pages/AnalyticsStudioPage.tsx
  • Create: apps/admin/src/pages/ActionCenterPage.tsx
  • Create: apps/admin/src/registry/tables.ts
  • Create: apps/admin/src/registry/datasets.ts
  • Create: apps/admin/src/registry/actions.ts
  • Test: apps/admin/src/routes.test.tsx
  • Test: apps/admin/src/components/PrivilegedGate.test.tsx
  • Test: apps/admin/src/pages/DataExplorerPage.test.tsx
  • Test: apps/admin/src/pages/AnalyticsStudioPage.test.tsx
  • Test: apps/admin/src/pages/ActionCenterPage.test.tsx

Shared types and docs

  • Create: packages/shared-types/src/admin.types.ts
  • Modify: packages/shared-types/src/index.ts
  • Create: docs/ADMIN_APP_ACCESS_SETUP.md

API and auth

  • Create: apps/api/src/helpers/adminTokens.ts
  • Create: apps/api/src/helpers/adminAudit.ts
  • Create: apps/api/src/helpers/adminRegistry.ts
  • Create: apps/api/src/routers/adminAuth.ts
  • Create: apps/api/src/routers/adminTables.ts
  • Create: apps/api/src/routers/adminDatasets.ts
  • Create: apps/api/src/routers/adminActions.ts
  • Create: apps/api/src/routers/admin.ts
  • Modify: apps/api/src/config.ts
  • Modify: apps/api/src/index.ts
  • Modify: apps/api/src/routers/index.ts
  • Modify: apps/api/src/middlewares/middleware.ts
  • Test: apps/api/src/__tests__/routes/adminAuth.test.ts
  • Test: apps/api/src/__tests__/routes/adminTables.test.ts
  • Test: apps/api/src/__tests__/routes/adminDatasets.test.ts
  • Test: apps/api/src/__tests__/routes/adminActions.test.ts
  • Test: apps/api/src/__tests__/middlewares/adminAuth.test.ts

Database

  • Create: supabase/migrations/20260424110000_create_admin_audit_log.sql
  • Create: supabase/migrations/20260424111000_create_admin_dataset_views.sql

Workspace wiring

  • Modify: package.json
  • Modify: turbo.json only if new task inputs or outputs are needed

Chunk 1: Access Foundation And App Scaffolding

Task 1: Scaffold apps/admin and workspace wiring

Files:

  • Create: apps/admin/package.json

  • Create: apps/admin/tsconfig.json

  • Create: apps/admin/vite.config.ts

  • Create: apps/admin/wrangler.toml

  • Create: apps/admin/index.html

  • Create: apps/admin/worker/index.ts

  • Create: apps/admin/src/main.tsx

  • Create: apps/admin/src/App.tsx

  • Create: apps/admin/src/routes.tsx

  • Create: apps/admin/src/main.css

  • Create: apps/admin/src/routes.test.tsx

  • Modify: package.json

  • Step 1: Write the failing route smoke test

import { MemoryRouter } from "react-router-dom";
import { render, screen } from "@testing-library/react";
import AppRoutes from "./routes";

it("renders the privileged gate on the root route", () => {
  render(
    <MemoryRouter initialEntries={["/"]}>
      <AppRoutes />
    </MemoryRouter>
  );

  expect(screen.getByText(/admin access token/i)).toBeInTheDocument();
});
  • Step 2: Run the new app test and verify it fails

Run: pnpm --filter @xtablo/admin test -- src/routes.test.tsx Expected: FAIL because @xtablo/admin and src/routes.tsx do not exist yet

  • Step 3: Copy the standalone app structure from apps/clients

Create a minimal app shell modeled on:

  • apps/clients/package.json
  • apps/clients/vite.config.ts
  • apps/clients/wrangler.toml
  • apps/clients/src/main.tsx
  • apps/clients/src/App.tsx

Use:

  • package name @xtablo/admin

  • dev port 5176

  • Cloudflare worker name xtablo-admin

  • private hostname route placeholders in wrangler.toml

  • Step 4: Add root workspace scripts

Add scripts to package.json:

{
  "dev:admin": "turbo dev --filter=@xtablo/admin",
  "deploy:admin": "turbo deploy --filter=@xtablo/admin"
}
  • Step 5: Add a minimal gate screen so the test passes

Stub the root route to render:

export function PrivilegedGatePlaceholder() {
  return <main>Admin access token required</main>;
}
  • Step 6: Re-run the route smoke test

Run: pnpm --filter @xtablo/admin test -- src/routes.test.tsx Expected: PASS

  • Step 7: Verify the app typechecks

Run: pnpm --filter @xtablo/admin typecheck Expected: PASS

  • Step 8: Commit the scaffold
git add package.json apps/admin
git commit -m "feat(admin): scaffold internal admin app"

Task 2: Add privileged token exchange and admin session verification in the API

Files:

  • Create: apps/api/src/helpers/adminTokens.ts

  • Create: apps/api/src/routers/adminAuth.ts

  • Create: apps/api/src/routers/admin.ts

  • Create: apps/api/src/__tests__/routes/adminAuth.test.ts

  • Create: apps/api/src/__tests__/middlewares/adminAuth.test.ts

  • Modify: apps/api/src/config.ts

  • Modify: apps/api/src/index.ts

  • Modify: apps/api/src/routers/index.ts

  • Modify: apps/api/src/middlewares/middleware.ts

  • Create: packages/shared-types/src/admin.types.ts

  • Modify: packages/shared-types/src/index.ts

  • Step 1: Write failing tests for token exchange and protected-session rejection

it("rejects requests without a valid privileged token", async () => {
  const res = await app.request("/api/v1/admin/auth/exchange", {
    method: "POST",
    body: JSON.stringify({ accessToken: "bad-token" }),
    headers: { "Content-Type": "application/json" },
  });

  expect(res.status).toBe(401);
});

it("rejects admin routes without an admin session", async () => {
  const res = await app.request("/api/v1/admin/tables/profiles");
  expect(res.status).toBe(401);
});
  • Step 2: Run the targeted API tests and verify failure

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminAuth.test.ts src/__tests__/middlewares/adminAuth.test.ts Expected: FAIL because the admin auth router and middleware do not exist

  • Step 3: Extend config for privileged token validation

Add to apps/api/src/config.ts:

ADMIN_TOKEN_SIGNING_SECRET: string;
ADMIN_TOKEN_AUDIENCE: string;
ADMIN_APP_URL: string;

Load them from env or secrets and fail fast if missing outside test mode.

  • Step 4: Add admin token helper and session payload types

In apps/api/src/helpers/adminTokens.ts, implement helpers shaped like:

export type AdminSessionClaims = {
  sub: string;
  role: "viewer" | "operator" | "superadmin";
  aud: string;
  exp: number;
};

export async function exchangePrivilegedToken(input: string, config: AppConfig) {
  // verify privileged token, then issue short-lived admin session
}

export async function verifyAdminSession(token: string, config: AppConfig) {
  // decode and validate admin session claims
}
  • Step 5: Add admin auth middleware

Extend apps/api/src/middlewares/middleware.ts with a dedicated admin middleware that:

  • reads Authorization: Bearer <admin-session>

  • verifies session claims

  • attaches adminSession to the Hono context

  • never falls back to normal product auth

  • Step 6: Add /api/v1/admin/auth/exchange and /api/v1/admin/auth/session

adminAuth.ts should expose:

  • POST /auth/exchange
  • GET /auth/session
  • POST /auth/logout

Use zod request validation and structured error responses.

  • Step 7: Register the new admin router

Mount it under /api/v1/admin without reusing the normal authenticated router chain.

  • Step 8: Add shared admin types

Export common types like:

export type AdminRole = "viewer" | "operator" | "superadmin";

export type AdminSessionResponse = {
  sessionToken: string;
  expiresAt: string;
  role: AdminRole;
  operatorEmail: string;
};
  • Step 9: Re-run API tests

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminAuth.test.ts src/__tests__/middlewares/adminAuth.test.ts Expected: PASS

  • Step 10: Commit the access backend
git add apps/api/src/config.ts apps/api/src/index.ts apps/api/src/routers/index.ts apps/api/src/middlewares/middleware.ts apps/api/src/helpers/adminTokens.ts apps/api/src/routers/adminAuth.ts apps/api/src/routers/admin.ts apps/api/src/__tests__/routes/adminAuth.test.ts apps/api/src/__tests__/middlewares/adminAuth.test.ts packages/shared-types/src/admin.types.ts packages/shared-types/src/index.ts
git commit -m "feat(admin): add privileged admin session exchange"

Task 3: Build the privileged gate and admin session client in apps/admin

Files:

  • Create: apps/admin/src/lib/api.ts

  • Create: apps/admin/src/lib/adminSession.ts

  • Create: apps/admin/src/hooks/useAdminSession.ts

  • Create: apps/admin/src/components/PrivilegedGate.tsx

  • Create: apps/admin/src/components/PrivilegedGate.test.tsx

  • Modify: apps/admin/src/routes.tsx

  • Modify: apps/admin/src/main.tsx

  • Step 1: Write the failing component test for exchanging a privileged token

it("exchanges a privileged token and enters the admin shell", async () => {
  render(<PrivilegedGate />);

  await userEvent.type(screen.getByLabelText(/access token/i), "valid-token");
  await userEvent.click(screen.getByRole("button", { name: /unlock admin/i }));

  expect(await screen.findByText(/operations home/i)).toBeInTheDocument();
});
  • Step 2: Run the test to confirm it fails

Run: pnpm --filter @xtablo/admin test -- src/components/PrivilegedGate.test.tsx Expected: FAIL because no session client or gate exists

  • Step 3: Create an admin-only API client

Implement apps/admin/src/lib/api.ts with:

export const adminApi = buildApi(import.meta.env.VITE_API_URL);

adminApi.interceptors.request.use((config) => {
  const token = getAdminSessionToken();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});
  • Step 4: Create local admin session storage helpers

In apps/admin/src/lib/adminSession.ts, keep the stored shape minimal:

type StoredAdminSession = {
  token: string;
  expiresAt: string;
  role: AdminRole;
  operatorEmail: string;
};
  • Step 5: Add useAdminSession

The hook should:

  • exchange privileged tokens

  • fetch current session metadata

  • clear expired sessions

  • expose isAuthenticated, role, operatorEmail, unlock, and logout

  • Step 6: Build the gate screen

PrivilegedGate.tsx should:

  • explain that normal Xtablo login is not sufficient

  • accept the special token

  • handle loading and invalid-token states

  • redirect authenticated operators into the shell

  • Step 7: Wire root routing through the gate

Root route behavior:

  • no admin session: gate

  • active admin session: AdminLayout

  • Step 8: Re-run admin frontend tests and typecheck

Run: pnpm --filter @xtablo/admin test -- src/components/PrivilegedGate.test.tsx src/routes.test.tsx Expected: PASS

Run: pnpm --filter @xtablo/admin typecheck Expected: PASS

  • Step 9: Commit the gate flow
git add apps/admin/src/lib/api.ts apps/admin/src/lib/adminSession.ts apps/admin/src/hooks/useAdminSession.ts apps/admin/src/components/PrivilegedGate.tsx apps/admin/src/components/PrivilegedGate.test.tsx apps/admin/src/routes.tsx apps/admin/src/main.tsx
git commit -m "feat(admin): add privileged token gate"

Chunk 2: Explorer, Guarded Mutations, And Auditability

Task 4: Build the admin shell and visual system

Files:

  • Create: apps/admin/src/components/AdminLayout.tsx

  • Create: apps/admin/src/components/AdminNavigation.tsx

  • Create: apps/admin/src/components/ProductionBadge.tsx

  • Create: apps/admin/src/pages/OperationsHomePage.tsx

  • Modify: apps/admin/src/App.tsx

  • Modify: apps/admin/src/routes.tsx

  • Modify: apps/admin/src/main.css

  • Test: apps/admin/src/components/AdminLayout.test.tsx

  • Step 1: Write a failing shell test for navigation and production context

it("shows the production badge and admin sections", () => {
  render(<AdminLayout />);

  expect(screen.getByText(/production/i)).toBeInTheDocument();
  expect(screen.getByRole("link", { name: /data explorer/i })).toBeInTheDocument();
  expect(screen.getByRole("link", { name: /analytics studio/i })).toBeInTheDocument();
});
  • Step 2: Run the shell test and verify failure

Run: pnpm --filter @xtablo/admin test -- src/components/AdminLayout.test.tsx Expected: FAIL because the admin shell does not exist

  • Step 3: Build the navigation and shell

The shell should include:

  • top-level nav for Operations Home, Data Explorer, Analytics Studio, Action Center

  • persistent operator identity

  • prominent but tasteful production badge

  • strong visual hierarchy using the design spec colors and spacing

  • Step 4: Replace the placeholder route target with the real shell

Route structure:

<Route element={<AdminLayout />}>
  <Route index element={<OperationsHomePage />} />
  <Route path="/explorer/:tableId?" element={<DataExplorerPage />} />
  <Route path="/analytics" element={<AnalyticsStudioPage />} />
  <Route path="/actions" element={<ActionCenterPage />} />
</Route>
  • Step 5: Re-run the shell test

Run: pnpm --filter @xtablo/admin test -- src/components/AdminLayout.test.tsx Expected: PASS

  • Step 6: Commit the shell
git add apps/admin/src/components/AdminLayout.tsx apps/admin/src/components/AdminNavigation.tsx apps/admin/src/components/ProductionBadge.tsx apps/admin/src/pages/OperationsHomePage.tsx apps/admin/src/App.tsx apps/admin/src/routes.tsx apps/admin/src/main.css
git commit -m "feat(admin): add admin shell and visual system"

Task 5: Add registry-driven table exploration and approved read endpoints

Files:

  • Create: apps/admin/src/registry/tables.ts

  • Create: apps/admin/src/hooks/useAdminTables.ts

  • Create: apps/admin/src/pages/DataExplorerPage.tsx

  • Create: apps/admin/src/components/data-explorer/AdminGrid.tsx

  • Create: apps/admin/src/components/data-explorer/RowDetailDrawer.tsx

  • Create: apps/admin/src/pages/DataExplorerPage.test.tsx

  • Create: apps/api/src/helpers/adminRegistry.ts

  • Create: apps/api/src/routers/adminTables.ts

  • Create: apps/api/src/__tests__/routes/adminTables.test.ts

  • Step 1: Write failing API tests for approved-table reads

it("returns table metadata for an approved table", async () => {
  const res = await authedAdminRequest("/api/v1/admin/tables/profiles/meta");
  expect(res.status).toBe(200);
});

it("rejects a table that is not in the registry", async () => {
  const res = await authedAdminRequest("/api/v1/admin/tables/secrets/rows");
  expect(res.status).toBe(404);
});
  • Step 2: Write a failing frontend test for switching tables
it("loads rows for the selected table", async () => {
  render(<DataExplorerPage />);

  await userEvent.click(screen.getByRole("button", { name: /profiles/i }));

  expect(await screen.findByText(/email/i)).toBeInTheDocument();
});
  • Step 3: Run API and frontend tests to verify failure

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTables.test.ts Expected: FAIL

Run: pnpm --filter @xtablo/admin test -- src/pages/DataExplorerPage.test.tsx Expected: FAIL

  • Step 4: Define the table registry

Seed the registry with 3 to 5 core resources only:

export const adminTables = {
  profiles: {
    label: "Users",
    source: "profiles",
    editableColumns: ["first_name", "last_name", "plan"],
  },
  organizations: {
    label: "Organizations",
    source: "organizations",
    editableColumns: ["name"],
  },
  tablo_access: {
    label: "Tablo Access",
    source: "tablo_access",
    editableColumns: ["is_active", "is_admin"],
  },
};
  • Step 5: Add the admin tables API

Endpoints:

  • GET /api/v1/admin/tables
  • GET /api/v1/admin/tables/:tableId/meta
  • GET /api/v1/admin/tables/:tableId/rows
  • GET /api/v1/admin/tables/:tableId/rows/:rowId

Keep all queries registry-driven. Do not allow arbitrary table names.

  • Step 6: Build the explorer UI

Implement:

  • left rail of table groups

  • AG Grid-based row list

  • row detail drawer with linked values and metadata

  • saved filter state scoped per table

  • Step 7: Re-run table API and explorer tests

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTables.test.ts Expected: PASS

Run: pnpm --filter @xtablo/admin test -- src/pages/DataExplorerPage.test.tsx Expected: PASS

  • Step 8: Commit approved reads
git add apps/admin/src/registry/tables.ts apps/admin/src/hooks/useAdminTables.ts apps/admin/src/pages/DataExplorerPage.tsx apps/admin/src/components/data-explorer/AdminGrid.tsx apps/admin/src/components/data-explorer/RowDetailDrawer.tsx apps/admin/src/pages/DataExplorerPage.test.tsx apps/api/src/helpers/adminRegistry.ts apps/api/src/routers/adminTables.ts apps/api/src/__tests__/routes/adminTables.test.ts
git commit -m "feat(admin): add registry-driven data explorer reads"

Task 6: Add guarded row updates and audit logging

Files:

  • Create: supabase/migrations/20260424110000_create_admin_audit_log.sql

  • Create: apps/api/src/helpers/adminAudit.ts

  • Modify: apps/api/src/routers/adminTables.ts

  • Create: apps/admin/src/components/data-explorer/RowEditForm.tsx

  • Modify: apps/admin/src/components/data-explorer/RowDetailDrawer.tsx

  • Create: apps/api/src/__tests__/routes/adminTableEdits.test.ts

  • Create: apps/admin/src/components/data-explorer/RowEditForm.test.tsx

  • Step 1: Write failing tests for guarded edits and audit emission

it("writes an audit log entry for a successful update", async () => {
  const res = await authedOperatorRequest("/api/v1/admin/tables/profiles/rows/user-1", {
    method: "PATCH",
    body: JSON.stringify({ first_name: "Ada" }),
  });

  expect(res.status).toBe(200);
  expect(mockInsertAuditLog).toHaveBeenCalled();
});
it("shows a diff preview before saving a sensitive record", async () => {
  render(<RowEditForm record={record} />);

  await userEvent.type(screen.getByLabelText(/plan/i), "team");
  await userEvent.click(screen.getByRole("button", { name: /review changes/i }));

  expect(screen.getByText(/before/i)).toBeInTheDocument();
  expect(screen.getByText(/after/i)).toBeInTheDocument();
});
  • Step 2: Run the edit tests and verify failure

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTableEdits.test.ts Expected: FAIL

Run: pnpm --filter @xtablo/admin test -- src/components/data-explorer/RowEditForm.test.tsx Expected: FAIL

  • Step 3: Add the audit log migration

Create a migration like:

create table public.admin_audit_log (
  id bigserial primary key,
  operator_id text not null,
  operator_email text not null,
  role text not null,
  action text not null,
  target_type text not null,
  target_id text not null,
  before jsonb,
  after jsonb,
  created_at timestamptz not null default now()
);
  • Step 4: Implement audit helper

In apps/api/src/helpers/adminAudit.ts:

export async function recordAdminAuditLog(args: {
  operatorId: string;
  operatorEmail: string;
  role: string;
  action: string;
  targetType: string;
  targetId: string;
  before?: unknown;
  after?: unknown;
}) {
  // insert into admin_audit_log
}
  • Step 5: Add PATCH support to adminTables.ts

Rules:

  • only registry-approved columns may change

  • role-aware permissions enforce viewer vs operator vs superadmin

  • sensitive fields return a diff payload before final confirmation

  • every successful write records an audit entry

  • Step 6: Add the row edit form and diff preview

Support typed editors for:

  • text

  • booleans

  • enums

  • nullable values

  • timestamps rendered read-only

  • Step 7: Re-run edit tests

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTableEdits.test.ts Expected: PASS

Run: pnpm --filter @xtablo/admin test -- src/components/data-explorer/RowEditForm.test.tsx Expected: PASS

  • Step 8: Commit guarded edits
git add supabase/migrations/20260424110000_create_admin_audit_log.sql apps/api/src/helpers/adminAudit.ts apps/api/src/routers/adminTables.ts apps/api/src/__tests__/routes/adminTableEdits.test.ts apps/admin/src/components/data-explorer/RowEditForm.tsx apps/admin/src/components/data-explorer/RowEditForm.test.tsx apps/admin/src/components/data-explorer/RowDetailDrawer.tsx
git commit -m "feat(admin): add guarded row edits with audit logging"

Chunk 3: Operations Views, Analytics, Actions, And Rollout

Task 7: Build Operations Home with curated operational cards

Files:

  • Modify: apps/admin/src/pages/OperationsHomePage.tsx

  • Create: apps/api/src/routers/adminOverview.ts

  • Create: apps/api/src/__tests__/routes/adminOverview.test.ts

  • Create: apps/admin/src/pages/OperationsHomePage.test.tsx

  • Modify: apps/api/src/routers/admin.ts

  • Step 1: Write failing tests for overview cards

it("returns overview sections for the operations home", async () => {
  const res = await authedAdminRequest("/api/v1/admin/overview");
  expect(res.status).toBe(200);
});
it("renders anomaly and recent-signup cards", async () => {
  render(<OperationsHomePage />);
  expect(await screen.findByText(/recent signups/i)).toBeInTheDocument();
  expect(await screen.findByText(/operational anomalies/i)).toBeInTheDocument();
});
  • Step 2: Run the overview tests and confirm failure

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminOverview.test.ts Expected: FAIL

Run: pnpm --filter @xtablo/admin test -- src/pages/OperationsHomePage.test.tsx Expected: FAIL

  • Step 3: Add the overview endpoint

Return a small curated payload:

  • recent signups

  • recent organizations

  • subscription exceptions

  • pinned saved explorer views

  • anomaly counts

  • Step 4: Build the home page cards

Make the first screen useful even if analytics is unfinished.

  • Step 5: Re-run overview tests

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminOverview.test.ts Expected: PASS

Run: pnpm --filter @xtablo/admin test -- src/pages/OperationsHomePage.test.tsx Expected: PASS

  • Step 6: Commit operations home
git add apps/api/src/routers/adminOverview.ts apps/api/src/__tests__/routes/adminOverview.test.ts apps/api/src/routers/admin.ts apps/admin/src/pages/OperationsHomePage.tsx apps/admin/src/pages/OperationsHomePage.test.tsx
git commit -m "feat(admin): add operations home dashboard"

Task 8: Add curated analytics datasets and dashboard building

Files:

  • Create: supabase/migrations/20260424111000_create_admin_dataset_views.sql

  • Create: apps/api/src/routers/adminDatasets.ts

  • Create: apps/api/src/__tests__/routes/adminDatasets.test.ts

  • Create: apps/admin/src/registry/datasets.ts

  • Create: apps/admin/src/hooks/useAdminDatasets.ts

  • Create: apps/admin/src/pages/AnalyticsStudioPage.tsx

  • Create: apps/admin/src/components/analytics/ChartBuilder.tsx

  • Create: apps/admin/src/components/analytics/SavedDashboardList.tsx

  • Create: apps/admin/src/pages/AnalyticsStudioPage.test.tsx

  • Step 1: Write failing analytics dataset tests

it("lists curated datasets only", async () => {
  const res = await authedAdminRequest("/api/v1/admin/datasets");
  expect(res.status).toBe(200);
  expect(await res.json()).toMatchObject({
    datasets: expect.arrayContaining([expect.objectContaining({ id: "signups" })]),
  });
});
it("builds a chart from a curated dataset", async () => {
  render(<AnalyticsStudioPage />);

  await userEvent.selectOptions(screen.getByLabelText(/dataset/i), "signups");
  await userEvent.selectOptions(screen.getByLabelText(/metric/i), "count");

  expect(await screen.findByRole("img", { name: /signups chart/i })).toBeInTheDocument();
});
  • Step 2: Run the analytics tests and confirm failure

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminDatasets.test.ts Expected: FAIL

Run: pnpm --filter @xtablo/admin test -- src/pages/AnalyticsStudioPage.test.tsx Expected: FAIL

  • Step 3: Create dataset views

Back them with curated views such as:

  • admin_signups_daily

  • admin_organizations_daily

  • admin_subscription_exceptions

  • Step 4: Add dataset registry and API

Expose:

  • GET /api/v1/admin/datasets
  • GET /api/v1/admin/datasets/:datasetId/query

Only allow registry-defined dimensions, metrics, and grains.

  • Step 5: Build Analytics Studio

Implement:

  • dataset picker
  • metric and dimension controls
  • date-range filters
  • chart type picker
  • saved dashboard list

Use recharts and accessible titles for chart containers.

  • Step 6: Re-run analytics tests

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminDatasets.test.ts Expected: PASS

Run: pnpm --filter @xtablo/admin test -- src/pages/AnalyticsStudioPage.test.tsx Expected: PASS

  • Step 7: Commit analytics
git add supabase/migrations/20260424111000_create_admin_dataset_views.sql apps/api/src/routers/adminDatasets.ts apps/api/src/__tests__/routes/adminDatasets.test.ts apps/admin/src/registry/datasets.ts apps/admin/src/hooks/useAdminDatasets.ts apps/admin/src/pages/AnalyticsStudioPage.tsx apps/admin/src/components/analytics/ChartBuilder.tsx apps/admin/src/components/analytics/SavedDashboardList.tsx apps/admin/src/pages/AnalyticsStudioPage.test.tsx
git commit -m "feat(admin): add curated analytics studio"

Task 9: Add Action Center and first custom admin workflows

Files:

  • Create: apps/admin/src/registry/actions.ts

  • Create: apps/admin/src/hooks/useAdminActions.ts

  • Create: apps/admin/src/pages/ActionCenterPage.tsx

  • Create: apps/admin/src/components/actions/ActionRunner.tsx

  • Create: apps/admin/src/pages/ActionCenterPage.test.tsx

  • Create: apps/api/src/routers/adminActions.ts

  • Create: apps/api/src/__tests__/routes/adminActions.test.ts

  • Modify: apps/api/src/routers/admin.ts

  • Step 1: Write failing tests for action discovery and execution

it("executes a registry-defined admin action", async () => {
  const res = await authedSuperadminRequest("/api/v1/admin/actions/repair-membership", {
    method: "POST",
    body: JSON.stringify({ userId: "user-1", organizationId: "org-1" }),
  });

  expect(res.status).toBe(200);
});
it("shows structured action results", async () => {
  render(<ActionCenterPage />);
  await userEvent.click(screen.getByRole("button", { name: /repair membership/i }));
  expect(await screen.findByText(/result/i)).toBeInTheDocument();
});
  • Step 2: Run the action tests and confirm failure

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminActions.test.ts Expected: FAIL

Run: pnpm --filter @xtablo/admin test -- src/pages/ActionCenterPage.test.tsx Expected: FAIL

  • Step 3: Define the action registry

Start with 2 to 4 actions only:

  • repair-membership
  • resync-subscription
  • merge-duplicate-profile

Each entry must define:

  • role requirement

  • form schema

  • confirmation copy

  • handler route

  • Step 4: Add admin action endpoints

Pattern:

adminActions.post("/:actionId", async (c) => {
  // load registry entry
  // validate input
  // execute handler
  // write audit log
  // return structured result
});
  • Step 5: Build the Action Center UI

Include:

  • action catalog

  • permission-aware disabled states

  • typed form rendering

  • confirmation step

  • structured success and partial-failure results

  • Step 6: Re-run action tests

Run: pnpm --filter @xtablo/api test -- src/__tests__/routes/adminActions.test.ts Expected: PASS

Run: pnpm --filter @xtablo/admin test -- src/pages/ActionCenterPage.test.tsx Expected: PASS

  • Step 7: Commit custom actions
git add apps/admin/src/registry/actions.ts apps/admin/src/hooks/useAdminActions.ts apps/admin/src/pages/ActionCenterPage.tsx apps/admin/src/components/actions/ActionRunner.tsx apps/admin/src/pages/ActionCenterPage.test.tsx apps/api/src/routers/adminActions.ts apps/api/src/__tests__/routes/adminActions.test.ts apps/api/src/routers/admin.ts
git commit -m "feat(admin): add audited action center workflows"

Task 10: Deployment hardening, access docs, and final verification

Files:

  • Create: docs/ADMIN_APP_ACCESS_SETUP.md

  • Modify: apps/admin/wrangler.toml

  • Modify: apps/api/src/config.ts if any env names changed during implementation

  • Step 1: Write the failing docs checklist

Create a checklist in docs/ADMIN_APP_ACCESS_SETUP.md covering:

  • private hostname or protected subdomain

  • Cloudflare Access or VPN requirements

  • privileged token issuance and rotation

  • required env vars

  • rollback path

  • Step 2: Add private-route deployment details

Document placeholders like:

[env.production]
route = { pattern = "admin.internal.xtablo.com", custom_domain = true }

Also document that public DNS alone is not sufficient; edge access policy must be enabled.

  • Step 3: Run focused verification

Run:

pnpm --filter @xtablo/api test -- src/__tests__/routes/adminAuth.test.ts src/__tests__/routes/adminTables.test.ts src/__tests__/routes/adminDatasets.test.ts src/__tests__/routes/adminActions.test.ts
pnpm --filter @xtablo/admin test -- src/routes.test.tsx src/components/PrivilegedGate.test.tsx src/pages/DataExplorerPage.test.tsx src/pages/AnalyticsStudioPage.test.tsx src/pages/ActionCenterPage.test.tsx
pnpm --filter @xtablo/api typecheck
pnpm --filter @xtablo/admin typecheck

Expected: all PASS

  • Step 4: Run broader workspace checks before merge

Run:

pnpm lint
pnpm typecheck

Expected: PASS, or only pre-existing unrelated failures clearly identified

  • Step 5: Commit docs and hardening
git add docs/ADMIN_APP_ACCESS_SETUP.md apps/admin/wrangler.toml apps/api/src/config.ts
git commit -m "docs(admin): add deployment and access runbook"

Notes For Execution

  • Do not bypass the privileged token model by reusing apps/main/src/lib/supabase.ts directly in the admin app.
  • Keep the initial registry intentionally small; expand after the guarded end-to-end slice is working.
  • Prefer API-mediated reads and writes for admin resources even where direct browser Supabase access would be faster to code.
  • When a task touches both API and frontend, land the contract tests first, then the server, then the UI.
  • If the private edge boundary cannot be fully provisioned in code, keep the repo changes ready and document the exact manual platform steps in docs/ADMIN_APP_ACCESS_SETUP.md.

Plan complete and saved to docs/superpowers/plans/2026-04-24-supabase-admin-dashboard.md. Ready to execute?