Growth Access management is a permission-administration screen in Signal (Power Digital's internal BI web app) at route /growth/access. It is where delegated Growth managers and Sauron super-admins decide who can see the sensitive Growth section (the People tab with salary/payroll data and the Performance Radar tab), what each person can do there, and which divisions/departments their data is limited to. It answers: "Who can see Growth data in Signal, exactly which slices of the company can they see, and who granted it?" It shows no revenue or performance numbers itself. All grant and capability records are Signal's own records (entered by users in Signal, stored in Signal's Supabase tables growth_access_grants and growth_access_capabilities); the division and department vocabulary offered in the picker originates in Nova (Power Digital's HR/ops platform), via the same employee dataset the Growth People tab shows.
Field | Value |
Route | /growth/access |
Access | GrowthManageRoute — requires the canManageGrowthAccess capability (delegated "Manage Growth access" flag OR Sauron super-admin), PLUS a second-factor unlock password checked by the backend on every management request |
Nav section | Access ("Growth Access" link, next to the Gandalf-only "General Access" link) |
Primary data sources | Signal's own records (grant rows, capability flags, registered-user list — all entered/managed in Signal); Nova (division + department names on employee records, used only as picker vocabulary) |
Writes | PUT /api/growth/access — inserts/soft-revokes rows in growth_access_grants and upserts/deletes the row in growth_access_capabilities (both Signal's own records) |
Key code refs | src/views/GrowthAccessView.tsx; src/views/GrowthAccessPanel.tsx; src/App.tsx (GrowthManageRoute, Access nav section); src/services/api.ts (getGrowthAccessList, setGrowthAccess, x-growth-key header logic); src/utils/growthDepartments.ts; server/index.ts (requireGrowthManage, GET/PUT /api/growth/access, /api/growth/my-access); server/growth-access.ts (pure scope/capability helpers); supabase/migrations/20260701_growth_access_grants.sql, 20260702_growth_access_department_scope.sql, 20260703_growth_access_capabilities.sql |
Last verified | staging @ 98df0c4, verified 2026-07-07 |
Purpose
The Growth section of Signal (the People tab and the Performance Radar tab) exposes the most sensitive data in the app: per-employee salary/payroll, compensation history, performance signals, and manager feedback. Growth Access management exists so that access to that data is (a) granted per person, (b) narrowed to only the divisions — and for three large divisions, only the departments — the person legitimately needs, (c) auditable (every grant records who granted it and when, and revocations are soft-deletes so history is never lost), and (d) delegable, so a small set of Growth managers can administer access without needing Signal admin or Gandalf roles.
Before July 2026 this access was a hardcoded email allowlist in the code; the Growth Access management screen replaced that with database-backed grants managed entirely in the Signal UI (migration 20260701_growth_access_grants.sql seeded the 8 previously-allowlisted emails as all-division grants so nobody lost access on deploy).
Access & visibility
Who can open the Growth Access management screen at /growth/access (verified staging @ 98df0c4):
Route guard: the route is wrapped in GrowthManageRoute (src/App.tsx), which allows only users where canManageGrowthAccess is true and redirects everyone else to /summary. Gandalf (Signal's general super-admin tier) is deliberately NOT auto-granted here — Growth access management is its own permission, separate from other admin roles.
Nav link: "Growth Access" sits in the Access nav section, flagged manageGrowthOnly, so it appears only for users with canManageGrowthAccess. The Access section also contains "General Access" (/users, Gandalf-only, for Signal's general roles) — the two are independent permission systems.
How canManageGrowthAccess is resolved: on sign-in, src/context/AuthContext.tsx calls GET /api/growth/my-access; the backend reads the user's row in the growth_access_capabilities table (Signal's own records) and returns canManageAccess. Sauron super-admins (a hardcoded 4-email list in server/index.ts SUPER_ADMIN_EMAILS, code constant as of Jul 2026: john@, jaime.diaz@, laura.bejarano@, lily.loyer@ powerdigitalmarketinginc.com) always get canManageAccess = true regardless of any database row.
Second factor — unlock password: opening the panel prompts for an unlock password. It is validated against the GROWTH_UNLOCK_KEY environment variable on the backend host (Render) — the password is never in the frontend bundle. The frontend stores it in sessionStorage under the key signal_growth_admin_key and sends it as the x-growth-key header on every /api/growth/access* request; the backend middleware requireGrowthManage rejects requests without the correct key (HTTP 403, code UNLOCK_REQUIRED). A wrong/expired stored key drops the user back to the password prompt. If GROWTH_UNLOCK_KEY is not set on the server (local dev), no password is required.
Backend enforcement: both /api/growth/access endpoints run requireAuth (Supabase JWT) + requireGrowthManage (Sauron OR can_manage_access capability, AND the unlock key). The gate is server-side, not just a hidden screen.
Important asymmetry: being able to MANAGE Growth access does not by itself grant any Growth DATA access. A Sauron or delegated manager who wants to see the People tab must hold their own division grant plus the can_view_people capability, exactly like anyone else.
Data sources & lineage
Growth Access management displays no business metrics — everything on screen is either an access record (Signal's own records) or picker vocabulary (Nova).
On-screen element | Origin system | Code path | Refresh | Caveat |
"Select a user…" dropdown (when adding access) | Signal's own records — the app_users table (users registered/provisioned in Signal) | GrowthAccessView.tsx reads app_users (email, full_name) directly via the Supabase client | Loaded once when the view mounts | Only people who exist in Signal's app_users table can be picked; users who never signed into Signal will not appear |
Grants table — User column, division/department chips | Signal's own records — active rows (revoked_at IS NULL) in growth_access_grants, entered by Growth managers in this screen | GET /api/growth/access → Supabase REST (service-role key) → resolveScopeFromGrants in server/growth-access.ts | Fetched on unlock and re-fetched after every save/revoke | Soft-revoked rows are excluded from display but retained in the table as an audit trail |
Grants table — capability chips ("People", "Performance Radar", "Manage access") | Signal's own records — growth_access_capabilities (one row per user), entered in this screen | Same GET /api/growth/access call → resolveCapabilities in server/growth-access.ts | Same as grants | A user with a division grant but NO capabilities row shows "People" by default (backward-compatibility rule in resolveCapabilities) |
Division checkbox list in the editor | Nova — the division names (novaDivision) on employee records, via the cached Growth People dataset | GET /api/growth/access → visibleDivisionsFromPeople(getGrowthPeopleCached()) in server/index.ts / server/growth-access.ts | People dataset: fresh for 15 min, served stale up to 24 h with background refresh (code constants as of Jul 2026); a Supabase snapshot backs cold server starts | Excludes external contractors, blank divisions, and placeholder names — the picker only ever offers divisions actually visible in the People tab. If the People dataset has never been built, the list is empty ("No divisions found yet — open the People tab once to warm the data") |
Department checkboxes (only for Shared Services, G&A, Healthcare) | Code constant in the app (GROWTH_DEPARTMENT_DIVISIONS in src/utils/growthDepartments.ts, as of Jul 2026), validated at save time against the live department names on Nova employee records | Frontend list from growthDepartments.ts; PUT /api/growth/access re-validates each department against departments present in the People dataset | Static list; validation vocabulary follows the People dataset refresh | A department name that drifts out of sync with Nova is rejected at save time ("Unknown department(s) for …") rather than silently matching nobody |
Error/permission messages ("Growth unlock key required…", "Growth access management permission required") | Signal backend responses | requireGrowthManage in server/index.ts | Per request | A 403 clears the stored key and re-prompts for the password |
Metrics & definitions
The Growth Access management view has no numeric metrics. Its on-screen concepts are:
Scope (division/department chips): WHICH data slices a user may see in the Growth tabs. Values: "All divisions" (a single grant row with division = NULL), a list of whole divisions (one row each, department = NULL), or — only for Shared Services, G&A, and Healthcare — a division narrowed to specific departments (one grant row per department). Basis: active rows in growth_access_grants (Signal's own records), collapsed by resolveScopeFromGrants. Any all-division row wins outright; a whole-division row wins over department rows for that division. Every Growth data endpoint (/api/growth/people, etc.) filters its response server-side to the caller's scope.
"No division data": the user has capabilities (for example manage-only) but zero active division grant rows. Their Growth viewing tabs would show nothing.
Capability "People" (can_view_people): the Growth → People tab is visible to this user. Defaults ON for a new grant in this screen; if a user has a division grant but no capabilities row at all, People visibility defaults to true (backward compatibility with pre-capability grants). Origin: growth_access_capabilities (Signal's own records).
Capability "Performance Radar" (can_view_radar): the Growth → Performance Radar tab is visible. Strictly opt-in (default false, no backward-compat default). Origin: growth_access_capabilities.
Capability "Manage access" (can_manage_access): the user may open /growth/access and grant/edit/revoke Growth access for others (across ALL divisions — management is not division-scoped). Does NOT grant any data visibility. Origin: growth_access_capabilities; Sauron super-admins have it implicitly.
Granted by / granted at: audit columns on each grant row — the email of the manager who saved the grant and the timestamp. Rows created by the July 2026 migration carry granted_by = 'system:migrated-from-allowlist'.
Note on capability vs. scope enforcement: can_view_people / can_view_radar gate tab VISIBILITY (frontend route guards PeopleRoute / RadarRoute); the division scope is what actually filters the data server-side. The two Growth viewing tabs share the same data endpoints, so a Radar-capable user sees the same scope-filtered dataset a People-capable user does.
How it's used
Onboarding a new Growth data viewer. A division president needs to see salary and performance data for their own division. A Growth manager opens /growth/access, enters the unlock password, clicks "+ Add access", picks the person's email from the registered-user dropdown, leaves "See People tab" checked, ticks the person's division (for example "Creative"), and saves. Within about 2 minutes (server-side permission cache TTL, code constant as of Jul 2026) the person can open Growth → People and sees only their division's rows.
Narrowing access to specific departments. An ops lead should see only the R&D and CRO departments inside Shared Services. The manager edits their grant, ticks "Shared Services", unticks "All departments", and ticks R&D and CRO. The People tab then shows only employees whose Nova department is R&D or CRO within Shared Services. (Department narrowing is available only for Shared Services, G&A, and Healthcare; every other division is granted whole.)
Unlocking the Performance Radar tab for a leader. Performance Radar is hidden by default even for existing People viewers. The manager edits the person's grant and checks "See Performance Radar tab". The tab appears in the person's Growth nav after their next sign-in or app reload.
Delegating access administration. The owner (Sauron) grants a People-team lead the "Manage Growth access (assign access to others)" capability with no divisions at all — the lead can now administer everyone's Growth access from /growth/access (after entering the unlock password) without seeing any salary data themselves.
Offboarding / revoking. When someone changes roles, the manager clicks "Revoke" on their row and confirms. All their division grants are soft-revoked and their capabilities row is removed; they disappear from the access table and lose the Growth tabs. The revoked grant rows remain in the database as an audit trail of who could see salary data and when.
Auditing who can see salary data. Leadership asks "who can currently see Healthcare compensation?" The manager opens /growth/access and reads the table: every user with "All divisions" or a Healthcare chip, with the capability chips showing which tabs they actually have.
What users can change here
Everything on this screen writes to Signal's own records via PUT /api/growth/access (gated by requireGrowthManage: manage capability or Sauron, plus the x-growth-key unlock password):
Grant / edit a user's division scope. Where it lands: rows in the growth_access_grants table (Signal's own Supabase records). The save is a full reconcile of that user's scope: rows no longer in the requested scope are soft-revoked (revoked_at stamped — never hard-deleted), new rows are inserted with granted_by = the saving manager's email. "All divisions" collapses to a single division = NULL row; a whole division is one (division, NULL) row; each specific department is its own row. The server validates every division and department against the live Nova People vocabulary and rejects unknown names. Revocations are deliberately applied BEFORE insertions so a mid-save failure errs toward LESS access, never more.
Set a user's capabilities (People / Performance Radar / Manage access). Where it lands: the user's single row in growth_access_capabilities (upserted with updated_by/updated_at). If the save leaves the user with no scope and no capabilities, the capabilities row is deleted so they drop off the access list entirely.
Revoke all access. The "Revoke" button sends scope [] with all three capabilities false — soft-revokes every grant row and deletes the capabilities row.
Side effects of any save: the backend invalidates its in-memory permission cache for that email immediately; other server instances pick the change up within the 2-minute cache TTL (code constant as of Jul 2026). The affected user's own frontend learns of capability changes the next time AuthContext calls /api/growth/my-access — i.e., on their next sign-in or full app reload; already-open tabs are additionally re-checked server-side on every data request, so revoked data access takes effect on their next API call even without a reload. Validation guardrails in the panel: a viewing capability (People or Radar) cannot be saved without at least one division (the tabs would be empty and the tab password gate could not even validate); a manage-only grant legitimately needs no divisions.
There is no other write on this view. It never modifies Nova, payroll systems, or any revenue data.
Caveats & data quality
The unlock password is a shared secret, stored only as the GROWTH_UNLOCK_KEY environment variable on the backend host (Render). If it is unset, the key check passes with no password (intended for local dev only). Rotating it requires a backend env change; there is no in-app rotation. For the management screen the entered key persists in sessionStorage for the browser session; for the Growth viewing tabs (People, Performance Radar) the same password is held in memory only, cleared on page reload and after ~5 minutes in a backgrounded tab (code constant BACKGROUND_RELOCK_MS in src/services/growthUnlock.ts as of Jul 2026).
Permission changes are not instant everywhere. The backend caches each user's scope and capabilities for 2 minutes (GROWTH_SCOPE_TTL_MS, code constant as of Jul 2026). Saves invalidate the cache on the instance that handled the save, but a multi-instance deployment relies on the TTL. The affected user's nav/tab visibility updates only when their frontend re-fetches /api/growth/my-access (sign-in or reload).
Resilience fallback (added Jul 2026): if the Supabase lookup for a user's scope or capabilities transiently fails (network blip, non-JSON response), the backend serves the last-known cached value — even if expired — rather than treating the failure as "no access". A genuine "no grants" result is still a denial.
The division picker depends on a warm People dataset. The offered divisions are derived from the Growth People dataset (built from Nova employee records plus payroll worker fetches; the build takes 30–90 seconds cold). If no cache or snapshot exists yet, the picker shows "No divisions found yet — open the People tab once to warm the data."
Department lists are code constants (src/utils/growthDepartments.ts, as of Jul 2026): only Shared Services (8 departments), G&A (10), and Healthcare (13) can be department-scoped. Adding a new department-scoped division requires a code change in both src/utils/growthDepartments.ts and server/growth-access.ts (DEPARTMENT_SCOPED_DIVISIONS). Department names that drift from Nova's are rejected at save time.
Sauron list is hardcoded (SUPER_ADMIN_EMAILS in server/index.ts, mirrored in src/context/AuthContext.tsx, code constant as of Jul 2026) so the owner cannot be locked out by database state.
The grants and capabilities tables have RLS enabled with NO policies — they are readable/writable only through the Signal backend with the service-role key, never with the public anon key in the frontend bundle.
Capability flags gate tab visibility only, not data. Both Growth viewing tabs share the same scope-filtered endpoints; there is no Radar-only data as of staging @ 98df0c4. A server-side note in server/index.ts warns that any future Radar-only endpoint must add its own guard.
Concurrent saves: a duplicate insert racing another manager's identical grant returns HTTP 409 from the database's unique active-grant index and is treated as a no-op (desired end state already reached). Distinct concurrent edits last-write-wins per row.
Refresh cadence of the underlying Nova employee data feed into the People dataset: Not defined in the app repo (the app reads whatever the warehouse copy of Nova currently holds).
Related views
Growth → People (/growth/people) — the salary/payroll/people dashboard whose visibility (can_view_people) and data scope this screen controls.
Growth → Performance Radar (/growth/performance-radar) — the performance view whose visibility (can_view_radar) this screen controls; data is filtered by the same division scope.
Access → General Access (/users) — Signal's general role administration (member/team-lead/admin/Gandalf flags in app_users). Completely separate permission system; Gandalf-gated; does not affect Growth access.
Employees (/employees) — general employee views; not gated by Growth access.
FAQ
Q: Who can open the Growth Access screen? A: Anyone with the "Manage Growth access" capability — granted on this same screen by an existing Growth manager — plus the four hardcoded Sauron super-admins (as of Jul 2026: john@, jaime.diaz@, laura.bejarano@, lily.loyer@ powerdigitalmarketinginc.com). Signal Admins and Gandalfs do NOT automatically get in; Growth access management is deliberately its own permission. On top of the account permission, everyone must enter the Growth unlock password.
Q: What is the password the Growth Access screen asks for, and where do I get it? A: It is a shared unlock password configured as the GROWTH_UNLOCK_KEY environment variable on Signal's backend — a second factor on top of your account permission. It is not stored anywhere in the app UI; get it from the Signal team (AI/ML team, who own Signal). The same password also unlocks the Growth People and Performance Radar tabs. On the management screen it is remembered for your browser session; on the viewing tabs it is asked again after every page reload and after ~5 minutes with the tab in the background.
Q: I granted someone access — why don't they see the Growth tabs yet? A: Two delays are expected: the backend caches permissions for about 2 minutes, and the person's own browser only re-checks which tabs to show when they sign in or fully reload Signal. Ask them to reload; if the tabs still don't appear after a couple of minutes, confirm on /growth/access that their row has the "People" (and/or "Performance Radar") capability chip AND at least one division chip — a capability with no division scope shows nothing.
Q: Why is the division list empty when I try to add access ("No divisions found yet")? A: The division picker is built from the Growth People dataset (employee records originating in Nova). On a freshly restarted server that dataset may not be built yet. Open the Growth → People tab once (which triggers the build, 30–90 seconds cold), then return to Growth Access.
Q: Can I limit someone to just one department instead of a whole division? A: Only for three divisions — Shared Services, G&A, and Healthcare (a code-defined list as of Jul 2026). Tick the division, untick "All departments", and tick specific departments. Every other division can only be granted whole. If you need department scoping for another division, that is a code change — ask the Signal team.
Q: Does giving someone "Manage Growth access" let them see salaries? A: No. Manage access only lets them open the Growth Access screen and administer grants for others. Seeing any Growth data (People, Performance Radar) requires their own division grant plus the corresponding tab capability. This is also true for the Sauron super-admins — they can always manage, but must grant themselves a scope to see data.
Q: What happens when I click Revoke — is the history deleted? A: No. Revoke soft-deletes: every grant row gets a revoked_at timestamp and the person's capabilities row is removed, so they lose the tabs and drop off the list — but the grant rows stay in Signal's database permanently as an audit trail of who could see salary data, who granted it, and when.
Q: Why does a user show "People" access when I never checked that box for them? A: Backward compatibility. Before per-user capabilities existed (July 2026), having any division grant implied People access. A user with a division grant but no explicit capabilities row still defaults to People-visible. Edit their access and save with "See People tab" unchecked to create an explicit row that turns it off.
Q: Is the Performance Radar tab on by default for people with Growth access? A: No. Performance Radar is strictly opt-in — hidden by default for everyone, including users who already had People access. It must be explicitly checked ("See Performance Radar tab") on this screen.
Q: What's the difference between "General Access" and "Growth Access" in the Access nav section? A: General Access (/users) manages Signal's overall roles — member, team lead, admin, Gandalf — stored in the app_users table, and is only editable by Gandalf super-admins. Growth Access (/growth/access) manages only the Growth section (People, Performance Radar, and who may manage Growth access), with its own tables, its own delegated managers, and an extra unlock password. Holding a role in one system grants nothing in the other.
Q: Why can't I find the person I want to grant access to in the dropdown? A: The dropdown lists users from Signal's own registered-user table (app_users), which is populated when someone signs into Signal with their Power Digital Google account. If the person has never signed in, have them log in to Signal once, then add them.
