Workforce privacy: aggregates only, encrypted franchisee names
Workforce is the one HQ page where the underlying subject, people, makes the privacy question sharper than almost anywhere else on the platform. It is reasonable to wonder whether a page that repor…
On this page
- The line this article holds
- Two different things are being protected here
- How the schema-level wall works
- The one legitimate bridge: the nightly aggregate-refresh cron
- Why the franchisee's name gets encrypted at all
- What a "group shared DEK" actually is
- Where Workforce lives, briefly
- Work Style (DISC) gets the same two-layer treatment
- Empty states you'll see
- Why this design, not a lighter one
- Data sources
- Related articles
The line this article holds
Workforce is the one HQ page where the underlying subject, people, makes the privacy question sharper than almost anywhere else on the platform. It is reasonable to wonder whether a page that reports headcount, safety incidents, and work-style mix means HQ is reading a franchisee's employee roster directly. It does not, and the reason is structural, not a policy promise. Every number Workforce shows comes from exactly two tables, the network data and the network data, and the code that reads them, the product and the product, is written against a client called hqAdmin that has no path to schema the private layer at all.
If you want the full tour of the Workforce page itself, the hero, Understaffed, Safety Risk, Bench Depth, Team Reviews, Work Style, and Team by Franchisee, read HQ Workforce: network staffing intelligence overview first. This article is the trust layer underneath it, the same relationship What HQ sees: the network privacy boundary has to the rest of HQ's product. It answers two separate questions that get conflated: what happens to an individual staff member's information, and what happens to the franchisee's own name.
Two different things are being protected here
1. Individual staff members: no record ever crosses. A technician, an admin coordinator, a branch manager, none of them has a name, a role, a pay figure, a contact detail, or a performance history anywhere on HQ. That is not because the interface hides a field that exists in the data HQ can see. It is because the tables HQ reads never contain that field in the first place. the network data stores headcount totals, role-mix totals, tenure averages, onboarding and offboarding counts, incident counts, and dates, one row per franchisee. the network data stores profiled-member counts and DISC primary-style counts, one row per franchisee. Neither table has a column for an employee's name, ID, or individual score, because the nightly rollup that populates them never writes one. There is no drill-down, no admin override, no "advanced view" anywhere in Workforce that reaches past the row for a franchisee into the roster underneath it.
2. The franchisee's own name: shown, but encrypted at rest. Unlike an employee, a franchisee's business name is meant to be visible on this page, it is the label on every tile ("Riverside Restoration is 3 below target," for example). That is not a privacy leak; it is the same visibility a company has into its own branch offices, or a franchise network has into its own membership roster. But because that name is still an identifying piece of business data sitting in a network-layer rollup table, Verinode encrypts it at rest as an added layer of defense, on top of, not instead of, the schema-level wall described below. That encryption, and exactly what it does and doesn't protect against, is the rest of this article.
Note
If you're looking for a person-level roster, compensation, certifications, or individual DISC results behind these numbers, that detail lives on the franchisee's own Verinode IQ account and belongs to them. Workforce's job is to show where staffing risk is concentrated across the network, never to be a substitute HR system for a business you don't employ.
How the schema-level wall works
hqAdmin (exported from the product) is not a Supabase client with a row-level-security policy that could theoretically be misconfigured. It's a direct Postgres connection authenticated as a database role, hq_app, that has explicit grants on the network-layer rollup tables Workforce and the rest of HQ read, and a hard, schema-level REVOKE on the private layer. A query against the private layer from this connection returns "permission denied for schema pii" at the database layer itself, the same error any misconfigured client would get, because the database refuses the connection regardless of what the application code asks for.
That's layer one of four:
- The database role.
hq_app's schema-level revoke on the private layer, enforced by Postgres, not by application logic. - The client binding. Every file under
lib/franchise/,lib/hq/, and the HQ route groups importshqAdminand nothing else. - A build-time lint. An automated check (the product) fails the build if any HQ-side file imports a private-layer client directly.
- An audit trail. Cross-schema access, the one legitimate bridge described next, is logged.
The one legitimate bridge: the nightly aggregate-refresh cron
Neither table populates itself, and Workforce's own page code never writes to either table. A separate process, the nightly refresh cron, is the sanctioned, documented crossing point between a franchisee's private team and DISC records and HQ's network view. Once a night, for each franchisee on a network's roster, it reads that franchisee's own private team, role-slot, incident, and DISC records, computes the counts and averages described above, and upserts one row per table into the network layer. That cron runs with legitimate cross-schema access because producing network-layer summaries from private-layer truth is its entire job; it's the one function explicitly excluded from the build-time lint, not a loophole in it. If a single franchisee's rollup fails to compute or write on a given night, the cron logs the failure and moves to the next franchisee rather than blocking the rest of the network's refresh, and that franchisee's row simply keeps whatever it last had.
Why the franchisee's name gets encrypted at all
Everything else in these two tables, headcount counts, tenure averages, incident counts, DISC tallies, is a number. Numbers alone don't identify a business if a table were ever exposed outside its normal read path (a database export, a misdirected backup, a compromised credential that only reaches as far as the network layer). The franchisee's name is the one column in both tables that does. Vault Slice B Half 2 closes that gap: the aggregator cron writes the franchisee's name in two forms on every row it upserts, a plaintext franchisee_name column, and a franchisee_name_ciphertext column holding the same value encrypted with AES-256-GCM under the network's own shared group DEK (data encryption key). Workforce's read code decrypts the ciphertext column back to a name before it ever reaches a tile.
Note
This is defense-in-depth for a business-level identifier that Workforce is meant to display, not a zero-knowledge scheme that hides franchisee names from HQ. The whole point of the page is that you see which franchisee a tile is about. What the encryption protects against is a table falling into the wrong hands through some path other than the product itself, not against a signed-in HQ admin, or against Verinode's own server infrastructure, seeing the name. See the note on the shared DEK's trust model below for exactly what that does and doesn't cover.
What a "group shared DEK" actually is
Every network has its own DEK, a randomly generated encryption key scoped to that one group. It's stored, wrapped, in the network data, keyed by (group_id, dek_label, wrap_method, principal_id). There are two variants in the platform's Vault design, and Workforce uses the weaker of the two on purpose:
admin_only, wrapped once per HQ admin under that admin's own personal Vault Key. There is no KMS-held copy of this variant anywhere; Verinode's infrastructure structurally cannot decrypt it without an authorized admin's active, unlocked session.shared, the variant Workforce's franchisee names use, wrapped once under a KMS master key (VAULT_ROOT_KEYin the deployment environment) instead of under any one admin's personal key. That's a necessary tradeoff, not an oversight: the aggregator cron writes these rows every night with no admin logged in to unwrap anadmin_onlykey for it, so the column it writes to has to use a key the server itself can unwrap on demand. The honest way to describe the guarantee: it protects the name from anyone who can read the the network layer schema's raw bytes but not the deployment's KMS key, but Verinode's own infrastructure, holding that key, can decrypt it, and so can any signed-in HQ admin's session, which is exactly what lets the page render the name at all.
Reading a name back out, concretely, in the product and the product: the query first pulls every row's franchisee_name and franchisee_name_ciphertext for the group. If at least one row actually has ciphertext, it unwraps the group's shared DEK once via KMS (unwrapSharedDekFromKms), then decrypts each row's ciphertext with that DEK. If a row has no ciphertext yet, or decryption throws for any reason, the code logs a warning and falls back to that row's plaintext franchisee_name column rather than showing a broken tile. That fallback is a deliberate resilience choice, the same "dual-write, fail open toward availability" posture the platform uses elsewhere for encrypted columns: a name always renders, and a rollup that predates the ciphertext column, or a transient KMS hiccup, degrades to plaintext for that one field instead of breaking the page.
Where Workforce lives, briefly
Open Workforce from the HQ sidebar, in the always-open Network group, at hq.verinode.ai/franchise/team (also reachable at the shorter /team alias). The page opens on a hero panel (network headcount, avg tenure, open incidents, OSHA 36mo) followed by six horizontal tile rows: Understaffed, Safety Risk, Bench Depth, Team Reviews, Work Style, and Team by Franchisee. Every tile in every row carries a franchisee's name, decrypted the way described above, and a count. None carries an individual's name. For what each row measures, how it's sorted, what each tile's label and sub-line mean, and every empty state, see HQ Workforce: network staffing intelligence overview; this article stops at the boundary question.
Work Style (DISC) gets the same two-layer treatment
The Work Style card, the wide row between Team Reviews and Team by Franchisee, rolls up DISC work-style profiling. the network data stores, per franchisee, how many team members have been profiled and a count of how many people's primary style falls into each of the four DISC dimensions, D (Dominance), I (Influence), S (Steadiness), C (Conscientiousness). It does not store, and Workforce cannot show, any individual's actual assessment, their raw D/I/S/C intensities, which dimension is their primary, or which employee any of it belongs to. That detail lives entirely in your operator data, on the franchisee's own IQ account, and the aggregate-refresh cron only ever reads it to produce the tallies above, the same way it reads team and incident records to produce the headcount rollup. The franchisee's name on this card goes through the identical shared-DEK decrypt path described above.
Empty states you'll see
- Understaffed, Safety Risk, Bench Depth, Team Reviews: each row has its own plain-language empty state (documented in HQ Workforce: network staffing intelligence overview) that reads as "nothing qualifies" rather than "no data," when the underlying rollup has real rows but none clear that row's criteria.
- Work Style: "No work-style profiles across the network yet. As franchisees run the DISC profile with their teams, the aggregate mix appears here. HQ sees the distribution only, never an individual's results."
- Team by Franchisee / the hero, before any franchisee has registered team data: "Team data will appear as franchisees register their team members."
None of these states are the encryption layer failing. A franchisee with no ciphertext row yet (a rollup written before the ciphertext column existed, or one write cycle behind) still renders its name from the plaintext column; you would not see a broken tile or a missing name because of it.
Why this design, not a lighter one
A franchisee agreeing to run Verinode inside their business is trusting that the people who work for them, not just their financials, stay theirs to manage. Workforce exists so a franchisor, multi-location enterprise, or PE-backed leadership team can see where staffing risk is concentrated across a whole network, understaffing, safety incidents, thin coverage, overdue reviews, without that visibility ever becoming a window into who a specific employee is or how they scored on a personality assessment. The franchisee-level name is the one identifying detail on the page, and it's both schema-isolated from the pii-side records that produced it and encrypted at rest as a second, independent layer, so that a table dump or a leaked credential that only reaches the network layer still doesn't hand over a readable franchisee roster. That's the same discipline the rest of HQ runs on: franchisees own their data, and what flows up is a summary, never the record.
Data sources
Data sources
- 1.Network headcount, role mix, tenure, onboarding, safety, and review rollup (the network data). Verinode network rollup, computed nightly from each franchisee's own team and incident records.
- 2.Network work-style (DISC) rollup (the network data). Verinode network rollup, computed from each franchisee's own DISC profiling.
- 3.HQ Vault Slice B specification (group-shared DEK design). Verinode.
- 4.Verinode Data Use Policy. Verinode.