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)returnstruefor the owner, else checksuser.permissions.includes(perm). TheAgencyPermissionGatecomponent overlays a "Permission Required" notice rather than hiding the screen. - Backend: controllers call
general_helper.hasAgencyPermission(user, perm)and return403if 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 value | Label | Grants |
|---|---|---|
agency.add_client_subscription | Add Client / Subscription | Create client companies and subscriptions (gates create/edit company, GHL create, package assign) |
agency.disable_clients | Disable Clients | Archive or disable client companies |
agency.impersonate_clients | Impersonate Clients | "Login as Owner" into a client account |
agency.subscription_payments | Subscription & Payments | Manage the agency subscription + payment methods (gates changePackage, payment screens) |
agency.manage_packages | Manage Packages | Create/edit client packages & allocations (gates the /agency/packages routes) |
agency.manage_settings | Agency Settings | Agency 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
| Action | Repository fn | Endpoint |
|---|---|---|
| Load one | getStaffMember(id) | GET /agency/staff/:id |
| List | getStaffMembers() | GET /agency/staff |
| Update (name/phone/permissions/status/password) | updateStaffMember(id, data) | PUT /agency/staff/:id |
| Delete | deleteStaffMember(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.
| Action | Repository fn | Endpoint | What it does |
|---|---|---|---|
| List eligible self-account members | getSelfAccountMembers() | GET /agency/staff/self-account-members | Members of the agency_self_account company, flagged already_linked |
| Migrate selected members → staff | migrateSelectedMembers(ids) | POST /agency/staff/migrate-selected | Clones each member into an agency_staff user (copies hashed password), links both ways; skips already-linked |
| Create a member for a staff user | createMemberForStaff(staffId) | POST /agency/staff/create-member | Creates 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_staffincluded). 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_idfirst, then removes the staff user (the linked member account remains). - Migration is idempotent. Already-linked members are skipped; if an
agency_staffalready 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-updatedsocket 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', fieldsagency_id,permissions[],linked_user_id(UserModel)