Skip to main content

Admin Control Plane

Implementation details for AD-017.

Admin API Endpoints

All routes require AdminGuard middleware (Keycloak bowrain-admin realm).

Workspace Management

MethodPathHandlerPurpose
GET/api/admin/workspacesHandleAdminListWorkspacesList all workspaces (search, filter by plan/status)
GET/api/admin/workspaces/:idHandleAdminGetWorkspaceFull workspace detail: plan, credits, members, activity
PUT/api/admin/workspaces/:id/planHandleAdminUpdatePlanChange workspace plan
POST/api/admin/workspaces/:id/creditsHandleAdminGrantCreditsGrant bonus credits
POST/api/admin/workspaces/:id/impersonateHandleAdminImpersonateCreate 1-hour impersonation token
POST/api/admin/workspaces/:id/membersHandleAdminAddMemberAdd/update workspace member

Feature Overrides & Notes

MethodPathHandlerPurpose
GET/api/admin/workspaces/:id/feature-overridesHandleAdminGetFeatureOverridesList overrides
PUT/api/admin/workspaces/:id/feature-overridesHandleAdminSetFeatureOverridesSet override
GET/api/admin/workspaces/:id/notesHandleAdminGetNotesInternal notes
POST/api/admin/workspaces/:id/notesHandleAdminAddNoteAdd note
GET/api/admin/workspaces/:id/ledgerHandleAdminGetLedgerCredit ledger

Users & Platform

MethodPathHandlerPurpose
GET/api/admin/usersHandleAdminListUsersSearch users (ILIKE on name + email)
GET/api/admin/users/:idHandleAdminGetUserUser detail + workspace memberships
GET/api/admin/metricsHandleAdminGetMetricsPlatform KPIs
GET/api/admin/eventsHandleAdminListEventsBilling events feed
GET/api/admin/upsellsHandleAdminGetUpsellsUpsell opportunities
GET/api/admin/overridesHandleAdminListOverridesAll feature overrides

Impersonation Flow

Admin clicks "View as Customer" in ctrl
→ POST /api/admin/workspaces/:id/impersonate
1. Look up workspace + find owner
2. CreateAPIToken(ownerID, wsID, "admin-impersonation", 1h expiry)
3. AddNote("Admin impersonation by admin@... — token bwt_xxxx expires ...")
4. Derive app URL from Origin header (ctrl.dev.bowrain.cloud → dev.bowrain.cloud)
5. Return { url, token, expires_at }
→ Frontend copies token to clipboard, opens URL in new tab

The token is a standard bwt_ API token validated by the existing handleAPIToken middleware. No special impersonation middleware needed.

Activity Read State Schema

CREATE TABLE activity_state (
user_id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, workspace_id)
);

Methods (on ActivityStore):

  • GetActivitySeenAt(ctx, userID, workspaceID)time.Time
  • SetActivitySeenAt(ctx, userID, workspaceID, seenAt)error
  • CountNewActivities(ctx, userID, workspaceID)int

API: GET /activities returns new_count field. POST /activities/seen updates cursor.

Frontend: ActivityIndicator receives newCount prop. TopBar calls markActivitiesSeen() when dropdown opens, then invalidates the activities query.

Member Add/Update Logic

// Check if already a member — update role instead of failing on duplicate key.
if existing, err := s.AuthStore.GetMembership(ctx, wsID, userID); err == nil && existing != nil {
if existing.Role == role {
return 409 Conflict "already a member with this role"
}
s.AuthStore.UpdateRole(ctx, wsID, userID, role) // update
} else {
s.AuthStore.AddMember(ctx, wsID, userID, role) // insert
}

Key Files

FilePurpose
platform/server/handlers_admin.goAll admin API handlers
platform/server/server.go (~line 850)Admin route registration
platform/billing/middleware.goAdminGuard middleware
platform/apps/ctrl/src/Control plane React app
platform/apps/ctrl/src/api.tsAdmin API client
platform/apps/ctrl/src/routes/workspace-detail.tsxWorkspace detail + impersonate + add member
platform/apps/ctrl/src/components/AddMemberDialog.tsxUser search + role dialog
platform/store/activity.goActivity read state (Get/Set/Count)