Skip to content

Route read-only raw Prisma queries to read replica#1467

Merged
N2D4 merged 6 commits into
devfrom
devin/1779398666-raw-queries-read-replica
May 22, 2026
Merged

Route read-only raw Prisma queries to read replica#1467
N2D4 merged 6 commits into
devfrom
devin/1779398666-raw-queries-read-replica

Conversation

@N2D4
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 commented May 21, 2026

Routes all read-only raw Prisma queries ($queryRaw/$queryRawUnsafe) to the database read replica via $replica(). Adds an AGENTS.md note documenting when to use $replica() for raw queries.

Queries intentionally left on primary: queries inside transactions and write queries (INSERT/UPDATE/DELETE, including CTEs).

Link to Devin session: https://app.devin.ai/sessions/58c157f791f0424498bab3a92bc50f3e
Requested by: @N2D4

Summary by CodeRabbit

  • Chores
    • Optimized database read operations across backend services, including authentication routes, payment processing, session management, and internal APIs, to enhance overall system performance.

Review Change Stack


Note

Medium Risk
Broadly changes database read routing for several backend endpoints (including auth and payments) from primary to replica, which could surface replication-lag consistency issues if any of these reads implicitly expect read-after-write behavior.

Overview
Routes read-only raw SQL calls ($queryRaw) to the database replica by switching to $replica().$queryRaw across multiple backend areas: CLI auth routes, external DB sync status aggregation, local emulator project helpers, payments transaction listing, session replay admin queries, conversations reads, and development-environment checks.

Adds an AGENTS.md guideline documenting when to use $replica() for raw queries and when not to (transactions or any write-containing SQL).

Reviewed by Cursor Bugbot for commit 8aed98d. Bugbot is set up for automated code reviews on this repo. Configure here.

- Update all pure-read $queryRaw/$queryRawUnsafe calls to use $replica()
- Add AGENTS.md note about when to use read replicas for raw queries
- Skip queries inside transactions, queries with writes in CTEs,
  and lookups that feed directly into create/upsert logic

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 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 May 22, 2026 6:11pm
stack-auth-mcp Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-auth-skills Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-backend Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-dashboard Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-demo Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-docs Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-preview-backend Ready Ready Preview, Comment May 22, 2026 6:11pm
stack-preview-dashboard Ready Ready Preview, Comment May 22, 2026 6:11pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

This PR routes all read-only Prisma database queries through $replica() to leverage database read replicas, reducing load on the primary connection. The changes span authentication endpoints, status diagnostics, project management, and core business services, with a new coding guideline in AGENTS.md documenting the pattern.

Changes

Replica Read Routing Implementation

Layer / File(s) Summary
Coding guideline and pattern documentation
AGENTS.md
New guideline instructs agents to route raw Prisma read-only queries ($queryRaw / $queryRawUnsafe) through $replica().$queryRaw, excluding transactions and write operations.
CLI authentication endpoints
apps/backend/src/app/api/latest/auth/cli/complete/route.tsx, apps/backend/src/app/api/latest/auth/cli/poll/route.tsx, apps/backend/src/app/api/latest/auth/cli/route.tsx
CLI auth routes route identity lookups through replica: getPendingCliAuthAttempt, getRefreshTokenSession in complete flow; polling endpoint's attempt lookup; anon refresh token lookup.
Status and diagnostics endpoints
apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts, apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts
External DB sync status routes all stats queries (fetchInternalStats and global scope counts) through replica; session replay admin rows routes admin query through replica.
Local emulator project management
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Project lookup by filepath, column existence check, onboarding status fetch, and project listing all switch to read replica.
Business logic and utility services
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx, apps/backend/src/lib/conversations.tsx, apps/backend/src/lib/development-environment.ts, apps/backend/src/lib/managed-email-domains.tsx
Payments ledger and refund queries; conversation row, state, summary, detail, and message/entry-point queries; development environment project check; managed email domain lookups all route through read replica.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • hexclave/stack-auth#1419: Modifies apps/backend/src/app/api/latest/auth/cli/poll/route.tsx polling handler database access patterns.
  • hexclave/stack-auth#1463: Similarly routes Prisma reads through replica infrastructure (prisma.$replica()) for scalability.

Suggested reviewers

  • BilalG1
  • nams1570

Poem

