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

ColumnTypeDescription
idUUIDPrimary key
org_idUUIDForeign key to organizations
nameTEXTDisplay name (1-255 chars)
deleted_atTIMESTAMPTZSoft-delete timestamp (NULL = active)
created_atTIMESTAMPTZAuto-set on insert
updated_atTIMESTAMPTZAuto-updated via trigger

business_unit_members

ColumnTypeDescription
idUUIDPrimary key
org_idUUIDForeign key to organizations
business_unit_idUUIDFK to business_units (CASCADE)
user_idUUIDFK to users (CASCADE)
created_atTIMESTAMPTZAuto-set on insert
created_byUUIDFK 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

  1. Admin clicks “Add Unit” in the Business Units tab
  2. Inline form appears with a name input
  3. Server Action validates name (1-255 chars), calls POST /v1/orgs/{orgId}/business-units
  4. Go handler inserts via CreateBusinessUnit query
  5. If name conflicts with an existing active unit, database returns unique constraint error
  6. TanStack Query cache invalidated, list refreshes

Update (Rename)

  1. Admin clicks “Edit” on a unit row
  2. Name field becomes editable inline
  3. Enter to save, Escape to cancel
  4. Server Action calls PUT /v1/orgs/{orgId}/business-units/{businessUnitId}
  5. Go handler updates via UpdateBusinessUnit query (only active units can be renamed)

Archive

  1. Admin clicks “Archive” on a unit row, confirms via browser dialog
  2. Server Action calls DELETE /v1/orgs/{orgId}/business-units/{businessUnitId}
  3. Go handler sets deleted_at = NOW() via SoftDeleteBusinessUnit query
  4. Projects referencing this unit retain their business_unit_id (FK is ON 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

  1. Admin clicks Members on a BU row in the Settings tab
  2. The Manage Members dialog opens, showing currently assigned users (name · email · org-default role badge)
  3. A searchable picker lists remaining org users (filtered live by name/email)
  4. Admin clicks Add — the user is assigned via POST /v1/orgs/{orgId}/business-units/{businessUnitId}/members
  5. The Go API writes the business_unit_members row (idempotent: ON CONFLICT DO NOTHING)
  6. A Temporal BackfillBusinessUnitMembership workflow runs asynchronously, adding the user to every live project in the BU using their current org-default RBAC role
  7. 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:

ScenarioWhenMechanism
Project createA new project is created under a BUInline: Go CreateProject handler adds all current BU members synchronously
User assignA user is assigned to an existing BUAsynchronous: Temporal BackfillBusinessUnitMembership adds the user to all live BU projects
Project moveAn existing project’s BU is changed and the admin opts inOpt-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

  1. Admin clicks Remove next to a member in the Manage Members dialog
  2. A confirmation dialog appears with an opt-in checkbox: “Also remove them from this BU’s projects”
  3. If unchecked (default): only the business_unit_members row is deleted — the user remains on all projects
  4. If checked: the remove_from_projects=true query param is sent to DELETE /v1/orgs/{orgId}/business-units/{businessUnitId}/members/{userId}, triggering a Temporal PruneBusinessUnitMembership workflow
  5. 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:

  1. Select a new BU from the Business Unit dropdown
  2. An “Add this BU’s members to the project” checkbox appears (only when the BU changed to a different non-null value)
  3. If checked, sync_business_unit_members: true is included in the PUT /v1/orgs/{orgId}/projects/{projectId} request body
  4. The Go API’s UpdateProject handler runs the same inline add-loop as CreateProject, 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:

ColumnDescription
NameUnit name (editable inline)
StatusActive or Archived badge
CreatedCreation date
ActionsEdit, 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

MethodPathDescription
GET/v1/orgs/{orgId}/business-unitsList (?include_archived)
POST/v1/orgs/{orgId}/business-unitsCreate
PUT/v1/orgs/{orgId}/business-units/{businessUnitId}Rename
DELETE/v1/orgs/{orgId}/business-units/{businessUnitId}Soft-delete
GET/v1/orgs/{orgId}/business-units/{businessUnitId}/membersList members (with role display)
POST/v1/orgs/{orgId}/business-units/{businessUnitId}/membersAssign 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_admin or organisational_manager role (enforced via authz.KeyBusinessunitsManage).
  • Tenant isolation — All queries include AND org_id = $N. The Go handler derives org_id from 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_archived flag.
  • 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_roles set.

Key Files

FilePurpose
backend/db/migrations/20260710171503_business_unit_members.sqlbusiness_unit_members join table migration
backend/db/queries/business_units.sqlsqlc queries: CRUD + soft-delete
backend/db/queries/business_unit_members.sqlsqlc queries: assign/remove/list BU members
backend/api/internal/businessunit/handler.goHTTP handlers for CRUD + member assign/remove/list
backend/api/internal/businessunit/service.goBusiness logic, pgtype conversions, Temporal client
backend/api/internal/businessunit/members_service.goMember assign/remove/list + workflow dispatch
backend/workers/workflows/business_unit_membership.goTemporal backfill + prune workflow definitions
backend/workers/activities/business_unit_membership.goTemporal activities: add/remove user from BU projects
apps/web/src/actions/business-units.tsBFF server actions: BU CRUD with Zod validation
apps/web/src/actions/business-unit-members.tsBFF server actions: assign/remove/list BU members
apps/web/src/app/org/settings/_components/business-units-tab.tsxCRUD UI with inline editing + Members action
apps/web/src/app/org/settings/_components/bu-members-dialog.tsxManage-members dialog (assign picker + remove AlertDialog)
apps/web/src/app/admin/_components/project-detail.tsxProject edit form (BU selector + sync checkbox)