Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
7.7 KiB
Client Portal Board Visibility Model Correction Design
Date: 2026-04-15
Slug: client-portal-board-visibility-model-correction
Summary
The original Client Portal Board Visibility Groups feature was intended to make visibility groups client-scoped while using board IDs as tenant-scoped visibility controls. The current implementation appears to assume that boards themselves belong to a client via boards.client_id, but the real schema does not model that relationship.
This design corrects the implementation to match the intended product model:
- visibility groups belong to a client
- contacts are assigned zero or one visibility group
- groups contain tenant board IDs
- ticket access remains client-scoped through
contact.client_idandticket.client_id - board filtering is applied as an additional restriction layer, not as proof of board ownership
Current-State Findings
Re-review of the PRD, migration, and current schema showed:
- The PRD clearly intends client-scoped groups and board-based filtering for that client's portal users.
- The migration adds:
client_portal_visibility_groups(tenant, group_id, client_id, ...)client_portal_visibility_group_boards(tenant, group_id, board_id)contacts.portal_visibility_group_id
- The migration does not add:
boards.client_id- a client-board join table
client_idonclient_portal_visibility_group_boards
- The live schema confirms
boardsis tenant-scoped and has noclient_idcolumn. - Current visibility-group actions and the shared visibility resolver still query
boards.client_id, causing intermittent 500s and invalid empty states.
So the mismatch is not that a product feature is missing. The mismatch is that the implementation filled an ambiguous technical gap in the PRD with the wrong data-model assumption.
Product Rule
The corrected rule is:
A client portal contact may only see tickets where
ticket.client_id = contact.client_id, and if that contact has an assigned visibility group, the ticket'sboard_idmust also be included in that group's allowed board IDs.
This means:
- Client isolation comes from contact/group/ticket relationships.
- Board IDs act as a secondary visibility filter.
- Board rows do not need to belong to a client.
Recommended Approach
Use a targeted model-correction fix.
Why:
- It matches the approved product interpretation and the shipped migration.
- It avoids inventing a new board-ownership model that the PRD never required.
- It preserves the current feature scope and acceptance criteria.
- It keeps the fix small enough to validate with focused smoke tests.
Core Rules
client_portal_visibility_groups.client_idremains the source of group ownership.client_portal_visibility_group_boardsstores tenant board IDs only.- Group assignment remains on
contacts.portal_visibility_group_id. - Unassigned contacts retain legacy full access within their own client.
- Restricted contacts remain scoped to their own client's tickets, then filtered by allowed board IDs.
- Group CRUD and assignment actions validate client ownership through the group and contact, not through the board row.
Board Selection and Validation
Board picker rule
For both MSP and client portal admin management surfaces, board pickers should list:
- all active boards in the tenant
This is intentional. Boards are tenant-scoped visibility mechanisms, not client-owned objects.
Validation rule
When a group is created or updated, each submitted board must:
- exist in the tenant
- be active
Validation must not require boards.client_id.
Inactive board rule
If a board already assigned to a group later becomes inactive:
- keep the board membership record in the group
- exclude the inactive board from management pickers and new ticket creation choices
- keep historical group membership data intact for auditability and predictable rollback
UI Behavior
MSP contact portal tab
The PSA contact portal tab remains the MSP management surface.
Expected behavior:
- assignment dropdown remains client-scoped through the contact's client
- visibility-group editor remains available from the contact portal tab
- board picker lists all active tenant boards
No boards availableshould only appear when there are truly no active tenant boards in the tenant
Client portal admin settings
The client portal settings screen remains the client-admin management surface.
Expected behavior:
- group CRUD remains limited to the acting admin's client
- contact assignments remain limited to contacts from that client
- board picker lists all active tenant boards
- board selection does not imply board ownership by the client
Server-Side Enforcement
Group CRUD and assignment
Actions must verify:
- acting user can manage the target client
- target contact belongs to the same client as the target group
- submitted boards are valid active boards in the tenant
They must not enforce a nonexistent boards.client_id relationship.
Shared visibility resolver
The shared resolver should:
- load the contact and its
client_id - return unrestricted access when
portal_visibility_group_idisNULL - if a group is assigned:
- load the group
- verify
group.client_id === contact.client_id - load board IDs from
client_portal_visibility_group_boards - optionally join
boardsonly to exclude inactive boards, not to enforce client ownership
Ticket list/detail/dashboard/create
These paths should continue to enforce:
ticket.client_id = contact.client_idticket.board_id in visibleBoardIdswhen a group is assigned
This preserves the intended security model:
- no cross-client access
- no hidden-board access within the same client
- no crafted ticket creation to a disallowed board
PRD Clarification
The original PRD was strong on product behavior but ambiguous in one technical phrase:
"allowed boards from the same client/tenant context"
This should be clarified in implementation notes as:
Boards remain tenant-scoped. Visibility groups are client-scoped. Enforcement uses the contact's client and the ticket's client, with group board IDs acting as a secondary visibility filter.
Testing Focus
Update or add tests for:
- MSP and client-portal board pickers using active tenant boards without
boards.client_id - group create/update validation accepting tenant board IDs
- cross-client assignment rejection through
group.client_idandcontact.client_id - shared visibility resolver returning allowed board IDs without client-owned board assumptions
- ticket list/detail/dashboard/create still enforcing both client scoping and board restriction
- inactive boards excluded from pickers and creation choices
Smoke Validation Target
After the correction lands, use Emerald City smoke data to validate:
- MSP group CRUD and assignment
- client portal admin group CRUD and assignment
- restricted user sees only allowed-board tickets
- full-access user keeps unrestricted behavior
- pre-invite assignment still applies after user creation
- empty-group behavior blocks ticket creation and shows the expected empty state
- assigned-group deletion remains blocked
Implementation Shape
Likely touchpoints:
packages/clients/src/actions/contact-actions/contactActions.tsxpackages/client-portal/src/actions/client-portal-actions/visibilityGroupActions.tspackages/tickets/src/lib/clientPortalVisibility.ts- related visibility-group tests
- related client-ticket and dashboard visibility tests
Conclusion
This should be treated as an implementation correction, not as a new feature or schema expansion. The correct model is:
- client-scoped groups
- tenant-scoped boards
- client-scoped ticket access
- board-based restriction layered on top