Agency Dashboard
Audience: Agency-success / Sales / Support · Where in app: /agency (route name agency-dashboard) · Plan availability: Agency tier (verify specifics)
The dashboard is the landing screen of the agency portal. It answers two questions at a glance: how many client sub-accounts do I have left to install? and how do I add the next one? It is intentionally light on analytics — the heavy lifting (per-client metrics, revenue) lives in the companies, subscriptions, and admin revenue screens. Think of it as the agency owner's capacity gauge plus a launcher.
What it does
- Shows the agency's sub-account capacity: total slots, used, and available.
- Offers three setup paths for adding a client: install on the agency's own website (self-account), connect a GoHighLevel agency account, or create a standard client account.
- Surfaces a welcome / empty state for brand-new agencies that have bought no sub-accounts yet.
- Provides quick links to the full companies list and the create-company flow.
- Handles the GHL OAuth return — when GoHighLevel redirects back after an install, the dashboard reads the callback query params and routes the user on.
How it works
The screen is driven entirely by the agency object in the app store (appStore.agency), which is loaded with the user session. There is no dedicated dashboard metrics endpoint — every number on this page is read from fields already on the agency record:
total_sub_accounts— slots granted by the agency's current package.available_sub_accounts— slots not yet consumed by a client company.- Used is computed on the client as
total − available.
If total_sub_accounts === 0, the page renders the empty "Welcome / buy sub accounts" state instead of the main content, with a single CTA to /agency/buy-sub-account (which redirects to the subscription plans tab — see Subscriptions).
The GHL connection state is read from agency.go_high_level.access_token (truthy = connected). The agency's own self-account state is read from appStore.company._id && appStore.company.agency_self_account.
Source: frontend/src/views/agency/Dashboard.vue.
Configuration & options
Sub-account capacity card
| Element | Source | Notes |
|---|---|---|
| Used Accounts | total_sub_accounts − available_sub_accounts | Dark teal segment of the bar |
| Available Accounts | agency.available_sub_accounts | Light teal segment |
| Total Accounts | agency.total_sub_accounts | Sum line |
| Progress bar | computed percentages | Labels hide below ~15–25% width |
Setup options card
| Option | Visible when | Action | Permission gate |
|---|---|---|---|
| Install on your own website | always | Opens "Setup own sub-account" modal (email field) → setupOwnCompany({ email }) → POST /agencies/setup-company; on success swaps the session to the new company and pushes to onboarding | Unlink button only for agency owner (appStore.isAgencyOwner) |
| Connect with your GHL Agency Account | agencyHasPermission('agency.manage_settings') | Opens GhlInstallModal → installGhl() → POST /go-high-level/install-url/agency, then redirects to the returned GHL OAuth URL | Hidden for staff lacking agency.manage_settings |
| Create Standard Client Account | always | Router link to /agency/companies/create | — |
Each option shows a green check + an Unlink / Uninstall button once that path is already set up:
- Unlink self account →
unlinkSelfAccount(companyId)→PUT /company/unlink/:id(owner only). - Uninstall GHL →
uninstallGhl(agency._id)→POST /go-high-level/uninstall/agency(confirm dialog first).
Stats strip
Three cards: Total Sub-Accounts, Used Sub-Accounts, Available Sub-Accounts — all derived from the same three numbers above. No separate API call.
GHL callback handling (onMounted)
| Query params on return | Behavior |
|---|---|
integration=go-high-level&success=true&company_name=... | Redirect to agency-companies carrying the query (company-level install completed) |
integration=go-high-level&success=true (no company) | Toast "GoHighLevel connected", clear query params (agency-level install completed) |
Playground (public demo sandbox)
The agency portal grows two playground surfaces (routes /agency/playground, /agency/simple-playground → frontend/src/views/agency/{playground,simple-playground}/List.vue) for demoing the product to prospects with no login:
- Anonymous playground — public
/playground(views/playground/PlaygroundView.vue).POST /playground/init(rate-limited, fingerprinted, no auth) creates a temp user + company + subscription seeded with 100 AI credits and kicks off website scraping. Auto-expires:PlaygroundCompanyModel.expires_at= 24h. - Simple Playground (shareable) — setup wizard at
/simple-playground(agency-authenticated via?agency_create=1/?agency_edit=<id>), prospect link at/simple-playground/test?company_id=<id>. Never expires (expires_atholds a far-future sentinel). Share links are collaborative — anyone with the link can edit (GET|PUT /playground/shared-company). Renders white-labeled under the agency's branding/domain.
Mechanics (backend PlaygroundController + playgroundRoutes.js):
- Every save path (owner, agency, shared link) goes through one whitelist —
PLAYGROUND_SIMPLE_FIELDS/PLAYGROUND_NESTED_FIELDS(widget styling,ai_chat.*/ai_calling.*agent config, voice id/language,playground_redirect_url). - Welcome video:
POST /playground/agency/:id/video(streamed compression, replace/remove), with presets from the admin Default Videos library. - Review:
GET /playground/agency/:id/calls— paginated voice-call recordings + chat conversations per playground. - Convert to real account:
POST /playground/convert. - Agency management:
GET /playground/agency/list|stats, CRUD on/playground/agency/:id. - Super-admin:
/admin/playground(views/admin/playground/List.vue) — list + stats with a Credits column (remaining plan + purchased credits per playground;GET /playground/admin/list|stats).
Behaviors & edge cases
- Zero capacity = empty state. An agency that has subscribed but somehow has
total_sub_accounts = 0sees only the buy-sub-accounts CTA; the setup cards and stats are hidden. - Self-account setup re-authenticates.
setupOwnCompanyreturns a freshtoken,user,company, andsubscription; the store is replaced in place and the user is pushed to onboarding. If the onboarding route is missing it stays on the dashboard and invalidates theagencyquery. - Staff visibility. Staff without
agency.manage_settingsnever see the "Connect GHL" card. The "Unlink" self-account control is owner-only regardless of permissions. - Numbers can lag. Because the cards read the cached agency object, creating/removing a company elsewhere only updates here after
loadUserInfo()/ query invalidation. A hard refresh is the reliable resync.
Plan & limits
- The dashboard is part of the agency portal, which is its own subscription tier (verify exact plan name/pricing).
total_sub_accountsis granted by the agency's package; see Subscriptions and Packages. The dashboard itself imposes no limits — it only reports them.
Technical implementation
- View:
frontend/src/views/agency/Dashboard.vue - Route:
frontend/src/router/agency.js→ nameagency-dashboard, path/agency, layoutAgencyLayout.vue,allowedUserType: [AGENCY, AGENCY_STAFF] - Store:
frontend/src/store/app.js(agency,company,isAgencyOwner,agencyHasPermission) - Repositories:
frontend/src/repositories/agency/index.js(setupOwnCompany,unlinkSelfAccount,uninstallGhl,installGhl) - Backend:
AgencyController(setupCompany,unInstallGhl),GoHighLevelServicefor install URLs — see backend service and CRM integration subsystem - Model:
AgencyModel(backend/app/models/AgencyModel.js)