Skip to main content

Staff Management

Audience: Agency-success / Support / Management · Where in app: /agency/staff (list, create, edit) · Plan availability: Agency tier (verify specifics)

Staff management lets an agency owner invite team members into the agency portal and grant them a subset of capabilities. Staff are real users with user_type: 'agency_staff', scoped to one agency. There is no separate "roles" abstraction in the UI — access is controlled by a flat list of six toggleable permissions per staff member. Only the agency owner can manage staff.

What it does

  • Create a staff account with name, email, phone, password, and a set of permissions.
  • List / edit / delete staff members.
  • Toggle any of six agency permissions per staff member.
  • Migrate existing members from the agency's self-account company into agency staff (and the reverse: create a member account for a staff user) so one person can use both surfaces with one password.

How it works

Every staff endpoint first calls _verifyAgencyOwner: the caller must be user_type === 'agency' (the owner, not staff), and an agency must exist for them. So staff cannot manage other staff — staff management is owner-only by construction, independent of permissions.

A created staff user is user_type: 'agency_staff', linked to the agency via agency_id, parented to the owner (user_parent.parent_id), pre-verified, and assigned the generic member role record (the role carries no permissions — capabilities come from the permissions array). The owner always passes every permission check; staff are checked against their permissions array.

Permission enforcement is two-sided:

  • Frontend: appStore.agencyHasPermission(perm) returns true for the owner, else checks user.permissions.includes(perm). The AgencyPermissionGate component overlays a "Permission Required" notice rather than hiding the screen.
  • Backend: controllers call general_helper.hasAgencyPermission(user, perm) and return 403 if missing.

Source: frontend/src/views/agency/staff/ and backend/app/controllers/AgencyStaffController.js.

Configuration & options

The six agency permissions

Defined in frontend/src/views/agency/staff/Create.vue (and Edit). Staff get portal access by default; these toggles grant extra capabilities on top.

Permission valueLabelGrants
agency.add_client_subscriptionAdd Client / SubscriptionCreate client companies and subscriptions (gates create/edit company, GHL create, package assign)
agency.disable_clientsDisable ClientsArchive or disable client companies
agency.impersonate_clientsImpersonate Clients"Login as Owner" into a client account
agency.subscription_paymentsSubscription & PaymentsManage the agency subscription + payment methods (gates changePackage, payment screens)
agency.manage_packagesManage PackagesCreate/edit client packages & allocations (gates the /agency/packages routes)
agency.manage_settingsAgency SettingsAgency branding, white-label, GHL connect, configuration

Create staff (staff/Create.vue)

| Field | Required | Validation | | ------------------------ | -------- | -------------------------------------------------- | ------------------------------------------------------------------------- | ------ | | Full Name (first_name) | yes | required | string | | Email | yes | required | email; unique among agency/agency_staff (may reuse a member's email) | | Phone | no | — | | Password | yes | required | string | min:6 | | Permissions | optional | array of the six values; toggled in the side panel |

createStaffMember(data)POST /agency/staff.

Edit / delete

ActionRepository fnEndpoint
Load onegetStaffMember(id)GET /agency/staff/:id
ListgetStaffMembers()GET /agency/staff
Update (name/phone/permissions/status/password)updateStaffMember(id, data)PUT /agency/staff/:id
DeletedeleteStaffMember(id)DELETE /agency/staff/:id

On update the backend emits a socket event user-data-updated to user:<staffId> so the staff member's live sessions refetch their permissions immediately.

Member ↔ staff migration

A person who is both a team member (inside the agency's self-account company) and an agency staff user is kept in sync via a bidirectional linked_user_id.

ActionRepository fnEndpointWhat it does
List eligible self-account membersgetSelfAccountMembers()GET /agency/staff/self-account-membersMembers of the agency_self_account company, flagged already_linked
Migrate selected members → staffmigrateSelectedMembers(ids)POST /agency/staff/migrate-selectedClones each member into an agency_staff user (copies hashed password), links both ways; skips already-linked
Create a member for a staff usercreateMemberForStaff(staffId)POST /agency/staff/create-memberCreates the member counterpart in the self-account company and links it

UI: staff/MigrateModal.vue.

Behaviors & edge cases

  • Owner-only, always. Every staff route returns 403 to non-owner callers (agency_staff included). Staff cannot create/edit/delete other staff regardless of permissions.
  • Email scoping. A staff email must be unique among agency/agency_staff users but may match an existing member email — that's how a person ends up linked across both surfaces. Auto-linking happens on create (_autoLink).
  • Deleting a staff member unsets the linked member's linked_user_id first, then removes the staff user (the linked member account remains).
  • Migration is idempotent. Already-linked members are skipped; if an agency_staff already exists for the email, the two are retroactively linked instead of duplicated.
  • Permissions are additive only. There's no "deny" — absence of a permission = no access. The owner is implicitly all-permissions.
  • Live permission changes. Because of the user-data-updated socket emit, a staff member sees permission changes without re-logging in.

Plan & limits

  • No documented cap on staff count (verify whether the agency tier limits seats).
  • Staff are part of the agency portal tier; they don't consume client sub-account slots.

Technical implementation

  • Views: frontend/src/views/agency/staff/{List,Create,Edit,MigrateModal}.vue
  • Routes: frontend/src/router/agency.js (agency-staff, -create, -edit)
  • Repository: frontend/src/repositories/agency/staff.js
  • Controller: backend/app/controllers/AgencyStaffController.js — see backend service
  • Permission helpers: appStore.agencyHasPermission (frontend/src/store/app.js), general_helper.hasAgencyPermission (backend), components/agency/AgencyPermissionGate.vue
  • Model: users with user_type: 'agency_staff', fields agency_id, permissions[], linked_user_id (UserModel)