Skip to content

External db sync#1036

Merged
BilalG1 merged 72 commits intodevfrom
external-db-sync
Feb 5, 2026
Merged

External db sync#1036
BilalG1 merged 72 commits intodevfrom
external-db-sync

Conversation

@aadesh18
Copy link
Copy Markdown
Contributor

@aadesh18 aadesh18 commented Nov 30, 2025

Screenshot 2026-02-04 at 9 47 16 AM

This PR revolves around the following components

  1. Sequencer - sequences the updates in the internal db
  2. Poller - polls for the latest updates to sync with the external db
  3. Outgoing Request Handler - essentially a trigger that can make http requests based on a change in the internal db
  4. Sync Engine - syncs with the latest changes from the internal db to the external db

What has been done

  • Added a global sequence id for ProjectUser, ContactChannel and DeletedRow.
  • Added the deletedRow table to keep track of the rows that were deleted across ProjectUser and ContactChannel.
  • Added the OutgoingRequest table to keep track of the outgoing requests
  • Added function for the sequencer to call to sequence updates
  • Added a sequencer that sequences all the changes in the internal db every 50 ms
  • Added a poller that polls for the latest changes in the internal db every 50 ms, and adds to a queue
  • Added a Vercel cron that calls sequencer and poller every minute
  • Added a queue that fulfills the outgoing requests by making http calls (for external db sync, it calls the sync engine endpoint)
  • Added a sync engine that uses the defined sql mapping query in the user's schema to pull in the changes for the user, and sync them with the external db
  • Added tests to test out each functionality

How to review this PR:

  1. Review the migrations (sequence id, deletedRow, triggers, backlog sync) (all files created under the migrations folder)
  2. Review sequencer
  3. Review poller
  4. Review the changes in schema
  5. Review sync-engine (the function, and it's helper file)
  6. Review the schema changes, and query mappings
  7. Review the tests (basic, advanced and race, along with the helper file)
  8. Review the changes made in Dockerfile to support local testing using the postgres docker

Note

Introduces a cron-driven external DB sync pipeline with global sequencing, internal poller and webhook sync engine, new DB tables/functions, config schema/mappings, and comprehensive e2e tests.

  • Database (Prisma/Migrations):
    • Add global sequence (global_seq_id) and sequenceId/shouldUpdateSequenceId to ProjectUser, ContactChannel, DeletedRow with partial indexes.
    • Create DeletedRow (capture deletes) and OutgoingRequest (queue) tables; add unique/indexes.
    • Add triggers/functions: log_deleted_row, reset_sequence_id_on_update, backfill_null_sequence_ids, enqueue_tenant_sync.
  • Backend/API:
    • New internal routes: GET /api/latest/internal/external-db-sync/sequencer, GET /poller, POST /sync-engine (Upstash-verified) for sync orchestration.
    • Add cron wiring: vercel.json schedules and local scripts/run-cron-jobs.ts; start in dev via dev script.
    • Tweak route handler (remove noisy logging) without behavior change.
  • Sync Engine:
    • Implement src/lib/external-db-sync.ts to read tenant mappings and upsert to external Postgres (schema bootstrap, param checks, sequencing).
    • Add default mappings DEFAULT_DB_SYNC_MAPPINGS and config schema dbSync.externalDatabases in shared config.
  • Testing/Infra:
    • Add extensive e2e tests (basics, advanced, race conditions) for sequencing, idempotency, deletes, pagination, multi-mapping, and permissions.
    • Docker compose: add external-db-test Postgres for tests; e2e deps for pg types.

Written by Cursor Bugbot for commit 3f2a8ef. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • External PostgreSQL sync: automatic, batched replication with mappings, resume/idempotency, and on-demand enqueueing.
  • Admin UI

    • Real-time External DB Sync dashboard and status API showing per-mapping backlog, sequencer/poller/sync-engine telemetry, and fusebox controls.
  • Tests

    • Large e2e suite: basic, advanced, race, high-volume tests and test utilities for external DB sync.
  • Chores

    • DB migrations, CI/workflow updates, background cron runner and local/dev test support.

@aadesh18 aadesh18 requested a review from N2D4 November 30, 2025 21:48
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Nov 30, 2025

CLA assistant check
All committers have signed the CLA.

@vercel
Copy link
Copy Markdown

vercel bot commented Nov 30, 2025

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

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Feb 5, 2026 6:08pm
stack-dashboard Ready Ready Preview, Comment Feb 5, 2026 6:08pm
stack-demo Ready Ready Preview, Comment Feb 5, 2026 6:08pm
stack-docs Ready Ready Preview, Comment Feb 5, 2026 6:08pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 30, 2025

📝 Walkthrough

Walkthrough

Adds a full External DB Sync feature: DB schema + migrations, sync library and queue, cron runner, internal APIs (sequencer, poller, sync-engine, fusebox, status), dashboard UI, extensive E2E tests and utilities, CI/workflow updates, and integration points in CRUD flows.

Changes

Cohort / File(s) Summary
Database schema & migrations
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/.../20251125030551_external_db_sync/migration.sql, apps/backend/prisma/migrations/.../20260204014127_external_db_metadata/migration.sql
Add models: OutgoingRequest, DeletedRow, ExternalDbSyncMetadata; add sequenceId and shouldUpdateSequenceId to ProjectUser/ContactChannel; create global_seq_id and multiple indexes.
Core sync library & queue
apps/backend/src/lib/external-db-sync.ts, apps/backend/src/lib/external-db-sync-queue.ts, apps/backend/src/lib/external-db-sync-metadata.ts
New external-db-sync implementation: validation, schema ensure, push/upsert, per-mapping batching/throttling, orchestration across external DBs, and enqueue helpers + fusebox metadata management.
Internal API routes
apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts, .../poller/route.ts, .../sync-engine/route.tsx, .../fusebox/route.ts, .../status/route.ts
Add sequencer, poller, sync-engine webhook, fusebox GET/POST, and detailed status endpoints with auth, batching, claim/release logic, tracing, and schemas.
Cron runner & package scripts
apps/backend/scripts/run-cron-jobs.ts, apps/backend/package.json
Add run-cron-jobs and test/dev variants; integrate cron runner into dev/test process lists and CI background tasks.
E2E tests & utilities
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-*.test.ts, apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts, apps/e2e/package.json, apps/e2e/tests/global-setup.ts
Add basic/advanced/race/high-volume E2E suites, TestDbManager and wait/verify helpers, add pg/@types/pg deps, and update dotenv order.
Performance seeds
apps/e2e/tests/backend/performance/mock-external-db-sync-projects.sql
Add heavy-load SQL seeding script for large-scale performance scenarios (up to millions of rows).
CRUD integration points
apps/backend/src/app/api/latest/contact-channels/crud.tsx, .../contact-channels/verify/verification-code-handler.tsx, apps/backend/src/app/api/latest/users/crud.tsx, apps/backend/src/lib/contact-channel.tsx, apps/backend/src/lib/tokens.tsx
Wrap user/contact-channel updates and deletions with external-sync helpers (withExternalDbSyncUpdate, markProjectUserForExternalDbSync, recordExternalDbSyncDeletion) to mark rows and enqueue syncs.
Mappings & config schema
packages/stack-shared/src/config/db-sync-mappings.ts, packages/stack-shared/src/config/schema.ts, packages/stack-shared/src/config/schema-fuzzer.test.ts
Add DEFAULT_DB_SYNC_MAPPINGS (users) with internal fetch and external upsert SQL; extend branch config schema and defaults to include dbSync.externalDatabases.
Status UI / Dashboard
apps/dashboard/src/app/.../external-db-sync/page.tsx, .../page-client.tsx
Add project-level External DB Sync dashboard pages showing sequencer/poller/mapping metrics, per-table details, and auto-refreshing telemetry.
Status & monitoring endpoint
apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts
New detailed status endpoint aggregating internal sequencing, queue, and external DB metadata for tenancy/global scopes.
CI / Workflows
.github/workflows/*, .github/workflows/e2e-*.yaml, .github/workflows/setup-tests*.yaml
Start run-cron-jobs in background, add STACK_* env vars for external-db-sync, add wait-on QStash, and switch test invocations to pnpm test run.
Dev & Docker env
apps/backend/.env.development, apps/e2e/.env.development, docker/dependencies/docker.compose.yaml, docker/dev-postgres-replica/entrypoint.sh, apps/backend/vercel.json, apps/backend/src/proxy.tsx
Add CRON_SECRET, quote env values, increase Postgres max_connections, enable pg_stat_statements for replica, exempt internal sync routes from dev artificial rate-limiter, add poller/sequencer crons to Vercel config.
Tooling & minor edits
apps/backend/scripts/db-migrations.ts, apps/backend/scripts/db-migrations.tsup.config.ts, apps/backend/scripts/run-cron-jobs.ts
Adjust Prisma migrate flags, small tsup typing change, add cron-runner script and CI background step.
E2E test tweaks & timeouts
apps/e2e/tests/helpers.ts, apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/index.test.ts, apps/e2e/tests/backend/endpoints/api/v1/users.test.ts
Increase retry/timeouts, add it.only in one test (note: narrows test run), and add CRON_SECRET to e2e env.

Sequence Diagram(s)

sequenceDiagram
    participant Cron as Cron Runner (run-cron-jobs)
    participant Seq as Sequencer API
    participant DB as Internal DB
    participant Queue as OutgoingRequest Queue
    participant Poll as Poller API
    participant Ext as External DB / Upstash

    Cron->>Seq: GET /api/latest/internal/external-db-sync/sequencer (Bearer)
    Seq->>DB: SELECT/UPDATE ProjectUser/ContactChannel (assign sequenceIds, mark shouldUpdateSequenceId)
    Seq->>Queue: INSERT batch OutgoingRequest rows
    Seq-->>Cron: { ok, iterations }

    Cron->>Poll: GET /api/latest/internal/external-db-sync/poller (Bearer)
    Poll->>Queue: CLAIM OutgoingRequest rows (FOR UPDATE SKIP LOCKED)
    alt Direct mode (STACK_EXTERNAL_DB_SYNC_DIRECT=true)
        Poll->>Ext: Apply INSERT/UPDATE/DELETE to external Postgres
    else Upstash/Publish mode
        Poll->>Ext: Publish batch payload to Upstash
    end
    Poll->>DB: DELETE/UPDATE OutgoingRequest (finished)
    Poll-->>Cron: { ok, requests_processed }
Loading
sequenceDiagram
    participant API as App API (CRUD)
    participant SyncLib as external-db-sync lib
    participant DB as Internal DB
    participant Queue as OutgoingRequest Queue

    API->>DB: Update ProjectUser / ContactChannel
    API->>SyncLib: withExternalDbSyncUpdate(wrapper)
    SyncLib->>DB: Set shouldUpdateSequenceId = true / update sequence metadata
    API->>SyncLib: markProjectUserForExternalDbSync(tenancyId, projectUserId)
    SyncLib->>Queue: enqueueExternalDbSync(tenancyId) -> INSERT OutgoingRequest (dedup)
    SyncLib-->>API: return updated entity
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through rows and metadata bright,

Sequencer counted, poller hummed at night,
I nudged queues gently, sent payloads on their way,
External tables smiled at the break of day,
Hooray — the sync now dances in my fluffy light.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'External db sync' is vague and generic. While it relates to the main changeset, it lacks specificity and doesn't clearly convey the scope of this substantial multi-component system addition. Enhance the title to be more descriptive, e.g., 'Add external database synchronization pipeline with sequencer, poller, and sync engine' or 'Implement cron-driven external DB sync with global sequencing'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and well-structured, covering objectives, implementation details, review guidance, and includes visual documentation. However, it does not follow the repository's template format (which only references CONTRIBUTING.md guidelines).

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch external-db-sync

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.

Comment thread apps/backend/prisma/migrations/20251125030551_add_sequence_id/migration.sql Outdated
@N2D4 N2D4 marked this pull request as ready for review November 30, 2025 23:42
Copilot AI review requested due to automatic review settings November 30, 2025 23:42
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Nov 30, 2025

Claude Code is working…

I'll analyze this and get back to you.

View job run

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a comprehensive external database synchronization system to replicate data from Stack's internal database to user-configured external databases in near real-time.

Key changes:

  • Adds sequence ID tracking for ProjectUser, ContactChannel, and DeletedRow tables
  • Implements a sequencer that assigns sequence IDs to track change order
  • Implements a poller that queues sync requests via QStash
  • Implements a sync engine that pulls changes and pushes them to external databases

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
vercel.json Adds cron jobs for sequencer and poller endpoints (runs every minute)
pnpm-lock.yaml Updates package dependencies including pg client library
package.json Adds @types/pg dependency for TypeScript support
apps/e2e/package.json Adds pg and @types/pg for E2E testing
docker/dependencies/docker.compose.yaml Adds external-db-test PostgreSQL service for testing
apps/backend/prisma/schema.prisma Adds sequenceId fields, OutgoingRequest and DeletedRow tables
apps/backend/prisma/migrations/* Database migrations for sequence IDs, triggers, and sync tables
packages/stack-shared/src/config/schema.ts Adds dbSync configuration schema for external databases
packages/stack-shared/src/config/db-sync-mappings.ts Defines default SQL mappings for PartialUsers table
apps/backend/src/lib/external-db-sync.ts Core sync logic for pushing data to external databases
apps/backend/src/app/api/latest/internal/external-db-sync/* API endpoints for sequencer, poller, and sync engine
apps/backend/scripts/run-cron-jobs.ts Local development script to simulate cron jobs
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-*.test.ts Comprehensive E2E tests for basic, advanced, and race condition scenarios
apps/backend/src/route-handlers/smart-route-handler.tsx Removes extensive logging from API requests
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vercel.json
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts Outdated
Comment thread apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts Outdated
Comment thread apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-advanced.test.ts Outdated
Comment thread packages/stack-shared/src/config/schema.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Nov 30, 2025

Greptile Overview

Greptile Summary

This PR implements a comprehensive external database synchronization system that continuously replicates changes from Stack's internal database to user-configured external databases. The implementation uses a sequence-based approach with four main components: a sequencer that assigns monotonically increasing IDs to changed rows, a poller that queues sync requests, a QStash-based queue for reliable delivery, and a sync engine that executes the actual data transfer using user-defined SQL mappings.

Key Changes:

  • Added sequenceId column to ProjectUser, ContactChannel, and DeletedRow tables with unique indexes
  • Created DeletedRow table to track deletions with triggers on ProjectUser and ContactChannel
  • Created OutgoingRequest table to queue sync operations
  • Implemented database triggers to automatically mark changed rows for sequencing
  • Added backfill_null_sequence_ids() function that processes up to 1000 rows per call and enqueues tenant syncs
  • Built sequencer and poller endpoints that run as Vercel cron jobs every minute (looping every 50ms internally)
  • Implemented sync engine that executes user-configured SQL queries to fetch and upsert data
  • Added comprehensive test suite covering basic operations, race conditions, and high-volume scenarios
  • Configured default PartialUsers mapping that syncs user and contact channel data

Architecture Strengths:

  • Sequence IDs provide total ordering of changes, preventing out-of-order updates
  • FOR UPDATE SKIP LOCKED prevents duplicate processing across concurrent workers
  • Triggers ensure no changes are missed, even during failures
  • QStash provides reliable async execution with retries
  • Comprehensive test coverage including race conditions and multi-tenant isolation

Concerns:

  • Uses $queryRawUnsafe with user-configured SQL queries (line 133 in external-db-sync.ts), though this is admin-controlled configuration
  • No rate limiting on external database connections
  • Error handling swallows some errors rather than alerting (e.g., sync failures just log to console)
  • The cron jobs run every minute but loop internally for 2 minutes, which could cause overlapping executions

Confidence Score: 3/5

  • This PR introduces a complex sync system with potential SQL injection risks, though mitigated by admin-only configuration
  • The implementation is architecturally sound with proper sequence-based ordering, concurrency controls, and comprehensive tests. However, the use of $queryRawUnsafe with user-configured SQL (even from admin config) presents a security concern. The overlapping cron execution pattern (2-minute loops every 1 minute) could cause resource issues. Error handling silently logs failures rather than alerting operators. These issues don't make it unsafe to merge but warrant attention.
  • apps/backend/src/lib/external-db-sync.ts requires attention for SQL injection mitigation and error handling improvements

Important Files Changed

File Analysis

Filename Score Overview
apps/backend/prisma/migrations/20251128210004_add_sync_functions/migration.sql 5/5 Added backfill function and tenant sync queue function for processing changes
apps/backend/src/lib/external-db-sync.ts 3/5 Core sync engine that pulls data from internal DB and pushes to external databases - has SQL injection concern
apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts 5/5 Sequencer endpoint that assigns sequence IDs to changed rows every 50ms
apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts 5/5 Poller endpoint that processes outgoing sync requests and publishes to QStash
apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx 5/5 Webhook endpoint triggered by QStash to execute actual database sync
packages/stack-shared/src/config/db-sync-mappings.ts 5/5 Default PartialUsers mapping with queries for fetching and upserting sync data

Sequence Diagram

sequenceDiagram
    participant User as User Action
    participant Internal as Internal DB
    participant Trigger as DB Triggers
    participant Sequencer as Sequencer<br/>(Cron: Every 1min)
    participant Poller as Poller<br/>(Cron: Every 1min)
    participant Queue as OutgoingRequest<br/>Table
    participant QStash as QStash Queue
    participant SyncEngine as Sync Engine
    participant External as External DB

    User->>Internal: INSERT/UPDATE/DELETE User/Contact
    Internal->>Trigger: Fire trigger
    Trigger->>Internal: Set shouldUpdateSequenceId=TRUE
    
    Note over Sequencer: Runs every 50ms for 2min
    Sequencer->>Internal: Call backfill_null_sequence_ids()
    Internal->>Internal: Assign sequenceId to changed rows
    Internal->>Internal: Set shouldUpdateSequenceId=FALSE
    Internal->>Queue: INSERT into OutgoingRequest<br/>(via enqueue_tenant_sync)
    
    Note over Poller: Runs every 50ms for 2min
    Poller->>Queue: SELECT pending requests<br/>(FOR UPDATE SKIP LOCKED)
    Queue->>Poller: Return up to 100 requests
    Poller->>Queue: UPDATE fulfilledAt=NOW()
    Poller->>QStash: publishJSON(sync-engine endpoint)
    
    QStash->>SyncEngine: POST /sync-engine
    SyncEngine->>SyncEngine: Verify Upstash signature
    SyncEngine->>Internal: Execute internalDbFetchQuery<br/>(with tenancyId parameter)
    Internal->>SyncEngine: Return changed rows (ordered by sequenceId)
    SyncEngine->>External: Connect to external DB
    SyncEngine->>External: Create table if needed<br/>(targetTableSchema)
    loop For each row
        SyncEngine->>External: Execute externalDbUpdateQuery<br/>(upsert with sequence check)
    end
    External->>External: Update only if newSeq > oldSeq
    SyncEngine->>QStash: Return success

Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

27 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
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: 10

🧹 Nitpick comments (20)
apps/backend/prisma/migrations/20251125030551_add_sequence_id/migration.sql (1)

1-6: Consider documenting the sequence increment strategy.

The INCREMENT BY 11 in the global sequence is unusual and likely implements a per-instance ID distribution strategy for multi-instance deployments. Add an inline SQL comment explaining this choice for future maintainers.

Ref: Past review feedback suggested consolidating all sequence-related migrations and documenting each step. Consider grouping this with the subsequent shouldUpdateSequenceId and index migrations into a single, well-documented migration file.

-- Add this comment to explain the strategy
CREATE SEQUENCE global_seq_id
    AS BIGINT
    START 1
    INCREMENT BY 11  -- Increment by 11 to enable per-instance ID ranges (e.g., instance 0: 1,12,23...; instance 1: 2,13,24...) in multi-instance deployments
    NO MINVALUE
    NO MAXVALUE;
apps/backend/prisma/migrations/20251128210001_add_should_update_sequence_id_indexes/migration.sql (1)

1-7: LGTM!

The partial indexes on shouldUpdateSequenceId = TRUE are an efficient choice for queries filtering unsynced records, reducing index size by excluding already-synced rows. The naming and SPLIT_STATEMENT_SENTINEL structure are consistent with related migrations.

Consider adding an inline SQL comment explaining the partial index strategy:

-- Partial index for efficient querying of unsynced records
CREATE INDEX "ProjectUser_shouldUpdateSequenceId_idx" ON "ProjectUser"("shouldUpdateSequenceId") WHERE "shouldUpdateSequenceId" = TRUE;
apps/backend/prisma/migrations/20251128210000_add_should_update_sequence_id_columns/migration.sql (1)

1-7: LGTM!

The addition of shouldUpdateSequenceId with default TRUE across ProjectUser, ContactChannel, and DeletedRow ensures all existing records are marked for synchronization to external databases. This flag enables efficient querying of unsynced records paired with the partial indexes in the follow-up migration.

Add inline SQL comments to clarify the column's purpose:

-- Track which records need to be synced to external databases (reset to FALSE after sync)
ALTER TABLE "ProjectUser" ADD COLUMN "shouldUpdateSequenceId" BOOLEAN NOT NULL DEFAULT TRUE;
apps/backend/prisma/migrations/20251125180000_add_deleted_row_table/migration.sql (1)

8-9: Consider data retention strategy for fulfilled deletions.

The fulfilledAt timestamp tracks when deletions have been synced, but there's no documented retention policy or cleanup process for old rows. Over time, the DeletedRow table could accumulate millions of fulfilled records, impacting query performance and storage.

Recommend documenting a data retention policy (e.g., "delete rows where fulfilledAt < NOW() - INTERVAL '30 days'") and implementing a cleanup job.

apps/backend/prisma/migrations/20251125172224_add_outgoing_request/migration.sql (1)

2-12: Consider adding an index on createdAt for query performance.

The poller route orders by createdAt when claiming pending requests (ORDER BY "createdAt"). As the table grows, this ordering operation on unfulfilled requests could benefit from a composite index.

 CREATE INDEX  "OutgoingRequest_fulfilledAt_idx" ON "OutgoingRequest"("fulfilledAt");
+
+CREATE INDEX  "OutgoingRequest_createdAt_idx" ON "OutgoingRequest"("createdAt");

Alternatively, a composite index (fulfilledAt, createdAt) could further optimize the query pattern used in the poller.

apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts (2)

81-87: Fragile environment detection pattern.

Using .includes("development") on the environment string is unusual and could match unintended values. The standard approach is direct equality comparison.

-if (getNodeEnvironment().includes("development") || getNodeEnvironment().includes("test")) {
+const env = getNodeEnvironment();
+if (env === "development" || env === "test") {

74-74: Consider defining a type for qstashOptions.

Casting to any loses type safety. Define an interface for the expected shape of qstashOptions to catch errors at compile time.

interface QstashOptions {
  url: string;
  body: unknown;
}
// ...
const options = request.qstashOptions as QstashOptions;
packages/stack-shared/src/config/schema.ts (1)

11-11: Unused import: yupArray

The import yupArray was added but doesn't appear to be used in this file. Based on the code, only yupTuple is used for array-like structures (Line 216).

-import { productSchema, userSpecifiedIdSchema, yupArray, yupBoolean, yupDate, yupMixed, yupNever, yupNumber, yupObject, yupRecord, yupString, yupTuple, yupUnion } from "../schema-fields";
+import { productSchema, userSpecifiedIdSchema, yupBoolean, yupDate, yupMixed, yupNever, yupNumber, yupObject, yupRecord, yupString, yupTuple, yupUnion } from "../schema-fields";
apps/backend/scripts/run-cron-jobs.ts (2)

25-29: Cron jobs don't run immediately on startup.

The setInterval schedules jobs to run after the first 60-second delay. Consider whether the jobs should also run immediately when the script starts:

  for (const endpoint of endpoints) {
+   run(endpoint); // Run immediately on startup
    setInterval(() => {
      run(endpoint);
    }, 60000);
  }

16-23: Consider adding a timeout to the fetch request.

The fetch call has no timeout. If the backend hangs, this could cause the cron job to stall indefinitely. Per coding guidelines, blocking calls without timeouts can cause cascading issues.

  const run = (endpoint: string) => runAsynchronously(async () => {
    console.log(`Running ${endpoint}...`);
-   const res = await fetch(`${baseUrl}${endpoint}`, {
-     headers: { 'Authorization': `Bearer ${cronSecret}` },
-   });
+   const controller = new AbortController();
+   const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 minute timeout
+   try {
+     const res = await fetch(`${baseUrl}${endpoint}`, {
+       headers: { 'Authorization': `Bearer ${cronSecret}` },
+       signal: controller.signal,
+     });
+     if (!res.ok) throw new StackAssertionError(`Failed to call ${endpoint}: ${res.status} ${res.statusText}\n${await res.text()}`, { res });
+     console.log(`${endpoint} completed.`);
+   } finally {
+     clearTimeout(timeoutId);
+   }
-   if (!res.ok) throw new StackAssertionError(`Failed to call ${endpoint}: ${res.status} ${res.statusText}\n${await res.text()}`, { res });
-   console.log(`${endpoint} completed.`);
  });
apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts (1)

50-65: Consider adding a circuit breaker for repeated failures.

The loop catches errors and continues (Lines 53-55), which could lead to repeatedly hitting the database with failing queries for the full 2-minute duration. Consider tracking consecutive failures and breaking early:

    let iterations = 0;
+   let consecutiveFailures = 0;
+   const maxConsecutiveFailures = 10;

    while (performance.now() - startTime < maxDurationMs) {
      try {
        await globalPrismaClient.$executeRaw`SELECT backfill_null_sequence_ids()`;
+       consecutiveFailures = 0; // Reset on success
      } catch (error) {
        console.warn('[sequencer] Failed to run backfill_null_sequence_ids:', error);
+       consecutiveFailures++;
+       if (consecutiveFailures >= maxConsecutiveFailures) {
+         console.error('[sequencer] Too many consecutive failures, breaking loop');
+         break;
+       }
      }

      iterations++;
      // ...
    }
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts (1)

298-349: Consider parameterizing the long sleep duration.

The 70-second sleep makes this test slow for CI. If the poller interval is configurable in tests, you could reduce the wait time proportionally. Alternatively, mark this as a "slow test" that runs only in nightly builds.

apps/backend/prisma/schema.prisma (1)

895-911: Consider adding an index on sequenceId for DeletedRow if sync performance becomes a concern.

The model has a partial index on shouldUpdateSequenceId (migration 20251128210001) that helps the backfill function. The sync engine's query filters by tenancyId and tableName, then orders by sequenceId. While the @unique constraint on sequenceId provides an index, a composite index on (tenancyId, tableName, sequenceId) could optimize the sync fetch query if DeletedRow volumes grow significantly. This is an optional optimization rather than a requirement.

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-advanced.test.ts (3)

2-2: Unused import: generateSecureRandomString

This import is not used anywhere in the file.

-import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto';

390-390: Missing test timeout

This test lacks an explicit timeout, unlike other tests in this file. Consider adding TEST_TIMEOUT for consistency and to prevent hanging tests.

-  });
+  }, TEST_TIMEOUT);

1011-1011: Prefer slice() over deprecated substr()

substr() is deprecated in favor of slice() or substring().

-    const testRunId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+    const testRunId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
apps/backend/src/lib/external-db-sync.ts (3)

48-69: Row-by-row processing may cause performance bottleneck

Each row is processed with a separate await externalClient.query() call. For high-volume syncs (e.g., the 200-user test), this results in 200+ sequential round-trips to the external database.

Consider batching multiple rows into a single transaction or using multi-value inserts for better throughput.

+  // Batch rows within a transaction for better performance
+  await externalClient.query('BEGIN');
+  try {
     for (const row of newRows) {
       const { tenancyId, ...rest } = row;
       await externalClient.query(upsertQuery, Object.values(rest));
     }
+    await externalClient.query('COMMIT');
+  } catch (err) {
+    await externalClient.query('ROLLBACK');
+    throw err;
+  }

82-90: Type safety: Excessive as any casts

Using as any to access sourceTables and targetTablePrimaryKey bypasses TypeScript's type checking. If the CompleteConfig type is correct, consider updating it to include these properties properly, or use type guards.


192-203: Sequential database syncing limits throughput

Each external database is synced sequentially. For tenants with multiple external DBs configured, this could be parallelized with Promise.all() to reduce total sync time.

-  for (const [dbId, dbConfig] of Object.entries(externalDatabases)) {
-    await syncDatabase(dbId, dbConfig, internalPrisma, tenancy.id);
-  }
+  await Promise.all(
+    Object.entries(externalDatabases).map(([dbId, dbConfig]) =>
+      syncDatabase(dbId, dbConfig, internalPrisma, tenancy.id)
+    )
+  );
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts (1)

209-215: Consider adding type for externalDatabases parameter

The externalDatabases parameter is typed as any. Consider using the proper type from the config schema for better type safety.

-export async function createProjectWithExternalDb(externalDatabases: any, projectOptions?: { display_name?: string, description?: string }) {
+export async function createProjectWithExternalDb(
+  externalDatabases: Record<string, { type: string; connectionString: string; mappings?: unknown }>,
+  projectOptions?: { display_name?: string; description?: string }
+) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb5e1cb and 3f2a8ef.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • apps/backend/package.json (2 hunks)
  • apps/backend/prisma/migrations/20251125030551_add_sequence_id/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251125172224_add_outgoing_request/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251125180000_add_deleted_row_table/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251128210000_add_should_update_sequence_id_columns/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251128210001_add_should_update_sequence_id_indexes/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251128210002_add_reset_sequence_id_function_and_triggers/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251128210003_log_deleted_row_function_and_triggers/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251128210004_add_sync_functions/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (4 hunks)
  • apps/backend/scripts/run-cron-jobs.ts (1 hunks)
  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts (1 hunks)
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts (1 hunks)
  • apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx (1 hunks)
  • apps/backend/src/lib/external-db-sync.ts (1 hunks)
  • apps/backend/src/route-handlers/smart-route-handler.tsx (1 hunks)
  • apps/e2e/package.json (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-advanced.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts (1 hunks)
  • docker/dependencies/docker.compose.yaml (2 hunks)
  • package.json (1 hunks)
  • packages/stack-shared/src/config/db-sync-mappings.ts (1 hunks)
  • packages/stack-shared/src/config/schema-fuzzer.test.ts (1 hunks)
  • packages/stack-shared/src/config/schema.ts (4 hunks)
  • vercel.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: For blocking alerts and errors, never use toast, as they are easily missed by the user. Instead, use alerts
Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition; apply transitions after the action instead
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). Use runAsynchronously or runAsynchronouslyWithAlert instead for asynchronous error handling
When creating hover transitions, avoid hover-enter transitions and just use hover-exit transitions (e.g., transition-colors hover:transition-none)
Use ES6 maps instead of records wherever possible

Files:

  • packages/stack-shared/src/config/schema-fuzzer.test.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
  • packages/stack-shared/src/config/db-sync-mappings.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts
  • apps/backend/src/route-handlers/smart-route-handler.tsx
  • apps/backend/scripts/run-cron-jobs.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/external-db-sync-utils.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-advanced.test.ts
  • packages/stack-shared/src/config/schema.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

When writing tests, prefer .toMatchInlineSnapshot over other selectors, if possible

Files:

  • packages/stack-shared/src/config/schema-fuzzer.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.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-advanced.test.ts
apps/backend/src/app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

The project uses a custom route handler system in the backend for consistent API responses

Files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts
apps/{backend,dashboard}/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if you can avoid them. Instead, prefer using a client component and prefer usePathname instead of await params

Files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx
  • apps/backend/src/route-handlers/smart-route-handler.tsx
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts
  • apps/backend/src/lib/external-db-sync.ts
packages/stack-shared/src/config/schema.ts

📄 CodeRabbit inference engine (AGENTS.md)

Whenever making backwards-incompatible changes to the config schema, update the migration functions in packages/stack-shared/src/config/schema.ts

Files:

  • packages/stack-shared/src/config/schema.ts
🧠 Learnings (4)
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Applies to packages/stack-shared/src/config/schema.ts : Whenever making backwards-incompatible changes to the config schema, update the migration functions in `packages/stack-shared/src/config/schema.ts`

Applied to files:

  • packages/stack-shared/src/config/schema-fuzzer.test.ts
  • packages/stack-shared/src/config/db-sync-mappings.ts
  • apps/backend/prisma/migrations/20251128210004_add_sync_functions/migration.sql
  • apps/backend/package.json
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/lib/external-db-sync.ts
  • packages/stack-shared/src/config/schema.ts
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Applies to apps/backend/src/app/api/**/*.{ts,tsx} : The project uses a custom route handler system in the backend for consistent API responses

Applied to files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
  • apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx
  • apps/backend/src/route-handlers/smart-route-handler.tsx
  • apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Always add new E2E tests when changing the API or SDK interface

Applied to files:

  • apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Applies to apps-{frontend,config}.ts{,x} : To update the list of apps available, edit `apps-frontend.tsx` and `apps-config.ts`

Applied to files:

  • packages/stack-shared/src/config/schema.ts
🧬 Code graph analysis (6)
apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx (5)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
  • createSmartRouteHandler (185-270)
packages/stack-shared/src/schema-fields.ts (5)
  • yupObject (247-251)
  • yupTuple (217-221)
  • yupString (187-190)
  • yupNumber (191-194)
  • yupBoolean (195-198)
apps/backend/src/lib/upstash.tsx (1)
  • ensureUpstashSignature (16-35)
apps/backend/src/lib/tenancies.tsx (1)
  • getTenancy (82-91)
apps/backend/src/lib/external-db-sync.ts (1)
  • syncExternalDatabases (192-203)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts (2)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts (6)
  • TestDbManager (16-67)
  • createProjectWithExternalDb (209-215)
  • waitForTable (150-169)
  • waitForCondition (73-88)
  • TEST_TIMEOUT (10-10)
  • waitForSyncedDeletion (125-145)
apps/e2e/tests/backend/backend-helpers.ts (1)
  • niceBackendFetch (109-173)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts (2)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts (9)
  • TestDbManager (16-67)
  • createProjectWithExternalDb (209-215)
  • waitForSyncedData (93-120)
  • verifyInExternalDb (182-189)
  • TEST_TIMEOUT (10-10)
  • waitForSyncedDeletion (125-145)
  • verifyNotInExternalDb (174-177)
  • waitForTable (150-169)
  • waitForCondition (73-88)
apps/e2e/tests/backend/backend-helpers.ts (1)
  • niceBackendFetch (109-173)
apps/backend/scripts/run-cron-jobs.ts (3)
packages/stack-shared/src/utils/env.tsx (1)
  • getEnvVariable (16-58)
packages/stack-shared/src/utils/promises.tsx (1)
  • runAsynchronously (343-366)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-advanced.test.ts (2)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts (9)
  • TestDbManager (16-67)
  • createProjectWithExternalDb (209-215)
  • waitForCondition (73-88)
  • TEST_TIMEOUT (10-10)
  • waitForTable (150-169)
  • waitForSyncedData (93-120)
  • HIGH_VOLUME_TIMEOUT (11-11)
  • waitForSyncedDeletion (125-145)
  • verifyNotInExternalDb (174-177)
packages/stack-shared/src/config/db-sync-mappings.ts (1)
  • DEFAULT_DB_SYNC_MAPPINGS (1-166)
packages/stack-shared/src/config/schema.ts (2)
packages/stack-shared/src/schema-fields.ts (5)
  • yupObject (247-251)
  • yupRecord (283-322)
  • userSpecifiedIdSchema (442-442)
  • yupString (187-190)
  • yupTuple (217-221)
packages/stack-shared/src/config/db-sync-mappings.ts (1)
  • DEFAULT_DB_SYNC_MAPPINGS (1-166)
🪛 GitHub Check: restart-dev-and-test
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts

[warning] 334-334:
Trailing spaces not allowed


[warning] 334-334:
Trailing spaces not allowed

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts

[failure] 223-223:
Expected indentation of 2 spaces but found 4


[failure] 222-222:
Expected indentation of 4 spaces but found 6


[failure] 221-221:
Expected indentation of 2 spaces but found 4


[warning] 31-31:
Trailing spaces not allowed


[failure] 223-223:
Expected indentation of 2 spaces but found 4


[failure] 222-222:
Expected indentation of 4 spaces but found 6


[failure] 221-221:
Expected indentation of 2 spaces but found 4


[warning] 31-31:
Trailing spaces not allowed

🪛 GitHub Check: restart-dev-and-test-with-custom-base-port
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts

[warning] 334-334:
Trailing spaces not allowed


[warning] 334-334:
Trailing spaces not allowed

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts

[failure] 223-223:
Expected indentation of 2 spaces but found 4


[failure] 222-222:
Expected indentation of 4 spaces but found 6


[failure] 221-221:
Expected indentation of 2 spaces but found 4


[warning] 31-31:
Trailing spaces not allowed


[failure] 223-223:
Expected indentation of 2 spaces but found 4


[failure] 222-222:
Expected indentation of 4 spaces but found 6


[failure] 221-221:
Expected indentation of 2 spaces but found 4


[warning] 31-31:
Trailing spaces not allowed

🪛 GitHub Check: setup-tests
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts

[warning] 334-334:
Trailing spaces not allowed


[warning] 334-334:
Trailing spaces not allowed

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts

[failure] 223-223:
Expected indentation of 2 spaces but found 4


[failure] 222-222:
Expected indentation of 4 spaces but found 6


[failure] 221-221:
Expected indentation of 2 spaces but found 4


[warning] 31-31:
Trailing spaces not allowed


[failure] 223-223:
Expected indentation of 2 spaces but found 4


[failure] 222-222:
Expected indentation of 4 spaces but found 6


[failure] 221-221:
Expected indentation of 2 spaces but found 4


[warning] 31-31:
Trailing spaces not allowed

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: CodeQL analysis (javascript-typescript)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: claude-review
🔇 Additional comments (38)
package.json (1)

68-68: LGTM!

The addition of @types/pg to the monorepo root devDependencies provides TypeScript type support for PostgreSQL interactions across backend migrations, utilities, and end-to-end tests introduced in this PR.

apps/e2e/package.json (1)

21-23: LGTM!

The additions of pg and @types/pg to e2e devDependencies enable end-to-end tests to interact with external PostgreSQL databases provisioned by the new external-db-test service. Version pinning is consistent with the monorepo root.

docker/dependencies/docker.compose.yaml (1)

207-231: LGTM!

The new external-db-test PostgreSQL service follows the established patterns used by other test databases in the compose file (port mapping with NEXT_PUBLIC_STACK_PORT_PREFIX, volume persistence, hardcoded dev credentials). This isolates external DB sync tests from the main database and enables reproducible e2e testing.

apps/backend/package.json (2)

10-10: LGTM!

The addition of run-cron-jobs to the dev concurrency labels integrates the background cron scheduler into local development, enabling testing of the sequencer and poller workflows that drive external DB synchronization.

Please verify that scripts/run-cron-jobs.ts properly handles process signals (SIGTERM, SIGINT) so that pnpm run dev terminates cleanly without leaving orphaned cron processes.


38-39: LGTM!

The run-cron-jobs script follows the established pattern: wrapped with with-env for environment setup and uses tsx for TypeScript execution. Dependencies (pg and tsx) are already present in the file.

apps/backend/prisma/migrations/20251125180000_add_deleted_row_table/migration.sql (1)

15-19: LGTM!

The indexes on tenancyId, tableName, and sequenceId are well-chosen for the expected query patterns: filtering by tenant, scanning deleted rows from specific tables, and correlating with sequence IDs for sync ordering.

vercel.json (1)

2-11: LGTM!

The cron configuration correctly schedules both the poller and sequencer endpoints to run every minute. This aligns with the internal route implementations that handle authentication via CRON_SECRET.

packages/stack-shared/src/config/schema-fuzzer.test.ts (1)

52-72: LGTM!

The dbSync fuzzer configuration properly mirrors the new schema structure with representative test values for all fields including externalDatabases, mappings, and their nested properties.

apps/backend/src/route-handlers/smart-route-handler.tsx (1)

101-102: Verify the logging removal is intentional.

Based on the AI summary, this change removes development-time logging including request/response details and timing measurements. While this reduces log noise, ensure you have alternative observability (e.g., via Sentry spans at lines 71-80 and 232-248) to debug production issues.

apps/backend/prisma/migrations/20251128210003_log_deleted_row_function_and_triggers/migration.sql (2)

44-54: Triggers are correctly attached.

Both triggers are appropriately configured as BEFORE DELETE FOR EACH ROW, which ensures the OLD record is available for logging before the actual deletion occurs.


2-42: Well-structured trigger function for audit logging.

The implementation correctly uses BEFORE DELETE to capture OLD row data and dynamically extracts primary key columns via pg_index/pg_attribute. Both ProjectUser and ContactChannel tables have the required tenancyId column, so the function will work as implemented. However, a couple of observations remain:

  1. If additional tables are added to this trigger function in the future without a tenancyId column, the function will fail at runtime. Consider documenting this requirement or adding a check.
  2. The dynamic PK extraction loop runs on every delete. For high-volume deletes, consider whether this overhead is acceptable or if caching the PK structure would be beneficial.
packages/stack-shared/src/config/schema.ts (2)

204-223: New dbSync schema added to branch config.

The schema structure looks reasonable. A few observations:

  1. connectionString is marked as defined() (required), but the defaults (Lines 594-595) set type and connectionString to undefined. This could cause validation issues when a database entry exists but isn't fully configured.

  2. The targetTablePrimaryKey uses yupTuple([yupString().defined()]) which only allows exactly one string element. If composite primary keys with multiple columns are needed, consider using yupArray(yupString().defined()) instead.

  3. Per the coding guidelines for this file, backwards-incompatible changes to the config schema require migration functions in migrateConfigOverride. Since this is a new field with defaults, existing configs should remain valid, but please confirm this is intentional.

Based on learnings, whenever making backwards-incompatible changes to the config schema, update the migration functions. Please verify that adding dbSync doesn't require a migration for existing projects.


591-598: Defaults for dbSync.externalDatabases look consistent with the schema.

The default factory function returns undefined for type and connectionString, with DEFAULT_DB_SYNC_MAPPINGS for mappings. This aligns with the schema where these fields are optional until explicitly configured.

apps/backend/scripts/run-cron-jobs.ts (1)

32-36: Error handling for main() follows project conventions.

The runAsynchronously usage for the run function and the .catch() on main() with process.exit(1) is appropriate for a standalone script. The eslint disable comment acknowledges the intentional deviation.

apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx (1)

7-31: Route handler structure follows project conventions.

The use of createSmartRouteHandler with metadata, request/response schemas aligns with the project's custom route handler system. The Upstash signature validation via ensureUpstashSignature is appropriate for webhook security.

apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts (2)

38-42: Auth check is straightforward and correct.

The Bearer token comparison against CRON_SECRET is appropriate for internal cron-triggered endpoints. The 401 StatusError on mismatch is the correct response.


14-37: Route handler metadata and schema are well-defined.

The hidden: true flag appropriately hides this internal endpoint from public documentation. The response schema correctly reflects the returned structure.

apps/backend/prisma/migrations/20251128210002_add_reset_sequence_id_function_and_triggers/migration.sql (1)

1-29: LGTM!

The trigger logic correctly re-flags rows for sequence ID updates: when a processed row (shouldUpdateSequenceId = FALSE) gets modified, the trigger resets it to TRUE so the sequencer picks it up again. The BEFORE UPDATE timing ensures the flag is set before the row is committed.

apps/backend/prisma/migrations/20251128210004_add_sync_functions/migration.sql (1)

24-99: LGTM! Batched processing with concurrency-safe locking.

The FOR UPDATE SKIP LOCKED pattern correctly handles concurrent sequencer invocations without deadlocks. Each table is limited to 1000 rows per invocation—ensure the sequencer cron runs frequently enough (50ms as documented) to drain backlogs during high-write periods.

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts (1)

222-750: Well-structured race condition test suite.

The tests comprehensively cover critical scenarios: uncommitted visibility, commit ordering, multi-row transactions, and sequence ID precedence. The finally blocks ensure proper client cleanup even on test failures.

packages/stack-shared/src/config/db-sync-mappings.ts (2)

77-164: Correct sequence-based conflict resolution.

The asymmetric comparison (>= for deletes, > for upserts) ensures delete events at the same sequence ID take precedence—preventing stale data resurrection. The ON CONFLICT clause's final guard (EXCLUDED."sequenceId" > "PartialUsers"."sequenceId") adds a second layer of protection against out-of-order processing.


1-76: LGTM!

The mapping correctly handles the BooleanTrue enum conversion (line 34) and uses GREATEST() to track the maximum sequence ID across the joined tables. The LIMIT 1000 ORDER BY sequenceId enables paginated incremental fetching.

apps/backend/prisma/schema.prisma (1)

176-178: LGTM!

The schema additions are well-designed: @unique on sequenceId prevents duplicates, @default(true) on shouldUpdateSequenceId ensures new rows are automatically queued for sequencing, and the OutgoingRequest index on fulfilledAt supports efficient pending-request lookups.

Also applies to: 258-260, 884-893

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-advanced.test.ts (6)

19-29: LGTM!

Test setup and teardown are properly structured with describe.sequential to avoid concurrency issues, and the TestDbManager lifecycle is correctly managed.


39-157: LGTM!

Multi-tenant isolation test is thorough - it properly validates that User A only appears in Project A's databases and User B only in Project B's, with explicit negative assertions to confirm no cross-tenant leakage.


167-237: LGTM!

The sequenceId tracking test correctly validates ordering guarantees using BigInt for comparison, which is appropriate for potentially large sequence values.


434-505: LGTM!

The high-volume batching test is well-designed with retry logic, unique email generation, and appropriate timeout handling for the 200-user stress test.


625-692: LGTM!

The write protection test properly validates that readonly PostgreSQL credentials prevent unauthorized modifications while still allowing reads - this is a valuable security verification.


883-980: LGTM!

The complex lifecycle test thoroughly exercises the full create → update → update → delete → recreate → update sequence with proper sequenceId tracking and ID verification at each step.

apps/backend/src/lib/external-db-sync.ts (1)

6-8: LGTM!

Simple and clean configuration accessor function.

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts (5)

1-17: LGTM!

Imports and test suite setup are correctly structured with describe.sequential to avoid shared state issues.


37-60: LGTM!

Insert test correctly validates that new users appear in the external DB after sync completes.


113-150: LGTM!

Delete test properly validates the full deletion flow including verifying 404 response from the internal API before checking external DB sync.


304-333: LGTM!

Resilience test is valuable - it confirms that one bad database configuration doesn't block synchronization to healthy databases.


520-565: LGTM!

The race condition protection test cleverly simulates an out-of-order delete by directly issuing a conditional DELETE against the external DB, validating that sequence-based guards work correctly.

apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts (3)

16-67: LGTM!

TestDbManager class is well-designed with proper resource management - unique database names prevent collisions, and cleanup safely handles errors during database drops.


73-88: LGTM!

Generic polling helper with configurable timeout, interval, and descriptive error messages - clean and reusable.


93-120: LGTM!

waitForSyncedData properly handles the case where the table doesn't exist yet (error code 42P01) and supports optional name verification.

Comment thread apps/backend/prisma/migrations/20251125180000_add_deleted_row_table/migration.sql Outdated
Comment thread apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts Outdated
Comment thread apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts Outdated
Comment thread apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts
Comment thread apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts Outdated
Comment thread apps/backend/src/app/api/latest/internal/external-db-sync/sync-engine/route.tsx Outdated
Comment thread apps/backend/scripts/run-cron-jobs.ts
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 left a comment

Choose a reason for hiding this comment

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

this looks awesome! i have a lot of comments but overall this is very exciting :]

- i really like how much of this PR is tests (almost two-thirds of all lines of code), made it very pleasant to review :]
- important: every time something is different than what we expect (eg. some assertion is not true, etc) it should always be loud and annoying. the worst thing that could happen is that an error is quiet, bothering users but not appearing in our alerts on sentry. throw new StackAssertionError​ is the preferred way to do this (or the throwErr​ function which does the same), with captureError for the cases where that doesn't work.
- good job on separating things into files! this folder structure makes a lot of sense, i think
- make sure to really understand the security sensitive parts of the PR. in particular i think the "grant access to the Users table to everyone" in the table schema query and the custom configs are really dangerous, a less thorough reviewer may have missed them

Comment thread apps/backend/prisma/migrations/20251128210004_add_sync_functions/migration.sql Outdated
Comment thread apps/backend/prisma/schema.prisma Outdated
Comment thread apps/backend/prisma/schema.prisma Outdated
Comment thread apps/backend/prisma/schema.prisma Outdated
Comment thread apps/backend/prisma/schema.prisma Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts Outdated
Comment thread apps/backend/src/lib/external-db-sync.ts
Comment thread apps/backend/src/lib/external-db-sync.ts
@N2D4 N2D4 requested a review from BilalG1 December 1, 2025 01:57
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f2a8ef and dcda402.

📒 Files selected for processing (1)
  • apps/backend/scripts/run-cron-jobs.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: For blocking alerts and errors, never use toast, as they are easily missed by the user. Instead, use alerts
Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition; apply transitions after the action instead
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). Use runAsynchronously or runAsynchronouslyWithAlert instead for asynchronous error handling
When creating hover transitions, avoid hover-enter transitions and just use hover-exit transitions (e.g., transition-colors hover:transition-none)
Use ES6 maps instead of records wherever possible

Files:

  • apps/backend/scripts/run-cron-jobs.ts
🧬 Code graph analysis (1)
apps/backend/scripts/run-cron-jobs.ts (4)
packages/stack-shared/src/utils/env.tsx (1)
  • getEnvVariable (16-58)
apps/dashboard/src/components/vibe-coding/chat-adapters.ts (1)
  • run (22-59)
packages/stack-shared/src/utils/promises.tsx (2)
  • runAsynchronously (343-366)
  • wait (260-268)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: all-good
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: restart-dev-and-test

Comment thread apps/backend/scripts/run-cron-jobs.ts Outdated
aadesh18 and others added 2 commits December 1, 2025 01:44
…er/route.ts

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
…er/route.ts

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
Comment thread apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts Outdated
Comment thread apps/backend/scripts/run-cron-jobs.ts Outdated
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: 1

♻️ Duplicate comments (3)
apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts (3)

46-49: Consider renaming busySleepMs and revisiting the 2‑minute window

Minor/naming and tuning nit:

  • busySleepMs is a bit misleading since this is not a busy-spin; something like pollSleepMs or loopSleepMs would be clearer.
  • Previous feedback suggested a maxDurationMs of ~3 minutes to tolerate worst‑case Vercel cron skew (e.g. invocations at :05 and :06 still overlapping enough to drain backlogs). If that concern still applies, consider bumping from 2 to 3 minutes.

79-85: Document the host.docker.internal rewrite for dev/test

The dev/test rewrite of localhost/127.0.0.1 to host.docker.internal is subtle and environment‑specific. Adding a short comment here explaining that this is required for Dockerized e2e / local QStash to reach the backend from inside the container would help future readers understand why this block exists and avoid accidentally “simplifying” it away.

You might also cache getNodeEnvironment() in a local variable to avoid calling it twice, though that’s purely cosmetic.


51-68: Avoid marking OutgoingRequest as fulfilled before QStash publish succeeds

Right now claimPendingRequests sets fulfilledAt = NOW() in a transaction (lines 54-64), before processRequests actually calls upstash.publishJSON (lines 88-91). If publishJSON fails, the request stays marked as fulfilled and is never retried, so the external DB can silently fall out of sync. This matches prior automated feedback and should be treated as at-least-once delivery instead of maybe-once.

A safer pattern here is:

  • In claimPendingRequests, only select pending rows with FOR UPDATE SKIP LOCKED and do not touch fulfilledAt.
  • After a successful publishJSON call for a given request, set fulfilledAt = NOW() in a separate transaction.
  • Leave failed publishes with fulfilledAt IS NULL so they get picked up again on the next poll.

Concretely:

-    async function claimPendingRequests(): Promise<OutgoingRequest[]> {
-      return await retryTransaction(globalPrismaClient, async (tx) => {
-        const rows = await tx.$queryRaw<OutgoingRequest[]>`
-          UPDATE "OutgoingRequest"
-          SET "fulfilledAt" = NOW()
-          WHERE "id" IN (
-            SELECT id
-            FROM "OutgoingRequest"
-            WHERE "fulfilledAt" IS NULL
-            ORDER BY "createdAt"
-            LIMIT 100
-            FOR UPDATE SKIP LOCKED
-          )
-          RETURNING *;
-        `;
-        return rows;
-      });
-    }
+    async function claimPendingRequests(): Promise<OutgoingRequest[]> {
+      return await retryTransaction(globalPrismaClient, async (tx) => {
+        const rows = await tx.$queryRaw<OutgoingRequest[]>`
+          SELECT "id", "createdAt", "qstashOptions", "fulfilledAt"
+          FROM "OutgoingRequest"
+          WHERE "fulfilledAt" IS NULL
+          ORDER BY "createdAt"
+          LIMIT 100
+          FOR UPDATE SKIP LOCKED;
+        `;
+        return rows;
+      });
+    }

and after a successful publish:

-          await upstash.publishJSON({
-            url: fullUrl,
-            body: options.body,
-          });
-
-          processed++;
+          await upstash.publishJSON({
+            url: fullUrl,
+            body: options.body,
+          });
+
+          // Only mark as fulfilled after successful QStash publish
+          await retryTransaction(globalPrismaClient, async (tx) => {
+            await tx.$queryRaw`
+              UPDATE "OutgoingRequest"
+              SET "fulfilledAt" = NOW()
+              WHERE "id" = ${request.id};
+            `;
+          });
+
+          processed++;

This removes the permanent-loss failure mode; downstream code should already be idempotent given the sequencing/sync use case, but if not, please double-check that repeated publishes are acceptable.

Also applies to: 88-93

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcda402 and f0cc311.

📒 Files selected for processing (1)
  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: For blocking alerts and errors, never use toast, as they are easily missed by the user. Instead, use alerts
Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition; apply transitions after the action instead
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). Use runAsynchronously or runAsynchronouslyWithAlert instead for asynchronous error handling
When creating hover transitions, avoid hover-enter transitions and just use hover-exit transitions (e.g., transition-colors hover:transition-none)
Use ES6 maps instead of records wherever possible

Files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
apps/backend/src/app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

The project uses a custom route handler system in the backend for consistent API responses

Files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
apps/{backend,dashboard}/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if you can avoid them. Instead, prefer using a client component and prefer usePathname instead of await params

Files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
🧠 Learnings (3)
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Applies to apps/backend/src/app/api/**/*.{ts,tsx} : The project uses a custom route handler system in the backend for consistent API responses

Applied to files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Applies to **/*.{ts,tsx} : NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error). Use `runAsynchronously` or `runAsynchronouslyWithAlert` instead for asynchronous error handling

Applied to files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
📚 Learning: 2025-11-28T21:21:39.123Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:21:39.123Z
Learning: Applies to **/*.{ts,tsx} : For blocking alerts and errors, never use `toast`, as they are easily missed by the user. Instead, use alerts

Applied to files:

  • apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: all-good
  • GitHub Check: Vercel Agent Review
  • GitHub Check: restart-dev-and-test

Comment thread apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts Outdated
@BilalG1 BilalG1 requested a review from N2D4 February 4, 2026 17:46
@BilalG1 BilalG1 assigned N2D4 and unassigned aadesh18 Feb 4, 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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docker/dev-postgres-replica/entrypoint.sh (1)

59-61: ⚠️ Potential issue | 🟡 Minor

Add defensive check for pg_stat_statements configuration.

pg_stat_statements is only configured during initial base backup (fresh PGDATA). While the configuration should persist in the named volume across restarts, it's worth adding a defensive warning if the config is missing in the else branch—this helps catch edge cases like accidental file deletion or partial volume resets.

Suggested warning check
 else
     echo "PGDATA already initialized, starting replica..."
+    if ! grep -q "pg_stat_statements" "${PGDATA}/postgresql.auto.conf"; then
+        echo "WARNING: pg_stat_statements is not configured. Recreate volume or update postgresql.auto.conf."
+    fi
 fi
🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts`:
- Around line 245-274: The loop never breaks on iterationResult.stopReason so
the poller keeps running until maxDurationMs; after each iteration (where
iterationResult is returned from traceSpan) check if iterationResult.stopReason
is non-null and if so break the while loop (or return) to stop further polling;
update the loop around the traceSpan call in route.ts (the while loop that uses
iterationCount/totalRequestsProcessed, traceSpan, getExternalDbSyncFusebox,
claimPendingRequests, processRequests, pollIntervalMs, and maxDurationMs) to
break when iterationResult.stopReason is truthy before awaiting
wait(pollIntervalMs).

In
`@apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts`:
- Around line 207-239: The loop never checks iterationResult.stopReason so it
keeps polling until maxDurationMs; after each traceSpan call (where
iterationResult is returned from traceSpan in the while loop), inspect
iterationResult.stopReason and break the loop when it's non-null (e.g.,
"disabled" or "idle"); use the existing variables/symbols
getExternalDbSyncFusebox, backfillSequenceIds, iterationResult, iterations,
pollIntervalMs and preserve the await wait(pollIntervalMs) behavior only when
continuing the loop.
🧹 Nitpick comments (1)
apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts (1)

56-59: getLocalApiBaseUrl is defined but never used.

This function is not called anywhere in the file. The request processing uses getEnvVariable("NEXT_PUBLIC_STACK_API_URL") instead.

♻️ Remove unused function
-function getLocalApiBaseUrl(): string {
-  const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
-  return `http://localhost:${prefix}02`;
-}
-

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: 1

🤖 Fix all issues with AI agents
In @.github/workflows/e2e-api-tests.yaml:
- Around line 162-171: The workflow steps named "Run tests", "Run tests again
(attempt 1)", and "Run tests again (attempt 2)" are calling the test script with
an extra literal "run" (e.g., `pnpm test run ...`) which results in `vitest run
run`; update each step to call `pnpm test` instead and keep the conditional
worker args intact (i.e., replace `pnpm test run ${{ matrix.freestyle-mode ==
'prod' && '--min-workers=1 --max-workers=1' || '' }}` with `pnpm test ${{
matrix.freestyle-mode == 'prod' && '--min-workers=1 --max-workers=1' || '' }}`
for the three steps).
🧹 Nitpick comments (1)
.github/workflows/e2e-source-of-truth-api-tests.yaml (1)

18-26: Annotate placeholder DB credentials to avoid secret-scanner noise.

The connection strings in this env block include password-looking tokens, and Checkov already flagged CKV_SECRET_4. If these are intentionally dummy values for local containers, add a Checkov skip comment or clarify with a safer placeholder to prevent false positives and accidental reuse as real secrets.

✅ Minimal suppression option
     env:
       NODE_ENV: test
       STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
       STACK_ACCESS_TOKEN_EXPIRATION_TIME: 30m
+      # checkov:skip=CKV_SECRET_4: local test DB credentials only (non-production)
       STACK_OVERRIDE_SOURCE_OF_TRUTH: '{"type": "postgres", "connectionString": "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/source-of-truth-db?schema=sot-schema"}'
       STACK_TEST_SOURCE_OF_TRUTH: true
+      # checkov:skip=CKV_SECRET_4: local test DB credentials only (non-production)
       STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
       STACK_FORCE_EXTERNAL_DB_SYNC: "true"
       STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
       STACK_EXTERNAL_DB_SYNC_DIRECT: "false"

Comment thread .github/workflows/e2e-api-tests.yaml
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: 1

🤖 Fix all issues with AI agents
In `@apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts`:
- Around line 208-319: Replace usages of catch (err: any) in waitForSyncedData,
waitForSyncedDeletion, and countUsersInExternalDb with catch (err: unknown) and
guard access to err.code via a narrow type check (e.g., ensure typeof err ===
'object' && err !== null && 'code' in err and then check (err as { code?:
unknown }).code === '42P01'); alternatively extract that logic into a small
helper like isPgUndefinedTableError(err: unknown): boolean and call it from each
catch branch so you safely detect the '42P01' case and preserve existing
behavior.

Comment thread apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-utils.ts
@BilalG1 BilalG1 enabled auto-merge (squash) February 5, 2026 02:51
@BilalG1 BilalG1 disabled auto-merge February 5, 2026 03:25
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

🤖 Fix all issues with AI agents
In `@apps/backend/prisma/schema.prisma`:
- Around line 1098-1117: The DeletedRow model currently retains full deleted-row
PII in the data field with no retention policy; add an optional DateTime field
(e.g., syncCompletedAt) to the DeletedRow model and set it when all external DB
syncs are confirmed (use the same code paths that reference
sequenceId/shouldUpdateSequenceId to mark completion), then implement a
scheduled cleanup job that either nullifies DeletedRow.data or deletes the
DeletedRow entirely for records where syncCompletedAt is older than your
retention window (e.g., 30 days); also ensure the sync completion code updates
syncCompletedAt and that any partial-index logic (WHERE shouldUpdateSequenceId =
TRUE) continues to work.

In `@apps/e2e/tests/backend/endpoints/api/v1/users.test.ts`:
- Line 12: The test currently uses it.only ("it.only(\"should not be able to
read own user\", ...)" ) which causes all other tests in the file to be skipped;
remove the ".only" so the test reads it("should not be able to read own user",
...) (or use test(...) if your suite uses that alias) to restore normal test
running and ensure the full suite executes.
🧹 Nitpick comments (2)
apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-basics.test.ts (1)

19-28: Consider adding proper typing for externalDatabases parameter.

The externalDatabases parameter uses any type. Per coding guidelines, any should be avoided. Consider defining a proper type or importing the expected shape from the sync configuration types.

Proposed type definition
+type ExternalDbConfig = {
+  [key: string]: {
+    type: 'postgres',
+    connectionString: string,
+  }
+};
+
 const createProjectWithExternalDb = (
-  externalDatabases: any,
+  externalDatabases: ExternalDbConfig,
   projectOptions?: { display_name?: string, description?: string }
 ) => {
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/external-db-sync/page-client.tsx (1)

153-156: Edge case: formatMillis(0) returns "—" instead of a formatted date.

The falsy check if (!value) will treat 0 as a missing value. While timestamp 0 (epoch) is unlikely in practice, this could mask issues during debugging.

Proposed fix
 function formatMillis(value: number | null) {
-  if (!value) return "—";
+  if (value === null || value === undefined) return "—";
   return new Date(value).toLocaleString();
 }

Comment thread apps/backend/prisma/schema.prisma
Comment thread apps/e2e/tests/backend/endpoints/api/v1/users.test.ts Outdated
@BilalG1 BilalG1 merged commit 2055d98 into dev Feb 5, 2026
25 of 26 checks passed
@BilalG1 BilalG1 deleted the external-db-sync branch February 5, 2026 20:04
@BilalG1 BilalG1 restored the external-db-sync branch February 5, 2026 20:05
@BilalG1 BilalG1 deleted the external-db-sync branch February 5, 2026 20:13
@coderabbitai coderabbitai bot mentioned this pull request Mar 30, 2026
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.

5 participants