diff --git a/docs/superpowers/plans/2026-04-24-supabase-admin-dashboard.md b/docs/superpowers/plans/2026-04-24-supabase-admin-dashboard.md
new file mode 100644
index 0000000..aea8956
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-24-supabase-admin-dashboard.md
@@ -0,0 +1,1009 @@
+# 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**
+
+```tsx
+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(
+
+
+
+ );
+
+ 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`:
+
+```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:
+
+```tsx
+export function PrivilegedGatePlaceholder() {
+ return Admin access token required;
+}
+```
+
+- [ ] **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**
+
+```bash
+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**
+
+```ts
+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`:
+
+```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:
+
+```ts
+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 `
+- 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:
+
+```ts
+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**
+
+```bash
+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**
+
+```tsx
+it("exchanges a privileged token and enters the admin shell", async () => {
+ render();
+
+ 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:
+
+```ts
+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:
+
+```ts
+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**
+
+```bash
+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**
+
+```tsx
+it("shows the production badge and admin sections", () => {
+ render();
+
+ 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:
+
+```tsx
+}>
+ } />
+ } />
+ } />
+ } />
+
+```
+
+- [ ] **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**
+
+```bash
+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**
+
+```ts
+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**
+
+```tsx
+it("loads rows for the selected table", async () => {
+ render();
+
+ 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:
+
+```ts
+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**
+
+```bash
+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**
+
+```ts
+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();
+});
+```
+
+```tsx
+it("shows a diff preview before saving a sensitive record", async () => {
+ render();
+
+ 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:
+
+```sql
+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`:
+
+```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**
+
+```bash
+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**
+
+```ts
+it("returns overview sections for the operations home", async () => {
+ const res = await authedAdminRequest("/api/v1/admin/overview");
+ expect(res.status).toBe(200);
+});
+```
+
+```tsx
+it("renders anomaly and recent-signup cards", async () => {
+ render();
+ 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**
+
+```bash
+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**
+
+```ts
+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" })]),
+ });
+});
+```
+
+```tsx
+it("builds a chart from a curated dataset", async () => {
+ render();
+
+ 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**
+
+```bash
+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**
+
+```ts
+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);
+});
+```
+
+```tsx
+it("shows structured action results", async () => {
+ render();
+ 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:
+
+```ts
+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**
+
+```bash
+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:
+
+```toml
+[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:
+
+```bash
+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:
+
+```bash
+pnpm lint
+pnpm typecheck
+```
+
+Expected: PASS, or only pre-existing unrelated failures clearly identified
+
+- [ ] **Step 5: Commit docs and hardening**
+
+```bash
+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?