Organization

Departments, teams, the org chart, access profiles

Departments, the people in them, the reporting tree, and the app access that follows from where someone sits. Three routes — /organization, /organization/departments, /organization/directory — over api/organization.py (19 endpoints) and api/teams.py (5).

Departments are not teams#

Both exist, they are different things, and confusing them is the main way to get lost here.

DepartmentTeam
Modeldepartments, department_members, department_positionsteams, team_members
ShapeA tree — every department has an optional parentA flat set
Answers"where does this person sit, and who do they report to""who works on this together"
Grants app accessYes, via an access profileNo
Owns on-callNoYes — rotations are team-scoped

A person is normally in exactly one department and any number of teams.

Access profiles#

This is the part with consequences. A department carries a DepartmentAccessProfile — a named bundle of app access, resolved from SYSTEM_APP_BUNDLES. Put somebody in Engineering and they get the engineering bundle; move them to Sales and their sidebar changes.

GET  /departments/{id}/access-profile
PUT  /departments/{id}/access-profile
GET  /access-profiles

This is why frontend/src/config/appDefinitions.ts and backend/src/aexy/models/app_definitions.py must agree: the resolver reads the backend copy, the sidebar reads the frontend one, and a disagreement means the nav offers apps the API refuses — or hides apps the user can reach by typing the URL. scripts/dump_app_catalog.py regenerates the fixture that holds the two together; test_app_catalog_fixture.py and appCatalogParity.test.ts enforce it from either side.

The org chart#

GET /org-chart returns DepartmentNodes — the tree, pre-nested, rather than a flat list for the client to assemble. POST /departments/{id}/reparent moves a subtree; it moves the children with it, and it re-resolves access for everyone underneath, which is the expensive part.

Positions#

department_positions describes a seat rather than a person — title, PositionStatus, and the department it belongs to. A vacant position is a real row, which is what lets Hiring open a requisition against it and what makes headcount reporting possible before anyone is hired.

Directory#

GET /people returns PersonSummary — the workspace-wide list behind /organization/directory. It is the "everybody" view; /insights/developers is the narrower one, showing only people with engineering activity in a period.

Common pitfalls#

  • Reparenting changes access. Moving a department under a different parent can change what its members can open. It is not a cosmetic drag.
  • Deleting a department with children is refused, not cascaded. Reparent the children first.
  • A person with no department has no profile-derived access and falls back to their role. That is a valid state, and it is why some users see a different sidebar than their colleagues.
  • Team membership does not imply department membership. On-call reads teams; app access reads departments. A rotation and a sidebar can disagree entirely and both be correct.