gotify/server · error
cannot delete last admin
Error message
cannot delete last admin
What it means
Returned by DeleteUserByID with HTTP 400 when the deletion would remove the only remaining admin user. The handler counts users with Admin=true and, if the target is that last admin, refuses the delete to guarantee the system always retains at least one administrator able to manage users.
Source
Thrown at api/user.go:351
// schema:
// $ref: "#/definitions/Error"
// 404:
// description: Not Found
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
user, err := a.DB.GetUserByID(id)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if user != nil {
adminCount, err := a.DB.CountUser(&model.User{Admin: true})
if success := successOrAbort(ctx, 500, err); !success {
return
}
if user.Admin && adminCount == 1 {
ctx.AbortWithError(400, errors.New("cannot delete last admin"))
return
}
if err := a.UserChangeNotifier.fireUserDeleted(id); err != nil {
ctx.AbortWithError(500, err)
return
}
successOrAbort(ctx, 500, a.DB.DeleteUserByID(id))
} else {
ctx.AbortWithError(404, errors.New("user does not exist"))
}
})
}
// ChangePassword changes the password from the current user
// swagger:operation POST /current/user/password user updateCurrentUser
//
// Update the password of the current user.
//View on GitHub (pinned to 14bfc25627)
Solutions
- Promote another user to admin first (PUT /api/users/{id} with admin:true), then delete the original
- Skip admin users in bulk-delete scripts (check user.Admin before DELETE)
- Create a dedicated break-glass admin account so routine admins can be deleted
- Re-order operations: demote the target to non-admin only after another admin exists
Example fix
// before
await api.deleteUserByID(adminId); // 400 cannot delete last admin
// after
await api.updateUserByID(otherId, { admin: true });
await api.deleteUserByID(adminId); Defensive patterns
Strategy: validation
Validate before calling
const target = await api.getUserByID(id);
if (target && target.admin) {
const admins = (await api.listUsers()).filter(u => u.admin);
if (admins.length <= 1) {
throw new Error('refusing to delete the last admin; promote another user first');
}
} Type guard
function isLastAdmin(user, allUsers) {
return user != null && user.admin === true && allUsers.filter(u => u.admin).length === 1;
} Try / catch
try {
await api.deleteUserByID(id);
} catch (e) {
if (e.status === 400 && /last admin/.test(e.message)) { await promoteReplacementAdmin(); await api.deleteUserByID(id); }
else { throw e; }
} Prevention
- Skip users with admin:true in bulk-delete scripts
- Maintain a break-glass admin account outside automation scope
- Promote-before-delete ordering in deprovisioning workflows
- Monitor admin count so single-admin states are caught early
When it happens
Trigger: DELETE /api/users/{id} where the target user is admin and CountUser(&User{Admin:true}) == 1; cleanup scripts deleting all demo/staging users including the bootstrap admin; tenant offboarding that removes every account in one loop.
Common situations: Automated teardown scripts hitting the seeded admin account; single-admin organizations running deprovisioning flows; attempting to delete an admin before promoting a replacement; migrations that strip the admin flag from everyone then try deletion.
Related errors
- you are not allowed to access this api
- you are not allowed to create an admin user
- username already exists
- basic auth required
- invalid credentials
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/f770c83a02b2ba7d.
Report an issue: GitHub.