Skip to content

[codex] harden live sync and customer identity matching - #139

Merged
bizzybee90 merged 1 commit into
mainfrom
codex/step2-live-sync-identity-2026-06-22
Jun 22, 2026
Merged

[codex] harden live sync and customer identity matching#139
bizzybee90 merged 1 commit into
mainfrom
codex/step2-live-sync-identity-2026-06-22

Conversation

@bizzybee90

@bizzybee90 bizzybee90 commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a shared workspace-scoped realtime invalidation hook for inbox, import progress, insights, learning, customer identity, and conversation views.
  • Adds an idempotent realtime publication migration for the live UI tables the frontend now subscribes to.
  • Hardens customer identity matching so placeholder emails do not collapse unknown senders together, and same-name link candidates require the same normalized service address.

Validation

  • npm run typecheck
  • npm run lint:ci
  • npm run test:run -- src/lib/api/tests/edgeFunctionSendContracts.test.ts
  • npm run test:run
  • npm run build
  • coderabbit review --agent --base-commit 8bd872d -> 0 issues

Safety

No deploy, no Supabase migration apply, no Supabase function deploy, no provider sends/calls, no review publishing, no billing mutation, no Cloudflare mutation, no queue destructive action, and no public signup change.

Summary by CodeRabbit

  • New Features

    • Real-time synchronization implemented across conversation history, customer timelines, activity feeds, and dashboard insights—components now automatically refresh when workspace data changes.
  • Improvements

    • Customer identity matching enhanced with stricter validation rules and improved data normalization.
  • Tests

    • Added comprehensive contract tests for realtime publication structure and customer identity matching validation.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a centralized useWorkspaceLiveInvalidation hook that subscribes to Supabase postgres_changes, debounces React Query cache invalidation, and dispatches browser CustomEvents consumed by five UI components (replacing prior per-component Supabase subscriptions). Adds a SQL migration enrolling tables in the supabase_realtime publication and a second migration introducing three PostgreSQL functions for hardened customer identity normalization and candidate matching.

Changes

Workspace Live Invalidation

Layer / File(s) Summary
Core invalidation hook
src/hooks/useWorkspaceLiveInvalidation.ts
Defines workspace/global React Query key prefixes, exports WORKSPACE_LIVE_INVALIDATION_EVENT constant and WorkspaceLiveInvalidationDetail type, and implements the full hook: Supabase channel per-table subscription (filtered by workspace_id), 350 ms debounced flush, React Query cache invalidation, and per-table CustomEvent dispatch with cleanup.
Realtime publication migration
supabase/migrations/20260622144238_live_ui_realtime_publication.sql
Transactional SQL migration that loops over a fixed table list and conditionally runs ALTER PUBLICATION supabase_realtime ADD TABLE for tables not yet enrolled, emitting NOTICE per addition.
App wiring and component subscriptions
src/App.tsx, src/components/dashboard/ActivityFeed.tsx, src/components/dashboard/InsightsWidget.tsx, src/components/dashboard/LearningInsightsWidget.tsx, src/components/conversations/CustomerConversationHistory.tsx, src/components/conversations/CustomerTimeline.tsx
Mounts the hook in RouterContent; replaces Supabase channel subscriptions in ActivityFeed and LearningInsightsWidget with window event listeners; adds new listeners to InsightsWidget, CustomerConversationHistory, and CustomerTimeline; guards CustomerTimeline messages query when conversationIds is empty.
Contract tests for live invalidation
src/lib/api/__tests__/edgeFunctionSendContracts.test.ts
Vitest contract test asserting the publication migration covers the correct table set and that the hook performs workspace-scoped event filtering and uses the expected event/table names.

Customer Identity Matching Hardening

