Skip to main content

Admin Panel

Overview

The admin panel is an enterprise-only superadmin dashboard for platform-wide management of users, workspaces, and audit logs. It consists of two components:

  • @agenthifive/admin -- A Next.js 16 application (runs on port 3002) that provides the web UI. Built with React 19, TailwindCSS 4, and TanStack React Query.
  • Enterprise API routes -- A set of Fastify routes registered under /v1/admin/* that the admin app consumes.

The enterprise API (apps/enterprise-api) extends the core AgentHiFive API by importing buildApp() and startServer() from the core, then registering additional route plugins for admin management and push notification subscriptions.

Admin UI Pages

The admin app uses Next.js App Router with two route groups:

RouteDescription
/loginSuperadmin authentication form
/ (dashboard)Platform overview / stats
/usersPaginated user list with search
/users/[id]User detail with workspace, agents, and connections
/workspacesPaginated workspace list
/workspaces/[id]Workspace detail with agent and connection lists
/auditPlatform-wide audit event log

Access Control

Superadmin Role Requirement

Every admin API endpoint requires the superadmin platform role. The requireSuperadmin preHandler hook is attached to all routes in the admin plugin. It checks request.user.platformRole and returns a 403 response with { error: "Superadmin access required" } if the caller is not a superadmin.

// plugins/admin-guard.ts
export async function requireSuperadmin(
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> {
if (request.user.platformRole !== "superadmin") {
return reply.code(403).send({ error: "Superadmin access required" });
}
}

The guard is registered as a preHandler hook on the entire admin routes plugin, so it applies to every route under /v1/admin/* automatically.

Superadmin Bootstrap (Seeding)

The first superadmin account is created automatically on server startup via the seedSuperadmin() service. This function is idempotent and runs every time the enterprise API starts.

Behavior:

  1. Reads ADMIN_EMAIL and ADMIN_PASSWORD from environment variables.
  2. If either variable is missing, the seed is skipped silently (safe for CI environments).
  3. If a user with that email already exists and is already a superadmin, no action is taken.
  4. If a user with that email exists but is not a superadmin, the user is promoted to superadmin and email verification is set to true.
  5. If no user exists, a new account is created via Better Auth's signUpEmail API (which handles password hashing), then promoted to superadmin with email verification bypassed.
  6. Includes race-condition recovery -- if account creation fails due to a concurrent insert, it finds and promotes the existing row.

Admin API Endpoints

All endpoints below are prefixed with /v1 and require superadmin authentication. Responses use JSON. Admin actions that modify users are recorded in the audit log.

GET /v1/admin/stats

Platform statistics. Returns platform-wide summary counts.

  • Auth: Superadmin required
  • Response (200):
FieldTypeDescription
totalUsersintegerTotal registered users
totalWorkspacesintegerTotal workspaces
totalAgentsintegerTotal agents across all workspaces
totalConnectionsintegerTotal connections across all workspaces
recentAuditCountintegerAudit events in the last 7 days

GET /v1/admin/users

List all users. Paginated user list with optional search by email or name.

  • Auth: Superadmin required
  • Query parameters:
ParamTypeDefaultDescription
searchstring--Filter users by email or name (case-insensitive ILIKE)
limitstring"50"Page size (max 100)
offsetstring"0"Pagination offset
  • Response (200):
FieldTypeDescription
usersarrayList of user summary objects
totalintegerTotal matching user count

Each user object contains: id, email, name, platformRole, emailVerified, disabledAt (nullable), createdAt, workspaceName (nullable).


GET /v1/admin/users/:id

User detail. Returns a single user with workspace info, agent count, connection count, and a list of connections.

  • Auth: Superadmin required
  • Path parameters: id (string) -- User ID
  • Response (200):
FieldTypeDescription
idstringUser ID
emailstringUser email
namestringUser display name
platformRolestring"user" or "superadmin"
emailVerifiedbooleanWhether email is verified
disabledAtstring or nullTimestamp if disabled
createdAtstringAccount creation timestamp
workspaceIdstring or nullOwned workspace ID
workspaceNamestring or nullOwned workspace name
agentCountintegerNumber of agents in workspace
connectionCountintegerNumber of connections in workspace
connectionsarrayList of connection objects (id, provider, service, label, status, createdAt)
  • Response (404): { "error": "User not found" }

PATCH /v1/admin/users/:id/role

Change user platform role. Promote a user to superadmin or demote to regular user. Cannot change your own role.

  • Auth: Superadmin required
  • Path parameters: id (string) -- Target user ID
  • Request body:
FieldTypeRequiredDescription
platformRolestringYes"user" or "superadmin"
  • Response (200): { "ok": true }
  • Response (400): { "error": "Cannot change own role" }
  • Response (404): { "error": "User not found" }
  • Audit event: admin:user.role_changed with { targetId, platformRole }

POST /v1/admin/users/:id/disable

Disable user. Sets disabled_at timestamp and deletes all of the user's sessions for immediate lockout. Cannot disable your own account.

  • Auth: Superadmin required
  • Path parameters: id (string) -- Target user ID
  • Response (200): { "ok": true }
  • Response (400): { "error": "Cannot disable own account" }
  • Response (404): { "error": "User not found or already disabled" }
  • Audit event: admin:user.disabled with { targetId }

POST /v1/admin/users/:id/enable

Re-enable user. Clears the disabled_at timestamp so a previously disabled user can log in again.

  • Auth: Superadmin required
  • Path parameters: id (string) -- Target user ID
  • Response (200): { "ok": true }
  • Response (404): { "error": "User not found or not disabled" }
  • Audit event: admin:user.enabled with { targetId }

DELETE /v1/admin/users/:id

Delete user. Hard-deletes a user and cascades to their workspace, agents, connections, and policies via foreign key constraints. Cannot delete your own account.

  • Auth: Superadmin required
  • Path parameters: id (string) -- Target user ID
  • Response (200): { "ok": true }
  • Response (400): { "error": "Cannot delete own account" }
  • Response (404): { "error": "User not found" }
  • Audit event: admin:user.deleted with { targetId }

GET /v1/admin/workspaces

List all workspaces. Returns all workspaces with owner info and resource counts.

  • Auth: Superadmin required
  • Query parameters:
ParamTypeDefaultDescription
limitstring"50"Page size (max 100)
offsetstring"0"Pagination offset
  • Response (200):
FieldTypeDescription
workspacesarrayList of workspace objects
totalintegerTotal workspace count

Each workspace object contains: id, name, ownerEmail, ownerName, agentCount, connectionCount, createdAt.


GET /v1/admin/workspaces/:id

Workspace detail. Returns workspace info with full lists of agents and connections. Never exposes decrypted tokens.

  • Auth: Superadmin required
  • Path parameters: id (string) -- Workspace ID
  • Response (200):
FieldTypeDescription
idstringWorkspace ID
namestringWorkspace name
ownerEmailstringOwner's email
ownerNamestringOwner's display name
createdAtstringCreation timestamp
agentsarrayList of agent objects (id, name, status, createdAt)
connectionsarrayList of connection objects (id, provider, service, label, status, createdAt)
  • Response (404): { "error": "Workspace not found" }

GET /v1/admin/audit

Cross-workspace audit log. Returns paginated audit events across all workspaces with cursor-based pagination.

  • Auth: Superadmin required
  • Query parameters:
ParamTypeDefaultDescription
actionstring--Filter by action name (exact match)
cursorstring--Cursor for pagination (an auditId from a previous response)
limitstring"50"Page size (max 100)
  • Response (200):
FieldTypeDescription
eventsarrayList of audit event objects
nextCursorstring or nullCursor for the next page, or null if no more results

Each event object contains: auditId, action, actor, agentId (nullable), connectionId (nullable), metadata (nullable), timestamp.

Push Subscription Endpoints

These endpoints manage Expo push notification tokens for the mobile app. They are registered under /v1/push/* and require standard authentication (any logged-in user, not superadmin-specific).

POST /v1/push/subscribe

Register push token. Registers or updates an Expo push token for the authenticated user and device. Upserts on the token -- if the same device switches accounts, the existing row is updated.

  • Auth: Authenticated user required
  • Request body:
FieldTypeRequiredDescription
expoPushTokenstringYesExpo push token (must start with ExponentPushToken[)
platformstringYes"ios" or "android"
deviceNamestringNoOptional human-readable device name
  • Response (200):
FieldTypeDescription
idstring (uuid)Subscription record ID
expoPushTokenstringThe registered token
platformstring"ios" or "android"
deviceNamestring or nullDevice name if provided
  • Response (400): { "error": "Invalid Expo push token format" }

DELETE /v1/push/unsubscribe

Remove push token. Removes an Expo push token so the device no longer receives push notifications. Only deletes tokens belonging to the authenticated user.

  • Auth: Authenticated user required
  • Request body:
FieldTypeRequiredDescription
expoPushTokenstringYesThe Expo push token to remove
  • Response (200): { "deleted": true } or { "deleted": false } if the token was not found for this user.

Enterprise API Architecture

The enterprise API (apps/enterprise-api) wraps the core AgentHiFive API rather than forking it. The startup sequence in server.ts is:

  1. Import core API -- buildApp() and startServer() are imported from core/apps/api/src/server.ts.
  2. Build the core app -- buildApp() returns a fully configured Fastify instance with all core routes, plugins, database connections, and authentication.
  3. Seed superadmin -- seedSuperadmin(app.log) runs idempotently to ensure the bootstrap admin account exists.
  4. Register enterprise routes -- Additional route plugins are registered under the /v1 prefix:
    • pushSubscriptionRoutes -- Push notification token management
    • adminRoutes -- Superadmin management endpoints
  5. Start server -- startServer(app) binds the Fastify instance to the configured port.

This architecture means the enterprise API includes all core API routes plus the enterprise-specific additions, served from a single process.

Configuration

The following environment variables are used by the admin panel system:

VariableRequiredDescription
ADMIN_EMAILNoEmail address for the bootstrap superadmin account. If not set, no superadmin is seeded on startup.
ADMIN_PASSWORDNoPassword for the bootstrap superadmin account. Must be set together with ADMIN_EMAIL.

Both variables are optional in the sense that the server will start without them, but at least one superadmin account is needed to access the admin panel. In production, set both variables on first deployment to bootstrap the initial superadmin, then optionally remove them once additional superadmins have been promoted via the API.