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.jsononly 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.jsonapps/clients/vite.config.tsapps/clients/wrangler.tomlapps/clients/src/main.tsxapps/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
adminSessionto the Hono context -
never falls back to normal product auth
-
Step 6: Add
/api/v1/admin/auth/exchangeand/api/v1/admin/auth/session
adminAuth.ts should expose:
POST /auth/exchangeGET /auth/sessionPOST /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, andlogout -
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/tablesGET /api/v1/admin/tables/:tableId/metaGET /api/v1/admin/tables/:tableId/rowsGET /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
viewervsoperatorvssuperadmin -
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/datasetsGET /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-membershipresync-subscriptionmerge-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.tsif 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.tsdirectly 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?