Skip to content

clickhouse sync contact channels, teams#1256

Closed
BilalG1 wants to merge 5 commits intodevfrom
clickhouse-sync-extra-tables
Closed

clickhouse sync contact channels, teams#1256
BilalG1 wants to merge 5 commits intodevfrom
clickhouse-sync-extra-tables

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Mar 16, 2026

Summary by CodeRabbit

  • New Features

    • External DB sync now covers Teams, Team Members, and Contact Channels (create/update/delete) with analytics integration and improved cascade deletion handling.
    • New analytics tables/views and supporting sync paths for teams, team member profiles, and contact channels.
  • Chores

    • Schema additions to track sequence IDs and update flags for relevant entities.
    • New sync mappings and migration support for contact channels and team-related analytics.
  • Tests

    • Expanded end-to-end tests covering sync lifecycles, ClickHouse verifications, and deletion cascades.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Mar 19, 2026 8:44pm
stack-backend Ready Ready Preview, Comment Mar 19, 2026 8:44pm
stack-dashboard Ready Ready Preview, Comment Mar 19, 2026 8:44pm
stack-demo Ready Ready Preview, Comment Mar 19, 2026 8:44pm
stack-docs Ready Ready Preview, Comment Mar 19, 2026 8:44pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2e3a0d9-c001-4b4c-8279-c1be4b9bf7f2

📥 Commits

Reviewing files that changed from the base of the PR and between 823036b and 1e1148c.

📒 Files selected for processing (1)
  • packages/stack-shared/src/config/db-sync-mappings.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stack-shared/src/config/db-sync-mappings.ts

📝 Walkthrough

Walkthrough

Adds sequence-tracking columns and indexes, ClickHouse tables/views, backfill and CRUD hooks to sync Team and TeamMember, updates external-db-sync normalization/mapping, and expands e2e tests and helpers for Teams, TeamMembers, and ContactChannels.

Changes

Cohort / File(s) Summary
DB Migrations & Schema
apps/backend/prisma/migrations/20260316000000_add_team_team_member_sequence_columns/migration.sql, apps/backend/prisma/schema.prisma
Adds sequenceId (BigInt?) and shouldUpdateSequenceId (Boolean DEFAULT true) to Team, TeamMember, ProjectUser, ContactChannel; creates unique/supporting indexes.
ClickHouse Migrations & Views
apps/backend/scripts/clickhouse-migrations.ts
Adds SQL for contact_channels, teams, team_member_profiles tables/views, grants for limited_user, and row-level isolation policies; integrates these migrations into existing flow.
Sequencer Backfill
apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts
Extends backfillSequenceIds to backfill Team and TeamMember sequenceId (nextval('global_seq_id')), clear flags, set spans, enqueue sync batches, and include new metrics/logging.
CRUD: Teams & Memberships
apps/backend/src/app/api/latest/teams/crud.tsx, apps/backend/src/app/api/latest/team-memberships/crud.tsx
Wraps create/update payloads with external DB sync update helper and records external-db-sync deletions for Team and TeamMember prior to deletion.
CRUD: Users (cascade deletions)
apps/backend/src/app/api/latest/users/crud.tsx
Invokes recordExternalDbSyncTeamMemberDeletionsForUser during user deletion flow to log team-member deletions for external sync before removal.
External DB Sync Library
apps/backend/src/lib/external-db-sync.ts
Extends ExternalDbSyncTarget with Team and TeamMember; adds deletion logging branches and helper functions for Team/TeamMember deletions; introduces CLICKHOUSE_COLUMN_NORMALIZERS and per-table normalization in pushRowsToClickhouse.
DB Sync Mappings
packages/stack-shared/src/config/db-sync-mappings.ts
Adds contact_channels mapping: Postgres/ClickHouse DDL, internal fetch queries (including tombstones), external update SQL (upsert/delete), and metadata tracking.
E2E Tests & Helpers
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts, apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts
Adds generic waitForExternalDbRow and new waiters for Teams/TeamMembers/ContactChannels; expands e2e tests covering CRUD, cascade deletions, ClickHouse verification, and sync scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Client as API Client
  participant CRUD as Backend CRUD
  participant Sequencer as Sequencer
  participant SyncLib as ExternalDbSyncLib
  participant PG as Postgres
  participant CH as ClickHouse

  Client->>CRUD: create/update/delete Team or TeamMember
  CRUD->>SyncLib: withExternalDbSyncUpdate / recordExternalDbSyncDeletion
  SyncLib->>PG: insert DeletedRow / upsert _stack_sync_metadata
  Note right of PG: internal Postgres state persisted
  Sequencer->>PG: select shouldUpdateSequenceId=true
  Sequencer->>PG: update sequenceId via nextval & clear flag
  Sequencer->>SyncLib: enqueue sync batch (tenancy)
  SyncLib->>CH: pushRowsToClickhouse (apply per-table normalizers)
  CH-->>SyncLib: ack
  SyncLib-->>CRUD: async ack
  CRUD-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • N2D4

Poem

"🐇 I hopped through rows and indexes new,

Gave teams and members a sequence true,
ClickHouse hums and Postgres keeps score,
Batches queued, the syncers run to the door,
A tiny rabbit cheers — syncs forevermore!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete, containing only the repository template comment without any implementation details, rationale, or explanation of the changes made. Add a comprehensive description explaining the changes made, including: what sequence-id tracking was added to which entities, why external DB sync integration was extended, and what ClickHouse tables/views were created.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main objective: extending ClickHouse synchronization to contact channels and teams, which are the primary focus of the file changes across migrations, schema updates, and external DB sync logic.

✏️ 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 clickhouse-sync-extra-tables
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

Extends the external DB sync system to sync ContactChannel, Team, and TeamMember tables to both Postgres and ClickHouse external databases, in addition to the existing ProjectUser sync.

  • Adds sequenceId and shouldUpdateSequenceId columns to Team and TeamMember Prisma models with a safe migration (nullable sequenceId, DEFAULT true for backfill)
  • Creates ClickHouse table definitions, views, row policies, and grants for contact_channels, teams, and team_members
  • Extends the sequencer to backfill sequence IDs for Team and TeamMember rows using the same CTE + FOR UPDATE SKIP LOCKED pattern
  • Adds deletion recording for Team and TeamMember in CRUD handlers, including cascade handling (team delete → member deletions, user delete → member deletions)
  • Refactors ClickHouse column normalization from hardcoded per-table logic to a data-driven CLICKHOUSE_COLUMN_NORMALIZERS map
  • Adds complete sync mapping definitions (fetch queries, upsert queries, schemas) for all three new tables in db-sync-mappings.ts
  • Comprehensive E2E tests cover CRUD sync for teams, team members, and contact channels, plus cascade deletion scenarios

Confidence Score: 4/5

  • This PR is well-structured and follows established patterns consistently; safe to merge with standard review.
  • The changes closely follow the existing sync pattern for ProjectUser/ContactChannel, with proper cascade deletion handling, safe migrations, and thorough E2E test coverage. The refactoring of ClickHouse normalization to a data-driven map is clean. No critical issues found.
  • apps/backend/src/lib/external-db-sync.ts has the most complex changes and deserves careful review of the deletion recording logic and normalizer map.

Important Files Changed

Filename Overview
apps/backend/prisma/migrations/20260316000000_add_team_team_member_sequence_columns/migration.sql Adds sequenceId (nullable BIGINT) and shouldUpdateSequenceId (NOT NULL, DEFAULT true) columns to Team and TeamMember tables with appropriate indexes. DEFAULT true ensures existing rows get backfilled. Safe migration.
apps/backend/scripts/clickhouse-migrations.ts Adds ClickHouse table/view definitions for contact_channels, teams, and team_members following the same structure as existing users table. Includes proper row policies and grants for the limited_user.
apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts Extends backfillSequenceIds with CTE-based batch updates for Team and TeamMember tables, following the same pattern as ProjectUser and ContactChannel. Uses FOR UPDATE SKIP LOCKED for safe concurrency.
apps/backend/src/app/api/latest/team-memberships/crud.tsx Wraps TeamMember create data with withExternalDbSyncUpdate() and records deletions via recordExternalDbSyncDeletion() before the Prisma delete, matching the existing pattern for other synced tables.
apps/backend/src/app/api/latest/teams/crud.tsx Wraps Team create/update data with withExternalDbSyncUpdate(). On team deletion, records both team member cascade deletions and team deletion before the Prisma delete.
apps/backend/src/app/api/latest/users/crud.tsx Adds recordExternalDbSyncTeamMemberDeletionsForUser call before user deletion to handle cascade-deleted team memberships in the external DB sync.
apps/backend/src/lib/external-db-sync.ts Adds Team/TeamMember deletion recording, bulk deletion helpers for cascades, and refactors ClickHouse normalization to a data-driven map. The plain object CLICKHOUSE_COLUMN_NORMALIZERS uses static keys, so it's acceptable.
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts Adds comprehensive E2E tests for Team/TeamMember/ContactChannel CRUD sync (Postgres and ClickHouse), including cascade deletion tests for user and team deletions.
packages/stack-shared/src/config/db-sync-mappings.ts Adds complete sync mapping definitions for contact_channels, teams, and team_members with Postgres and ClickHouse schemas, internal DB fetch queries, and external DB upsert queries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph CRUD["CRUD Operations"]
        TC["Team Create/Update"] -->|withExternalDbSyncUpdate| TDB["Team table\n(shouldUpdateSequenceId=true)"]
        TMC["TeamMember Create"] -->|withExternalDbSyncUpdate| TMDB["TeamMember table\n(shouldUpdateSequenceId=true)"]
        TD2["Team Delete"] -->|recordExternalDbSyncTeamMemberDeletionsForTeam| DR1["DeletedRow (TeamMember)"]
        TD2 -->|recordExternalDbSyncDeletion| DR2["DeletedRow (Team)"]
        TMD["TeamMember Delete"] -->|recordExternalDbSyncDeletion| DR3["DeletedRow (TeamMember)"]
        UD["User Delete"] -->|recordExternalDbSyncTeamMemberDeletionsForUser| DR4["DeletedRow (TeamMember)"]
    end

    subgraph Sequencer["Sequencer (Cron)"]
        SEQ["backfillSequenceIds"] -->|assigns global_seq_id| TDB
        SEQ -->|assigns global_seq_id| TMDB
        SEQ -->|assigns global_seq_id| DR1
        SEQ -->|assigns global_seq_id| DR2
        SEQ --> ENQ["enqueueExternalDbSyncBatch"]
    end

    subgraph Sync["Sync Worker"]
        ENQ --> PG["Postgres External DB\n(teams, team_members,\ncontact_channels)"]
        ENQ --> CH["ClickHouse\n(analytics_internal.teams,\nteam_members, contact_channels)"]
    end
Loading

Last reviewed commit: 3c88950

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts`:
- Around line 731-752: Add an explicit create/delete lifecycle for a secondary
ContactChannel in this test: after User.create(...) use the API route that
creates contact channels (the same endpoint the app exposes for POST
/api/v1/contact-channels) to create a secondary channel (e.g., value
'cc-secondary@example.com'), wait for it to appear in the external DB with
waitForSyncedContactChannel(client, 'cc-secondary@example.com'), then call the
API DELETE for that channel, wait for removal with
waitForSyncedContactChannelDeletion(client, 'cc-secondary@example.com'), and
finally assert the row is gone (no rows returned from SELECT on contact_channels
for that value); keep the existing primary-channel assertions and reuse
createProjectWithExternalDb and dbManager client as in the test.
- Around line 644-675: Add ClickHouse E2E assertions for the new mappings by
extending the existing "Team sync (ClickHouse)" test: after creating the team
and awaiting its presence via runQueryForCurrentProject, add at least one
happy-path SELECT for contact_channels (e.g., query "SELECT * FROM
contact_channels WHERE ...") and one for team_members (e.g., query "SELECT *
FROM team_members WHERE ...") using runQueryForCurrentProject and expect 200 and
non-empty results; also add a negative/delete visibility assertion for one of
those tables by deleting or marking a record invisible via the existing test
helpers (or creating then deleting a contact_channel/team_member) and then
polling with the same runQueryForCurrentProject loop until the row is absent,
asserting length === 0. Ensure you reuse the same timeout/interval pattern and
reference response variable checks like response.body.result to validate the
results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea49df6a-2308-4d43-bd5c-6c965a84ced7

📥 Commits

Reviewing files that changed from the base of the PR and between 46cacd4 and 3c88950.

📒 Files selected for processing (11)
  • apps/backend/prisma/migrations/20260316000000_add_team_team_member_sequence_columns/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/scripts/clickhouse-migrations.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts
  • apps/backend/src/app/api/latest/team-memberships/crud.tsx
  • apps/backend/src/app/api/latest/teams/crud.tsx
  • apps/backend/src/app/api/latest/users/crud.tsx
  • apps/backend/src/lib/external-db-sync.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts
  • packages/stack-shared/src/config/db-sync-mappings.ts

@BilalG1 BilalG1 changed the title clickhouse sync extra tables clickhouse sync contact channels, teams Mar 17, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/backend/scripts/clickhouse-migrations.ts (1)

298-333: Clarify the intended use of the embedded user JSON column.

The user JSON column denormalizes user data into team_member_profiles. This creates a snapshot of user data at sync time, which:

  1. May become stale if the user record is updated separately
  2. Could contain PII (e.g., primary_email) that duplicates data already in the users table

If the intent is to provide a point-in-time snapshot for analytics, this is fine. If consumers expect this to reflect current user state, consider whether joining to the users view would be more appropriate for queries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/scripts/clickhouse-migrations.ts` around lines 298 - 333, The
embedded user JSON column in TEAM_MEMBER_PROFILES_TABLE_BASE_SQL (the `user`
field) denormalizes a point-in-time snapshot which may become stale or duplicate
PII from the users table; clarify intended behavior and either (A) document in
the schema comment and view (TEAM_MEMBER_PROFILES_VIEW_SQL) that this is a
snapshot for analytics and may contain PII, or (B) change the design to not
store full user JSON—store only non-PII fields or a reference (user_id) and
update TEAM_MEMBER_PROFILES_VIEW_SQL to join the canonical users view at query
time so consumers see current user state; update names/comments and any
downstream documentation to reflect the chosen approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/scripts/clickhouse-migrations.ts`:
- Around line 218-257: The contact_channels table definitions
(CONTACT_CHANNELS_TABLE_BASE_SQL) store raw PII in the value column and the view
(CONTACT_CHANNELS_VIEW_SQL) currently exposes it; to address privacy/compliance,
change the schema to stop exposing raw values—add a hashed_value (or
masked_value) column to analytics_internal.contact_channels and populate it at
ingest or via a migration instead of storing/returning the plain value, then
update default.contact_channels view to select hashed_value (or a masked
representation) instead of value; additionally add a retention policy/TTL or
clear-on-delete behavior in the table definition and document the retention
rules for the new hashed/masked field.

In `@apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts`:
- Around line 673-674: Replace the non-null assertion on the `response` variable
in the assertions (the two lines referencing `response!.body.result`) with a
null-coalescing check that calls `throwErr(...)` to provide an explicit error if
`response` is undefined; e.g., use `const resp = response ?? throwErr('Expected
response to be set before assertions')` and then assert on
`resp.body.result.length` and `resp.body.result[0].display_name`. Ensure you
import `throwErr` from `@stackframe/stack-shared/dist/utils/errors` if it’s not
already imported.

In `@packages/stack-shared/src/config/db-sync-mappings.ts`:
- Line 825: The mapping for last_active_at_millis mistakenly extracts epoch
milliseconds from "ProjectUser"."createdAt"; update the expression that defines
last_active_at_millis in db-sync-mappings (the CASE/EXTRACT expression
associated with 'last_active_at_millis') to use "ProjectUser"."lastActiveAt"
instead of "ProjectUser"."createdAt", ensuring the NULL check still references
"ProjectUser"."lastActiveAt" so the produced value matches the ClickHouse
variant.

---

Nitpick comments:
In `@apps/backend/scripts/clickhouse-migrations.ts`:
- Around line 298-333: The embedded user JSON column in
TEAM_MEMBER_PROFILES_TABLE_BASE_SQL (the `user` field) denormalizes a
point-in-time snapshot which may become stale or duplicate PII from the users
table; clarify intended behavior and either (A) document in the schema comment
and view (TEAM_MEMBER_PROFILES_VIEW_SQL) that this is a snapshot for analytics
and may contain PII, or (B) change the design to not store full user JSON—store
only non-PII fields or a reference (user_id) and update
TEAM_MEMBER_PROFILES_VIEW_SQL to join the canonical users view at query time so
consumers see current user state; update names/comments and any downstream
documentation to reflect the chosen approach.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f8c340a-094c-4c4e-820e-66bf969d8f1a

📥 Commits

Reviewing files that changed from the base of the PR and between 3c88950 and 823036b.

📒 Files selected for processing (7)
  • apps/backend/scripts/clickhouse-migrations.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts
  • apps/backend/src/lib/external-db-sync.ts
  • apps/e2e/tests/backend/endpoints/api/v1/analytics-query.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts
  • packages/stack-shared/src/config/db-sync-mappings.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts

@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Mar 18, 2026
@BilalG1 BilalG1 requested a review from N2D4 March 18, 2026 21:36
Was using createdAt instead of lastActiveAt in the Postgres variant of
the team_member_profiles sync query.
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.

2 participants