Layer / File(s) Summary
Normalization helper functions
supabase/migrations/20260622144553_harden_customer_identity_matching.sql (lines 1–108)
Creates bb_norm_identifier (IMMUTABLE; normalizes email/phone, rejects unknown/no-reply placeholders) and bb_customer_identity_address_key (IMMUTABLE; builds a normalized address key from p_address or custom_fields).
Candidate-finding function + permissions
supabase/migrations/20260622144553_harden_customer_identity_matching.sql (lines 110–262)
Adds STABLE SECURITY DEFINER bb_find_customer_identity_link_candidates: enforces workspace access, self-joins customers on shared normalized keys, computes a weighted score, tiers into strong/review/weak (≥ 0.55 threshold), returns ranked pairs with JSON signals/evidence. Revokes public execute and grants to authenticated and service_role for all three new functions.
Contract test for identity hardening
src/lib/api/__tests__/edgeFunctionSendContracts.test.ts (lines 488–505)
Vitest contract test asserting placeholder identity handling, normalization helper usage, stricter join semantics, and removal of weaker matching paths via negative assertions.

Sequence Diagram(s)

sequenceDiagram
  participant DB as Supabase DB (postgres_changes)
  participant Hook as useWorkspaceLiveInvalidation
  participant QC as React Query Cache
  participant Win as window (CustomEvent bus)
  participant UI as Dashboard / Conversation Components

  DB->>Hook: postgres_changes event (table, workspace_id)
  Hook->>Hook: add table to pending Set, schedule 350ms debounce
  Hook->>QC: invalidateQueries(workspaceKeys + globalKeys)
  loop for each changed table
    Hook->>Win: dispatchEvent(WORKSPACE_LIVE_INVALIDATION_EVENT, {workspaceId, table})
  end
  Win->>UI: onLiveChange handler fires
  UI->>UI: filter by workspaceId + table name
  UI->>UI: fetchActivities() / fetchInsights() / fetchTimeline() / fetchConversations()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A channel once whispered to each widget alone,
Now one hook collects every Supabase groan.
It debounces and flushes, then shouts on the bus —
The dashboards all listen without any fuss!
Identities hardened, no "unknown" shall pass,
Live updates now sparkle like dew on the grass. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: hardening live sync and customer identity matching functionality across multiple components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/step2-live-sync-identity-2026-06-22

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
supabase/migrations/20260622144553_harden_customer_identity_matching.sql (2)

149-153: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Correlated subquery for conversation_count may impact performance at scale.

This subquery executes once per customer row in the workspace before filtering. For workspaces with many customers, consider replacing with a pre-aggregated CTE or lateral join:

♻️ Suggested refactor using pre-aggregated CTE
+  conversation_counts as (
+    select customer_id, count(*)::integer as cnt
+    from public.conversations
+    where customer_id in (select id from public.customers where workspace_id = p_workspace_id)
+    group by customer_id
+  ),
   scoped_customers as (
     select
       c.id,
       ...
-      (
-        select count(*)::integer
-        from public.conversations co
-        where co.customer_id = c.id
-      ) as conversation_count
+      coalesce(cc.cnt, 0) as conversation_count
     from public.customers c
+    left join conversation_counts cc on cc.customer_id = c.id
     where c.workspace_id = p_workspace_id
   ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260622144553_harden_customer_identity_matching.sql`
around lines 149 - 153, The correlated subquery in the conversation_count field
executes for every customer row, causing performance issues at scale. Replace
this subquery with a pre-aggregated CTE that groups public.conversations by
customer_id and calculates the count once, then left join this CTE to the
customer table (aliased as c) on customer_id to retrieve the pre-calculated
conversation_count for each customer. This avoids the repeated subquery
execution and improves query performance.

240-248: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

The 'weak' tier branch is unreachable.

The WHERE s.score >= 0.55 filter on line 248 excludes all rows that would fall into the else 'weak' branch (score < 0.55). This appears intentional for hardening, but the dead branch adds slight confusion.

Consider either removing the else branch or adding a comment clarifying the intentional exclusion:

     case
       when s.score >= 0.90 then 'strong'
-      when s.score >= 0.55 then 'review'
-      else 'weak'
+      else 'review'  -- scores < 0.55 filtered by WHERE clause
     end as tier,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260622144553_harden_customer_identity_matching.sql`
around lines 240 - 248, The CASE statement in the SELECT clause contains an
unreachable else branch that would assign the 'weak' tier, but the WHERE clause
on line 248 filters out all rows where s.score is less than 0.55, making this
branch impossible to reach. Either remove the else 'weak' branch entirely from
the CASE expression since it will never execute, or if this design is
intentional, add a SQL comment above the WHERE clause explaining that scores
below 0.55 are intentionally excluded for customer identity hardening purposes.
src/components/conversations/CustomerConversationHistory.tsx (1)

53-68: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider adding workspaceId filter for consistency.

Other consumers (ActivityFeed, LearningInsightsWidget, InsightsWidget) check detail?.workspaceId === workspace?.id before triggering refresh. This component and CustomerTimeline skip that check. While functionally safe (the hook only emits events for the current workspace), adding the check would maintain a consistent pattern across all consumers.

♻️ Suggested consistency fix
+ import { useWorkspace } from '`@/hooks/useWorkspace`';

  export const CustomerConversationHistory = ({
    customerId,
    currentConversationId,
  }: CustomerConversationHistoryProps) => {
+   const { workspace } = useWorkspace();
    const [conversations, setConversations] = useState<Tables<'conversations'>[]>([]);
    // ...

    useEffect(() => {
      const onLiveChange = (event: Event) => {
        const detail = (event as CustomEvent<WorkspaceLiveInvalidationDetail>).detail;
        if (
-         detail &&
+         detail?.workspaceId === workspace?.id &&
          ['conversations', 'message_events', 'customers', 'customer_identities'].includes(
            detail.table,
          )
        ) {
          void fetchConversations();
        }
      };

      window.addEventListener(WORKSPACE_LIVE_INVALIDATION_EVENT, onLiveChange);
      return () => window.removeEventListener(WORKSPACE_LIVE_INVALIDATION_EVENT, onLiveChange);
-   }, [fetchConversations]);
+   }, [fetchConversations, workspace?.id]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/conversations/CustomerConversationHistory.tsx` around lines 53
- 68, In the CustomerConversationHistory component, the useEffect hook
containing the onLiveChange event listener is missing a workspaceId validation
check that other consumers like ActivityFeed, LearningInsightsWidget, and
InsightsWidget include. Add a check for detail?.workspaceId === workspace?.id
alongside the existing table check to ensure the refresh only triggers for
events from the current workspace, maintaining consistency with the pattern used
in other components that listen to WORKSPACE_LIVE_INVALIDATION_EVENT.
src/components/conversations/CustomerTimeline.tsx (1)

172-188: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Consider adding workspaceId filtering for consistency.

Unlike InsightsWidget, this listener doesn't filter by detail.workspaceId. While the current implementation is safe (the hook is workspace-scoped at the app level), adding a workspaceId check would provide defensive filtering consistent with other components.

This would require either passing workspaceId as a prop or deriving it from the customer data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/conversations/CustomerTimeline.tsx` around lines 172 - 188,
The onLiveChange function in the useEffect hook checks the detail.table against
a list of tables but does not filter by detail.workspaceId like other similar
components do. Add a workspaceId check to the existing conditional that
validates detail.table. You will need to either pass workspaceId as a prop to
this component or derive it from the existing customer data to compare against
detail.workspaceId, then include this check in the same if statement alongside
the table validation to ensure the event is for the current workspace.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/migrations/20260622144553_harden_customer_identity_matching.sql`:
- Around line 40-42: The phone number normalization logic in the migration
contains a hardcoded assumption that local format numbers (starting with 0 and
10-12 digits) are UK numbers, prefixing them with +44. This will cause incorrect
normalization for workspaces in other countries. Remove or refactor the
hardcoded country code logic in the conditional block that checks `if v_digits
like '0%' and length(v_digits) >= 10 and length(v_digits) <= 12` by either
making the country code configurable through a workspace parameter or setting,
or removing this heuristic entirely to prevent false matches across different
geographical regions.

---

Nitpick comments:
In `@src/components/conversations/CustomerConversationHistory.tsx`:
- Around line 53-68: In the CustomerConversationHistory component, the useEffect
hook containing the onLiveChange event listener is missing a workspaceId
validation check that other consumers like ActivityFeed, LearningInsightsWidget,
and InsightsWidget include. Add a check for detail?.workspaceId ===
workspace?.id alongside the existing table check to ensure the refresh only
triggers for events from the current workspace, maintaining consistency with the
pattern used in other components that listen to
WORKSPACE_LIVE_INVALIDATION_EVENT.

In `@src/components/conversations/CustomerTimeline.tsx`:
- Around line 172-188: The onLiveChange function in the useEffect hook checks
the detail.table against a list of tables but does not filter by
detail.workspaceId like other similar components do. Add a workspaceId check to
the existing conditional that validates detail.table. You will need to either
pass workspaceId as a prop to this component or derive it from the existing
customer data to compare against detail.workspaceId, then include this check in
the same if statement alongside the table validation to ensure the event is for
the current workspace.

In `@supabase/migrations/20260622144553_harden_customer_identity_matching.sql`:
- Around line 149-153: The correlated subquery in the conversation_count field
executes for every customer row, causing performance issues at scale. Replace
this subquery with a pre-aggregated CTE that groups public.conversations by
customer_id and calculates the count once, then left join this CTE to the
customer table (aliased as c) on customer_id to retrieve the pre-calculated
conversation_count for each customer. This avoids the repeated subquery
execution and improves query performance.
- Around line 240-248: The CASE statement in the SELECT clause contains an
unreachable else branch that would assign the 'weak' tier, but the WHERE clause
on line 248 filters out all rows where s.score is less than 0.55, making this
branch impossible to reach. Either remove the else 'weak' branch entirely from
the CASE expression since it will never execute, or if this design is
intentional, add a SQL comment above the WHERE clause explaining that scores
below 0.55 are intentionally excluded for customer identity hardening purposes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ff6190ac-2840-414d-8743-81164093cee2

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd872d and 7e29cf2.

📒 Files selected for processing (10)
  • src/App.tsx
  • src/components/conversations/CustomerConversationHistory.tsx
  • src/components/conversations/CustomerTimeline.tsx
  • src/components/dashboard/ActivityFeed.tsx
  • src/components/dashboard/InsightsWidget.tsx
  • src/components/dashboard/LearningInsightsWidget.tsx
  • src/hooks/useWorkspaceLiveInvalidation.ts
  • src/lib/api/__tests__/edgeFunctionSendContracts.test.ts
  • supabase/migrations/20260622144238_live_ui_realtime_publication.sql
  • supabase/migrations/20260622144553_harden_customer_identity_matching.sql

Comment on lines +40 to +42
if v_digits like '0%' and length(v_digits) >= 10 and length(v_digits) <= 12 then
v_digits := '+44' || substring(v_digits from 2);
end if;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hardcoded UK country code assumption for local phone numbers.

Numbers starting with 0 and 10-12 digits are assumed to be UK local numbers and prefixed with +44. This will produce incorrect normalized keys for workspaces operating in other countries (e.g., a US number 0555123456 would become +44555123456).

Consider making the default country code configurable via a workspace setting or parameter, or removing this heuristic to avoid false matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260622144553_harden_customer_identity_matching.sql`
around lines 40 - 42, The phone number normalization logic in the migration
contains a hardcoded assumption that local format numbers (starting with 0 and
10-12 digits) are UK numbers, prefixing them with +44. This will cause incorrect
normalization for workspaces in other countries. Remove or refactor the
hardcoded country code logic in the conditional block that checks `if v_digits
like '0%' and length(v_digits) >= 10 and length(v_digits) <= 12` by either
making the country code configurable through a workspace parameter or setting,
or removing this heuristic entirely to prevent false matches across different
geographical regions.

@bizzybee90
bizzybee90 merged commit 2306d5e into main Jun 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant