PSA/ee/docs/plans/2026-04-15-client-portal-board-visibility-model-correction-design.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

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_id and ticket.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:

  1. The PRD clearly intends client-scoped groups and board-based filtering for that client's portal users.
  2. 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
  3. The migration does not add:
    • boards.client_id
    • a client-board join table
    • client_id on client_portal_visibility_group_boards
  4. The live schema confirms boards is tenant-scoped and has no client_id column.
  5. 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's board_id must also be included in that group's allowed board IDs.

This means:

  1. Client isolation comes from contact/group/ticket relationships.
  2. Board IDs act as a secondary visibility filter.
  3. Board rows do not need to belong to a client.

Use a targeted model-correction fix.

Why:

  1. It matches the approved product interpretation and the shipped migration.
  2. It avoids inventing a new board-ownership model that the PRD never required.
  3. It preserves the current feature scope and acceptance criteria.
  4. It keeps the fix small enough to validate with focused smoke tests.

Core Rules

  1. client_portal_visibility_groups.client_id remains the source of group ownership.
  2. client_portal_visibility_group_boards stores tenant board IDs only.
  3. Group assignment remains on contacts.portal_visibility_group_id.
  4. Unassigned contacts retain legacy full access within their own client.
  5. Restricted contacts remain scoped to their own client's tickets, then filtered by allowed board IDs.
  6. 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:

  1. exist in the tenant
  2. be active

Validation must not require boards.client_id.

Inactive board rule

If a board already assigned to a group later becomes inactive:

  1. keep the board membership record in the group
  2. exclude the inactive board from management pickers and new ticket creation choices
  3. 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:

  1. assignment dropdown remains client-scoped through the contact's client
  2. visibility-group editor remains available from the contact portal tab
  3. board picker lists all active tenant boards
  4. No boards available should 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:

  1. group CRUD remains limited to the acting admin's client
  2. contact assignments remain limited to contacts from that client
  3. board picker lists all active tenant boards
  4. board selection does not imply board ownership by the client

Server-Side Enforcement

Group CRUD and assignment

Actions must verify:

  1. acting user can manage the target client
  2. target contact belongs to the same client as the target group
  3. 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:

  1. load the contact and its client_id
  2. return unrestricted access when portal_visibility_group_id is NULL
  3. 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 boards only to exclude inactive boards, not to enforce client ownership

Ticket list/detail/dashboard/create

These paths should continue to enforce:

  1. ticket.client_id = contact.client_id
  2. ticket.board_id in visibleBoardIds when 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:

  1. MSP and client-portal board pickers using active tenant boards without boards.client_id
  2. group create/update validation accepting tenant board IDs
  3. cross-client assignment rejection through group.client_id and contact.client_id
  4. shared visibility resolver returning allowed board IDs without client-owned board assumptions
  5. ticket list/detail/dashboard/create still enforcing both client scoping and board restriction
  6. inactive boards excluded from pickers and creation choices

Smoke Validation Target

After the correction lands, use Emerald City smoke data to validate:

  1. MSP group CRUD and assignment
  2. client portal admin group CRUD and assignment
  3. restricted user sees only allowed-board tickets
  4. full-access user keeps unrestricted behavior
  5. pre-invite assignment still applies after user creation
  6. empty-group behavior blocks ticket creation and shows the expected empty state
  7. assigned-group deletion remains blocked

Implementation Shape

Likely touchpoints:

  • packages/clients/src/actions/contact-actions/contactActions.tsx
  • packages/client-portal/src/actions/client-portal-actions/visibilityGroupActions.ts
  • packages/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