🐰 Hopping through queries with replica in sight,
Read-only scrolls now routed just right,
No primary strain from the data we seek,
Just replica lanes, load-balanced and sleek! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: routing read-only raw Prisma queries to the read replica.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly outlines the objective (routing read-only raw Prisma queries to replica), provides rationale (replication lag consistency), and explains intentional exclusions (transactions, writes).

✏️ 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 devin/1779398666-raw-queries-read-replica

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.

…nous)

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
Now that we know writes await replication, there's no read-after-write
concern. Update remaining SELECT queries in CLI auth routes,
getManagedEmailDomainByTenancyAndSubdomain, and local emulator project
lookup.

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR routes all read-only raw Prisma queries ($queryRaw/$queryRawUnsafe) to the database read replica via $replica(), reducing load on the primary. A new AGENTS.md note documents when to use $replica() and explicitly calls out the exceptions (transactions and write queries).

  • Most of the 30+ changed queries are pure read-only SELECTs (stats, conversations, domain lookups, session replays, payments listing) where replica routing is safe.
  • Two queries in the local emulator path are incorrectly routed to the replica: the "get-or-create" lookup in getOrCreateLocalEmulatorProjectId and the onboarding-status read in syncLocalEmulatorOnboardingStatus both execute immediately before or after primary writes, making them sensitive to replication lag.

Confidence Score: 3/5

The bulk of the changes are safe, but the local emulator endpoint has two reads incorrectly moved to the replica that bracket primary writes — one can silently overwrite the project mapping and the other will throw an assertion error on first use.

The overwhelming majority of queries are straightforward read-only SELECTs with no writes depending on them. The local emulator route is the exception: getOrCreateLocalEmulatorProjectId reads from the replica to decide whether to create a new project ID then writes unconditionally to the primary, causing silent project mapping corruption; syncLocalEmulatorOnboardingStatus then reads the just-written Project row from the replica before it replicates, throwing an uncaught StackAssertionError on every first-time creation.

apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx — both getOrCreateLocalEmulatorProjectId and syncLocalEmulatorOnboardingStatus need their $replica() reads reverted to primary reads.

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx Two replica reads follow primary writes without any lag protection: the "get-or-create" lookup in getOrCreateLocalEmulatorProjectId can return stale/empty results and corrupt the project mapping, and the onboarding-status lookup in syncLocalEmulatorOnboardingStatus can throw an assertion error on first project creation.
apps/backend/src/app/api/latest/auth/cli/complete/route.tsx Both reads (CliAuthAttempt and RefreshToken) routed to replica; the subsequent writes use atomic UPDATE…WHERE CAS conditions on the primary, so replica staleness can cause a spurious 400 at worst but not a security bypass.
apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts All 12 queries are aggregate stats/counts for a monitoring dashboard; slightly stale replica reads are acceptable here.
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx getTransactions is called with a regular (non-transaction) Prisma client; both the main and refund queries are read-only and safe to route to the replica.
apps/backend/src/lib/conversations.tsx Four read-only conversation queries correctly routed to replica.
AGENTS.md Adds clear documentation on when to use $replica() for raw queries, including the transaction and write exclusions.

Comments Outside Diff (2)

  1. apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx, line 85-91 (link)

    P1 Stale replica read breaks "get-or-create" idempotency

    getOrCreateLocalEmulatorProjectId reads the existing project mapping from the replica, but immediately after writes to the primary. If the replica lags behind a recent write (e.g., on a rapid second call after project creation), the lookup returns no row, so a fresh UUID is generated — and then the ON CONFLICT DO UPDATE SET "projectId" = EXCLUDED."projectId" overwrites the mapping to point at the newly-generated project instead of the one already in the database. This silently reassigns which project a given file path resolves to. The read here should remain on the primary to preserve the original idempotency guarantee.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
    Line: 85-91
    
    Comment:
    **Stale replica read breaks "get-or-create" idempotency**
    
    `getOrCreateLocalEmulatorProjectId` reads the existing project mapping from the replica, but immediately after writes to the primary. If the replica lags behind a recent write (e.g., on a rapid second call after project creation), the lookup returns no row, so a fresh UUID is generated — and then the `ON CONFLICT DO UPDATE SET "projectId" = EXCLUDED."projectId"` overwrites the mapping to point at the newly-generated project instead of the one already in the database. This silently reassigns which project a given file path resolves to. The read here should remain on the primary to preserve the original idempotency guarantee.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx, line 201-209 (link)

    P1 Assertion error on replica lag after project creation

    syncLocalEmulatorOnboardingStatus is called immediately after getOrCreateLocalEmulatorProjectId writes a newly-created Project row to the primary. If the replica hasn't propagated that write yet, the $queryRaw on line 201 returns no rows and line 208 throws a StackAssertionError. This means first-time project creation will reliably fail with an internal server error whenever replication lag exists. The read should target the primary for this write-following query.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
    Line: 201-209
    
    Comment:
    **Assertion error on replica lag after project creation**
    
    `syncLocalEmulatorOnboardingStatus` is called immediately after `getOrCreateLocalEmulatorProjectId` writes a newly-created `Project` row to the primary. If the replica hasn't propagated that write yet, the `$queryRaw` on line 201 returns no rows and line 208 throws a `StackAssertionError`. This means first-time project creation will reliably fail with an internal server error whenever replication lag exists. The read should target the primary for this write-following query.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx:85-91
