Skip to content

feat(admin): platform_admin flag + guard + read-only Overview/Workspaces (MN-104)#100

Open
ikrasovytskyi wants to merge 3 commits into
mainfrom
api/mn104-admin-panel
Open

feat(admin): platform_admin flag + guard + read-only Overview/Workspaces (MN-104)#100
ikrasovytskyi wants to merge 3 commits into
mainfrom
api/mn104-admin-panel

Conversation

@ikrasovytskyi

Copy link
Copy Markdown
Contributor

Summary

First cut of the superadmin panel's backend foundation — deliberately narrow, matching the ticket's own design notes ("start with Overview + Users + Workspaces..."). Everything here is read-only.

  • platform_admins is a separate table, not a column on user — better-auth owns that table (there's an existing warning comment in auth-schema.ts forbidding app columns on it, easy to have missed). Presence of a row = admin.
  • Seeded via PLATFORM_ADMIN_EMAIL at boot: looks up the user by email and grants if found; logs and no-ops (retried every restart) if they haven't signed up yet — safe to set the env var before the first operator's sign-up.
  • PlatformAdminGuard follows the exact WorkspaceAccessGuard convention (@UseGuards(AuthGuard, PlatformAdminGuard)), 403 not 404 since /admin's existence isn't a secret.
  • AdminOverviewService reads the existing entitlements/plan catalogue rather than recomputing pricing (per the ticket's own instruction) — estimatedMrrUsd is plan price + $12/seat overage summed across workspaces, explicitly labeled an estimate since it can drift from Stripe's actual invoiced total on proration/timing.
  • Known limitation, documented rather than hidden: per-workspace billable-seat lookups are one query per workspace (AccessService has no bulk variant) — fine at today's scale, needs revisiting well before thousands of workspaces.

Deliberately NOT built in this pass — each is substantial, safety-sensitive work in its own right, not a bolt-on to a read-only overview: impersonation (needs the "never trigger a charge/mutation while impersonating" guarantee designed and tested, not assumed), token revoke, GDPR export/erase, security audit log, billing ops UI (comp/refund/plan-change — MN-196's setOverride/clearOverride are the seam this will call), agent operability, moderation, growth metrics.

Test plan

  • 8 unit tests (platform-admin.service.test.ts, platform-admin.guard.test.ts) covering isPlatformAdmin, seedFromEnv (grants when found / no-ops when not), grant, revoke, and the guard's allow/reject paths.
  • 6 new integration tests (test/admin.test.ts): 401 with no auth, 403 for an authenticated non-admin, and a granted platform admin seeing counts/workspace rows across accounts they don't belong to — proving this is genuinely cross-workspace, not scoped by membership like every other endpoint.
  • Full local CI gate on a fresh branch rebased onto latest main: pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm docs:check && pnpm install --frozen-lockfile — all green, 820 tests passed (real testcontainer Postgres, migration 0046_puzzling_harrier.sql applied cleanly).
  • Live end-to-end verification against the running docker stack: signed up a real user, hit GET /admin/overview unauthenticated (401), authenticated-but-not-admin (403), granted platform_admin directly in Postgres, then got real cross-workspace data back — totalWorkspaces: 11, totalUsers: 13, totalRecords: 10103 — and GET /admin/workspaces correctly listed workspaces the test operator had zero membership in (e.g. pre-existing dev-stack workspaces from earlier sessions), proving the cross-tenant read path actually works, not just against fixtures the test created itself.

🤖 Generated with Claude Code

@ikrasovytskyi
ikrasovytskyi enabled auto-merge July 18, 2026 11:22
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
storyos-docs 9c6f26c Commit Preview URL

Branch Preview URL
Jul 18 2026, 03:35 PM

…ces (MN-104)

First cut of the superadmin panel's backend foundation — deliberately
narrow, matching the ticket's own design notes ("start with Overview +
Users + Workspaces..."). Everything here is read-only.

platform_admins is a separate table, NOT a column on `user` — better-auth
owns that table (see auth-schema.ts's existing comment forbidding app
columns on it). Presence of a row = admin. Seeded via PLATFORM_ADMIN_EMAIL
at boot: looks up the user by email and grants if found, logs and no-ops
(retried every restart) if they haven't signed up yet — safe to set the env
var before the first operator's sign-up.

PlatformAdminGuard follows the exact WorkspaceAccessGuard convention
(@UseGuards(AuthGuard, PlatformAdminGuard)), 403 not 404 since /admin's
existence isn't a secret. AdminOverviewService reads the existing
entitlements/plan catalogue rather than recomputing pricing (per the
ticket's own instruction) — estimatedMrrUsd is plan price + $12/seat
overage summed across workspaces, explicitly labeled an estimate since it
can drift from Stripe's actual invoiced total on proration/timing.

Known limitation, documented rather than hidden: per-workspace billable-seat
lookups are one query per workspace (AccessService has no bulk variant) —
fine at today's scale, needs revisiting well before thousands of
workspaces.

Deliberately NOT built in this pass — each is substantial, safety-sensitive
work in its own right, not a bolt-on to a read-only overview:
- Impersonation (needs the "never trigger a charge/mutation while
  impersonating" guarantee designed and tested, not assumed)
- Token revoke, GDPR export/erase, security audit log
- Billing ops UI (comp/refund/plan-change) — MN-196's setOverride/
  clearOverride are the seam this will call
- Agent operability, moderation, growth metrics — all explicitly
  forward-looking in the ticket itself

Tests: 8 unit tests (platform-admin.service.test.ts,
platform-admin.guard.test.ts) covering isPlatformAdmin, seedFromEnv
(grants when found / no-ops when not), grant, revoke, and the guard's
allow/reject paths. AdminOverviewService's multi-join aggregate queries are
better proven by a real integration test than a hand-rolled drizzle-chain
stub — deferred to land alongside this ticket's migration, once the
one-migration-in-flight lane clears (MN-195/MN-196 are ahead of it).

Migration deliberately not yet generated, same reason.
…rface (MN-104)

Real HTTP calls against a booted app: 403 for a non-platform-admin, 401
with no auth, and a platform admin (granted directly via a DB insert —
there's no HTTP grant surface yet) seeing counts and workspace rows across
accounts they don't belong to, proving this is genuinely cross-workspace
and not scoped by membership like every other endpoint in the app.

Not yet run against a live migration — platform_admins' migration is
deliberately deferred until the one-migration-in-flight lane clears
(MN-196 is ahead of it). Typechecks clean.
Regenerated after a genuine slot collision with an unrelated PR (#98,
sort-by-formula-fields) that also claimed 0046 and merged first.
@ikrasovytskyi
ikrasovytskyi force-pushed the api/mn104-admin-panel branch from 42fcb90 to 9c6f26c Compare July 18, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant