semaphoreui/semaphore · error
owner can not left the project
Error message
owner can not left the project
What it means
removeUser (api/projects/users.go:126) blocks a project owner from removing themselves via LeftProject/RemoveUser unless the caller is an instance admin: if !me.Admin && targetUser.ID == me.ID && myRole == db.ProjectOwner it writes 'owner can not left the project'. This prevents projects from being left ownerless — ownership must be transferred first.
Solutions
- Assign another member the ProjectOwner role first (UpdateUser with role owner), then leave/remove yourself.
- Have an instance admin perform the removal — admins bypass this check.
- Create a new owner account, transfer ownership, then retry the leave operation.
- If the owner account is unavailable, use admin-level tooling to reassign project ownership in the database/store.
Example fix
// before
DELETE /api/project/{id}/users/{ownerId} // 400 owner can not left the project
// after: transfer ownership first, then remove
PUT /api/project/{id}/users/{otherUserId} {"role": "owner"}
DELETE /api/project/{id}/users/{ownerId} Defensive patterns
Strategy: type-guard
Validate before calling
// before leave/remove self
const me = await getProjectUser(projectId, myId);
if (me.role === "owner" && !isAdmin) {
await transferOwnership(projectId, newOwnerId); // must happen first
} Type guard
function isSelfOwner(user, me) {
return user.id === me.id && me.role === "owner" && !me.admin;
} Try / catch
try {
await deleteProjectUser(projectId, userId);
} catch (e) {
if (/owner can not left the project/.test(e.message)) {
show("Promote another owner before leaving the project.");
}
} Prevention
- Disable the leave button in the UI for sole owners
- Always promote a successor owner before offboarding an owner
- Use admin accounts for membership cleanups involving owners
- Educate teams: ownership transfer is a separate step from leaving
When it happens
Trigger: DELETE project-user (RemoveUser) or the 'leave project' endpoint (LeftProject) where the authenticated user is the target, is not an admin, and their project role is ProjectOwner.
Common situations: Sole owner clicking 'Leave project' in the UI; owner trying to clean up their own membership after handing off but before the role change was saved; scripting bulk membership removals that include the owner.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/adf370f6a6d9b5e4.
Report an issue: GitHub.
Appendix: source
Thrown at api/projects/users.go:126
helpers.EventLog(r, helpers.EventLogCreate, helpers.EventLogItem{
UserID: helpers.UserFromContext(r).ID,
ProjectID: project.ID,
ObjectType: db.EventUser,
ObjectID: projectUser.UserID,
Description: fmt.Sprintf("User ID %d added to team", projectUser.UserID),
})
w.WriteHeader(http.StatusNoContent)
}
// removeUser removes a user from a project team
func removeUser(targetUser db.User, w http.ResponseWriter, r *http.Request) {
project := helpers.GetFromContext(r, "project").(db.Project)
me := helpers.GetFromContext(r, "user").(*db.User) // logged in user
myRole := helpers.GetFromContext(r, "projectUserRole").(db.ProjectUserRole)
if !me.Admin && targetUser.ID == me.ID && myRole == db.ProjectOwner {
helpers.WriteError(w, fmt.Errorf("owner can not left the project"))
return
}
err := helpers.Store(r).DeleteProjectUser(project.ID, targetUser.ID)
if err != nil {
helpers.WriteError(w, err)
return
}
helpers.EventLog(r, helpers.EventLogDelete, helpers.EventLogItem{
UserID: helpers.UserFromContext(r).ID,
ProjectID: project.ID,
ObjectType: db.EventUser,
ObjectID: targetUser.ID,
Description: fmt.Sprintf("User ID %d removed from team", targetUser.ID),
})
View on GitHub (pinned to 1774ccb71a)