Business Units
Org-level configurable lookup for grouping projects and managing membership. Managed in the Settings page. Supports soft-delete (archive) with a partial unique index ensuring active unit names are unique within an org.
Data Model
business_units
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
org_id | UUID | Foreign key to organizations |
name | TEXT | Display name (1-255 chars) |
deleted_at | TIMESTAMPTZ | Soft-delete timestamp (NULL = active) |
created_at | TIMESTAMPTZ | Auto-set on insert |
updated_at | TIMESTAMPTZ | Auto-updated via trigger |
business_unit_members
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
org_id | UUID | Foreign key to organizations |
business_unit_id | UUID | FK to business_units (CASCADE) |
user_id | UUID | FK to users (CASCADE) |
created_at | TIMESTAMPTZ | Auto-set on insert |
created_by | UUID | FK to users (who assigned this member) |
A UNIQUE (business_unit_id, user_id) constraint makes all member inserts idempotent.
Unique Name Constraint
CREATE UNIQUE INDEX idx_business_units_unique_active
ON business_units (org_id, name)
WHERE deleted_at IS NULL;This partial unique index allows archived units with the same name as active ones — an org can archive “Operations” and create a new “Operations” unit without conflict.
CRUD Operations
List
GET /v1/orgs/{orgId}/business-units returns active units by default. Pass ?include_archived=true to include soft-deleted units.
Create
- Admin clicks “Add Unit” in the Business Units tab
- Inline form appears with a name input
- Server Action validates name (1-255 chars), calls
POST /v1/orgs/{orgId}/business-units - Go handler inserts via
CreateBusinessUnitquery - If name conflicts with an existing active unit, database returns unique constraint error
- TanStack Query cache invalidated, list refreshes
Update (Rename)
- Admin clicks “Edit” on a unit row
- Name field becomes editable inline
- Enter to save, Escape to cancel
- Server Action calls
PUT /v1/orgs/{orgId}/business-units/{businessUnitId} - Go handler updates via
UpdateBusinessUnitquery (only active units can be renamed)
Archive
- Admin clicks “Archive” on a unit row, confirms via browser dialog
- Server Action calls
DELETE /v1/orgs/{orgId}/business-units/{businessUnitId} - Go handler sets
deleted_at = NOW()viaSoftDeleteBusinessUnitquery - Projects referencing this unit retain their
business_unit_id(FK isON DELETE SET NULL— but soft-delete doesn’t trigger this; the reference remains)
Member Management
Admins can assign org users to a Business Unit so they automatically join that BU’s projects.
Assigning Users to a Business Unit
- Admin clicks Members on a BU row in the Settings tab
- The Manage Members dialog opens, showing currently assigned users (name · email · org-default role badge)
- A searchable picker lists remaining org users (filtered live by name/email)
- Admin clicks Add — the user is assigned via
POST /v1/orgs/{orgId}/business-units/{businessUnitId}/members - The Go API writes the
business_unit_membersrow (idempotent:ON CONFLICT DO NOTHING) - A Temporal
BackfillBusinessUnitMembershipworkflow runs asynchronously, adding the user to every live project in the BU using their current org-default RBAC role - If Temporal is unavailable (dev/preview), the membership row is still written; the backfill is skipped with a warning log
Auto-Join Behaviour
Users assigned to a BU auto-join that BU’s projects in three scenarios:
| Scenario | When | Mechanism |
|---|---|---|
| Project create | A new project is created under a BU | Inline: Go CreateProject handler adds all current BU members synchronously |
| User assign | A user is assigned to an existing BU | Asynchronous: Temporal BackfillBusinessUnitMembership adds the user to all live BU projects |
| Project move | An existing project’s BU is changed and the admin opts in | Opt-in: The admin ticks “Add this BU’s members to the project” when editing project settings; sync_business_unit_members: true is sent with the UpdateProject request |
In all cases, membership is additive only — existing project members are never removed. The same user appearing via both BU membership and auto_member_roles is deduped by ON CONFLICT in the database.
Removing Users from a Business Unit
- Admin clicks Remove next to a member in the Manage Members dialog
- A confirmation dialog appears with an opt-in checkbox: “Also remove them from this BU’s projects”
- If unchecked (default): only the
business_unit_membersrow is deleted — the user remains on all projects - If checked: the
remove_from_projects=truequery param is sent toDELETE /v1/orgs/{orgId}/business-units/{businessUnitId}/members/{userId}, triggering a TemporalPruneBusinessUnitMembershipworkflow - The prune workflow skips any user whose org-default role is in the org’s effective
auto_member_roles— these users are auto-entitled org-wide and must not be removed by this flow
Project Move Sync
When editing a project’s Business Unit in the Admin panel:
- Select a new BU from the Business Unit dropdown
- An “Add this BU’s members to the project” checkbox appears (only when the BU changed to a different non-null value)
- If checked,
sync_business_unit_members: trueis included in thePUT /v1/orgs/{orgId}/projects/{projectId}request body - The Go API’s
UpdateProjecthandler runs the same inline add-loop asCreateProject, adding all current BU members to the project
UI
The Business Units tab lives in Settings (/org-dashboard/{orgId}/settings). It displays a table with columns:
| Column | Description |
|---|---|
| Name | Unit name (editable inline) |
| Status | Active or Archived badge |
| Created | Creation date |
| Actions | Edit, Archive, and Members buttons (active only, admin only) |
A “Show archived” checkbox toggles the include_archived query parameter. TanStack Query (useQuery) manages data fetching; useMutation handles create, update, archive, assign, and remove with cache invalidation.
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/{orgId}/business-units | List (?include_archived) |
POST | /v1/orgs/{orgId}/business-units | Create |
PUT | /v1/orgs/{orgId}/business-units/{businessUnitId} | Rename |
DELETE | /v1/orgs/{orgId}/business-units/{businessUnitId} | Soft-delete |
GET | /v1/orgs/{orgId}/business-units/{businessUnitId}/members | List members (with role display) |
POST | /v1/orgs/{orgId}/business-units/{businessUnitId}/members | Assign user to BU |
DELETE | /v1/orgs/{orgId}/business-units/{businessUnitId}/members/{userId} | Remove user from BU (?remove_from_projects) |
Security
- Admin-only — All mutations require
master_adminororganisational_managerrole (enforced viaauthz.KeyBusinessunitsManage). - Tenant isolation — All queries include
AND org_id = $N. The Go handler derivesorg_idfrom the path parameter and validates it against the session — never from the request body. - Soft-delete — No permanent data removal. Archived units can be viewed via the
include_archivedflag. - Unique names — Partial unique index prevents duplicate active names within an org.
- Role safety — Auto-prune never removes users whose org-default role is in the effective
auto_member_rolesset.
Key Files
| File | Purpose |
|---|---|
backend/db/migrations/20260710171503_business_unit_members.sql | business_unit_members join table migration |
backend/db/queries/business_units.sql | sqlc queries: CRUD + soft-delete |
backend/db/queries/business_unit_members.sql | sqlc queries: assign/remove/list BU members |
backend/api/internal/businessunit/handler.go | HTTP handlers for CRUD + member assign/remove/list |
backend/api/internal/businessunit/service.go | Business logic, pgtype conversions, Temporal client |
backend/api/internal/businessunit/members_service.go | Member assign/remove/list + workflow dispatch |
backend/workers/workflows/business_unit_membership.go | Temporal backfill + prune workflow definitions |
backend/workers/activities/business_unit_membership.go | Temporal activities: add/remove user from BU projects |
apps/web/src/actions/business-units.ts | BFF server actions: BU CRUD with Zod validation |
apps/web/src/actions/business-unit-members.ts | BFF server actions: assign/remove/list BU members |
apps/web/src/app/org/settings/_components/business-units-tab.tsx | CRUD UI with inline editing + Members action |
apps/web/src/app/org/settings/_components/bu-members-dialog.tsx | Manage-members dialog (assign picker + remove AlertDialog) |
apps/web/src/app/admin/_components/project-detail.tsx | Project edit form (BU selector + sync checkbox) |