**Stale replica read breaks "get-or-create" idempotency**

`getOrCreateLocalEmulatorProjectId` reads the existing project mapping from the replica, but immediately after writes to the primary. If the replica lags behind a recent write (e.g., on a rapid second call after project creation), the lookup returns no row, so a fresh UUID is generated — and then the `ON CONFLICT DO UPDATE SET "projectId" = EXCLUDED."projectId"` overwrites the mapping to point at the newly-generated project instead of the one already in the database. This silently reassigns which project a given file path resolves to. The read here should remain on the primary to preserve the original idempotency guarantee.

### Issue 2 of 2
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx:201-209
**Assertion error on replica lag after project creation**

`syncLocalEmulatorOnboardingStatus` is called immediately after `getOrCreateLocalEmulatorProjectId` writes a newly-created `Project` row to the primary. If the replica hasn't propagated that write yet, the `$queryRaw` on line 201 returns no rows and line 208 throws a `StackAssertionError`. This means first-time project creation will reliably fail with an internal server error whenever replication lag exists. The read should target the primary for this write-following query.

Reviews (1): Last reviewed commit: "Merge branch 'dev' into devin/1779398666..." | Re-trigger Greptile

@N2D4 N2D4 enabled auto-merge (squash) May 22, 2026 00:29
Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
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 routes read-only raw Prisma queries ($queryRaw / $queryRawUnsafe) through $replica() to reduce load on the primary database, and documents the intended usage in AGENTS.md.

Changes:

  • Updated multiple backend raw SQL reads to call prisma.$replica().$queryRaw(...).
  • Applied the same change across several internal API routes and backend libs.
  • Added an AGENTS.md guideline describing when to use $replica() for raw queries.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
apps/backend/src/lib/managed-email-domains.tsx Routes managed-email domain raw SELECTs through $replica().
apps/backend/src/lib/development-environment.ts Routes the development-environment flag lookup through $replica().
apps/backend/src/lib/conversations.tsx Routes conversation read queries through $replica().
apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts Routes session replay admin list raw query through $replica().
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx Routes internal ledger transaction raw queries through $replica().
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx Routes several local-emulator read queries through $replica().
apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts Routes internal stats/count raw queries through $replica().
apps/backend/src/app/api/latest/auth/cli/route.tsx Routes refresh-token validation read through $replica().
apps/backend/src/app/api/latest/auth/cli/poll/route.tsx Routes CLI auth attempt polling read through $replica().
apps/backend/src/app/api/latest/auth/cli/complete/route.tsx Routes CLI auth attempt + refresh-token reads through $replica().
AGENTS.md Documents $replica() usage guidance for raw queries.
Comments suppressed due to low confidence (9)

apps/backend/src/app/api/latest/auth/cli/poll/route.tsx:61

  • Using the read replica here can break the CLI auth flow due to replica lag: the POST route inserts into CliAuthAttempt on the primary and the client may poll immediately; a replica read may not see the row yet and incorrectly throw InvalidPollingCodeError. This query should stay on the primary (or otherwise guarantee read-your-writes consistency for newly created attempts).
    const cliAuthRows = await prisma.$replica().$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
      SELECT
        "id",
        "refreshToken",
        "expiresAt",
        "usedAt"
      FROM ${sqlQuoteIdent(schema)}."CliAuthAttempt"
      WHERE "tenancyId" = ${tenancy.id}::UUID
        AND "pollingCode" = ${polling_code}
      LIMIT 1
    `);

apps/backend/src/app/api/latest/auth/cli/complete/route.tsx:118

  • Reading pending CliAuthAttempt from the replica risks false "Invalid login code" failures right after the attempt is created (replica lag). Since this endpoint is part of the immediate CLI login flow, this lookup should use the primary connection to ensure read-your-writes behavior.
async function getPendingCliAuthAttempt(tenancy: Tenancy, loginCode: string) {
  // CliAuthAttempt lives in the tenancy's source-of-truth DB, consistent with cli/poll/route.tsx.
  const prisma = await getPrismaClientForTenancy(tenancy);
  const schema = await getPrismaSchemaForTenancy(tenancy);
  const rows = await prisma.$replica().$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
    SELECT
      "id",
      "tenancyId",
      "refreshToken",
      "anonRefreshToken",
      "expiresAt",
      "usedAt"
    FROM ${sqlQuoteIdent(schema)}."CliAuthAttempt"
    WHERE "tenancyId" = ${tenancy.id}::UUID
      AND "loginCode" = ${loginCode}
    LIMIT 1
  `);

apps/backend/src/app/api/latest/auth/cli/route.tsx:50

  • This refresh-token lookup is used for authentication/authorization decisions. Routing it to the replica can allow recently revoked/deleted refresh tokens to be accepted until replication catches up. Consider keeping this read on the primary (or otherwise ensuring strongly consistent reads for session/refresh-token validation).
    if (anon_refresh_token != null) {
      const refreshTokenRows = await globalPrismaClient.$replica().$queryRaw<RefreshTokenRow[]>(Prisma.sql`
        SELECT "tenancyId", "projectUserId", "expiresAt"
        FROM "ProjectUserRefreshToken"
        WHERE "refreshToken" = ${anon_refresh_token}
        LIMIT 1
      `);

apps/backend/src/app/api/latest/auth/cli/complete/route.tsx:143

  • This refresh-token lookup participates in session validation. If the refresh token row is revoked/deleted on the primary, a replica read may still return it briefly, causing incorrect acceptance. Consider keeping this query on the primary DB for strong consistency in auth flows.
async function getRefreshTokenSession(tenancyId: string, refreshToken: string) {
  // ProjectUserRefreshToken lives in the global DB (see tokens.tsx and oauth/model.tsx).
  const rows = await globalPrismaClient.$replica().$queryRaw<RefreshTokenRow[]>(Prisma.sql`
    SELECT
      "id",
      "tenancyId",
      "projectUserId",
      "refreshToken",
      "expiresAt"
    FROM "ProjectUserRefreshToken"
    WHERE "refreshToken" = ${refreshToken}
    LIMIT 1
  `);

apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx:93

  • This existence check influences subsequent writes (project/tenancy upserts and the LocalEmulatorProject mapping upsert). If the replica is behind, this can incorrectly treat an existing mapping as missing and create a new projectId, potentially overwriting the mapping and leaving orphaned Project/Tenancy rows. This read should stay on the primary (or run inside a transaction on the primary) to avoid stale-read write skew.
async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<{ projectId: string, created: boolean }> {
  const existingRows = await globalPrismaClient.$replica().$queryRaw<LocalEmulatorProjectMappingRow[]>(Prisma.sql`
    SELECT "projectId"
    FROM "LocalEmulatorProject"
    WHERE "absoluteFilePath" = ${absoluteFilePath}
    LIMIT 1
  `);
  const existingRow = existingRows.length > 0 ? existingRows[0] : undefined;
  const projectId = existingRow ? existingRow.projectId : generateUuid();

apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx:206

  • These reads are used to drive write decisions later in the function (e.g., whether to update onboardingStatus / onboardingState). Reading from the replica can apply the wrong transition when the primary has a newer onboardingStatus value (replica lag), leading to inconsistent state. For correctness, use the primary for the onboardingStatus read (and consider whether the information_schema check needs to be strongly consistent as well).
async function syncLocalEmulatorOnboardingStatus(projectId: string, showOnboarding: boolean): Promise<ProjectOnboardingStatus> {
  const onboardingStateColumnExistsRows = await globalPrismaClient.$replica().$queryRaw<Array<{ exists: boolean }>>(Prisma.sql`
    SELECT EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = 'Project'
        AND column_name = 'onboardingState'
    ) AS "exists"
  `);
  const onboardingStateColumnExists = onboardingStateColumnExistsRows[0]?.exists === true;

  const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ onboardingStatus: string }>>(Prisma.sql`
    SELECT "onboardingStatus"
    FROM "Project"
    WHERE "id" = ${projectId}
    LIMIT 1
  `);

apps/backend/src/lib/managed-email-domains.tsx:112

  • This lookup is used as an existence check before creating external resources (Resend domain, DNSimple zone/records) in managed-email-onboarding. If the replica lags, it may not see an already-created ManagedEmailDomain row and the code can attempt duplicate external creation (often failing with a misleading 409) or hit the unique constraint later. Consider keeping this query on the primary (or providing a separate strongly-consistent variant for pre-write checks).
export async function getManagedEmailDomainByTenancyAndSubdomain(options: {
  tenancyId: string,
  subdomain: string,
}): Promise<ManagedEmailDomain | null> {
  const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
    SELECT *
    FROM "ManagedEmailDomain"
    WHERE "tenancyId" = ${options.tenancyId}
      AND "subdomain" = ${options.subdomain}
    LIMIT 1
  `);

apps/backend/src/lib/managed-email-domains.tsx:125

  • This lookup is used by applyManagedEmailProvider to decide whether to proceed with state transitions (demote/mark applied, save config). Replica lag can cause stale status checks (e.g., still seeing VERIFIED after it was marked APPLIED, or missing the row right after creation/webhook update), which can lead to incorrect behavior or confusing errors. Prefer primary reads for these state-machine / pre-write checks, or split into a primary-consistent helper for callers that require it.
export async function getManagedEmailDomainByResendDomainId(resendDomainId: string): Promise<ManagedEmailDomain | null> {
  const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
    SELECT *
    FROM "ManagedEmailDomain"
    WHERE "resendDomainId" = ${resendDomainId}
    LIMIT 1
  `);

apps/backend/src/lib/development-environment.ts:17

  • This value is used as a gate before allowing environment-config override writes. Reading it from the replica can temporarily allow writes that should be blocked (or block writes that should be allowed) if the project was just marked/unmarked as a development environment and replication is behind. Consider keeping this check on the primary for correctness of write blocking.
export async function isDevelopmentEnvironmentProject(projectId: string): Promise<boolean> {
  const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ isDevelopmentEnvironment: boolean }>>(Prisma.sql`
    SELECT "isDevelopmentEnvironment"
    FROM "Project"
    WHERE "id" = ${projectId}
    LIMIT 1
  `);
  return rows[0]?.isDevelopmentEnvironment === true;

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

Comment thread AGENTS.md

### Code-related
- Use ES6 maps instead of records wherever you can.
- **Read replicas for raw Prisma queries**: When writing raw SQL queries (`$queryRaw`, `$queryRawUnsafe`), always use `$replica()` for read-only queries (e.g. `globalPrismaClient.$replica().$queryRaw\`SELECT ...\``). This routes reads to the database replica and reduces load on the primary. Do NOT use `$replica()` for queries inside transactions or queries containing writes (INSERT/UPDATE/DELETE, even in CTEs).
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8aed98d. Configure here.

const onboardingStateColumnExists = onboardingStateColumnExistsRows[0]?.exists === true;

const rows = await globalPrismaClient.$queryRaw<Array<{ onboardingStatus: string }>>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ onboardingStatus: string }>>(Prisma.sql`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Read-after-write from replica in same request fails

Medium Severity

syncLocalEmulatorOnboardingStatus reads the Project table from the replica via $replica().$queryRaw, but the project may have just been created on the primary by getOrCreateLocalEmulatorProjectId in the same request (line 325→327). When the replica hasn't caught up, the query returns zero rows and throws a StackAssertionError, surfacing as a 500 to the user. The same pattern at line 86 can cause getOrCreateLocalEmulatorProjectId to miss an existing mapping and orphan the old project. Before this PR both reads hit the primary, so no replication lag was possible. The default replication wait strategy is "none", meaning no wait occurs even when a real replica is configured.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8aed98d. Configure here.

@N2D4 N2D4 disabled auto-merge May 22, 2026 18:16
@N2D4 N2D4 merged commit 197ad09 into dev May 22, 2026
34 of 38 checks passed
@N2D4 N2D4 deleted the devin/1779398666-raw-queries-read-replica branch May 22, 2026 18:16
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