Users & departments
User CRUD, department CRUD, and the relationships between them.
Users
All /users/* endpoints require admin or superadmin platform role, except /users/me which requires only auth.
GET /users
List all users.
Query params:
search— case-insensitive match on name or emailplatformRole— filternone|admin|engineer|superadminorgPosition— filtermember|manager|ceodepartmentId— filter by departmentlimit/offset— pagination
Response 200:
{
"users": [
{
"id": "uuid",
"email": "...",
"name": "...",
"platformRole": "admin",
"orgPosition": "manager",
"departmentId": "uuid",
"department": {"id": "...", "name": "...", "color": "..."},
"avatarColor": "#93a4c4",
"status": "active",
"lastLoginAt": "...",
"createdAt": "..."
}
],
"total": 42
}
POST /users
Create a user. The caller must be admin or superadmin.
Request:
{
"email": "new@company.com",
"name": "New User",
"password": "min 8 chars",
"platformRole": "none" | "admin" | "engineer", // superadmin rejected
"orgPosition": "member" | "manager" | "ceo",
"departmentId": "uuid" // optional; omit for platform staff & CEO
}
Response 201:
{ "user": {...} }
Errors:
400 department_not_found— referenced department doesn't exist403 forbidden_role— attempted to createsuperadmin(only bootstrap can do that)409 ceo_exists— attempted to create a second CEO (CEO is a singleton)409 email_exists— email already taken
GET /users/:id
Fetch one user.
PATCH /users/:id
Update a user. Restrictions:
- Only
superadmincan change another user'splatformRoleto/fromadminorengineer - Nobody can set
platformRole = superadminvia this endpoint - CEO transfer (changing
orgPositiontoceowhile another exists) returns409 ceo_exists
{
"name": "...",
"email": "...",
"platformRole": "...",
"orgPosition": "...",
"departmentId": "...",
"status": "active" | "inactive",
"password": "..." // if present, re-hashes
}
DELETE /users/:id
Delete a user. Cascades: removes their grants, group memberships, project memberships. Conversations and messages are retained but user is set to null on them.
- Cannot delete the superadmin
- Cannot delete yourself
GET /users/me
Returns the current authenticated user. Used by the web app's auth context for hydration.
Departments
/departments/* endpoints require admin/superadmin, except GET-only endpoints also allow department manager or CEO.
GET /departments
List departments with member counts.
Response 200:
{
"departments": [
{
"id": "uuid",
"name": "Engineering",
"color": "#2a6fdb",
"description": "...",
"_count": {"members": 8, "groups": 3, "grants": 12}
}
]
}
POST /departments
Create a department.
Request:
{
"name": "Design",
"color": "#6b46c1",
"description": "Product + visual designers"
}
Name must be unique. Returns 201 with the created department.
PATCH /departments/:id
Update a department. admin/superadmin or the department's own manager.
DELETE /departments/:id
Delete a department. Blocked if:
- Any users belong to it (reassign them first)
- Any groups exist under it (delete them first)
- Any grants reference it (revoke them first)
Returns 409 with the blocker reason.
GET /departments/:id/members
List members of a department.
POST /departments/:id/members
Add existing users to a department. Requires admin/superadmin.
Request:
{ "userIds": ["uuid", "uuid"] }
A user can only belong to one department — POSTing a user already in another dept 409s unless replace: true is passed:
{ "userIds": [...], "replace": true }
DELETE /departments/:id/members/:userId
Remove a user from the department (sets their departmentId = null).
Department managers
There's no separate "manager" role on the department side. A user is the department's manager if:
user.orgPosition === "manager"user.departmentId === <this dept's id>
The requireCeoOrManager(paramKey) middleware enforces this on endpoints where the dept's manager should be able to act (e.g., creating groups, managing members within the dept).
Singleton constraints
- CEO: at most one user has
orgPosition = 'ceo'(enforced bysingle_ceopartial unique index) - Superadmin: at most one user has
platformRole = 'superadmin'(enforced bysingle_superadminpartial unique index)
Both are non-transferable in the dev phase. The wizard creates them; afterwards they can be renamed but not reassigned.