Skip to content

Commit 3cd7eaf

Browse files
committed
improvement(db): add opt-in read-replica client and harden migration runner
1 parent 20dd654 commit 3cd7eaf

20 files changed

Lines changed: 196 additions & 53 deletions

File tree

apps/sim/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Database (Required)
22
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
3+
# Optional read-replica connection string for offloading heavy read paths
4+
# (logs listing, audit logs, dashboard aggregations). Reads fall back to
5+
# DATABASE_URL when unset.
6+
# DATABASE_REPLICA_URL=""
37

48
# Authentication (Required unless DISABLE_AUTH=true)
59
BETTER_AUTH_SECRET=your_secret_key # Use `openssl rand -hex 32` to generate, or visit https://www.better-auth.com/docs/installation

apps/sim/app/api/logs/export/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { db } from '@sim/db'
1+
import { dbReplica } from '@sim/db'
22
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, desc, eq, sql } from 'drizzle-orm'
@@ -79,7 +79,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
7979
let offset = 0
8080
try {
8181
while (true) {
82-
const rows = await db
82+
const rows = await dbReplica
8383
.select(selectColumns)
8484
.from(workflowExecutionLogs)
8585
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))

apps/sim/app/api/logs/stats/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { db } from '@sim/db'
1+
import { dbReplica } from '@sim/db'
22
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq, sql } from 'drizzle-orm'
@@ -48,7 +48,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
4848
const commonFilters = buildFilterConditions(params, { useSimpleLevelFilter: true })
4949
const whereCondition = commonFilters ? and(workspaceFilter, commonFilters) : workspaceFilter
5050

51-
const boundsQuery = await db
51+
const boundsQuery = await dbReplica
5252
.select({
5353
minTime: sql<string>`MIN(${workflowExecutionLogs.startedAt})`,
5454
maxTime: sql<string>`MAX(${workflowExecutionLogs.startedAt})`,
@@ -83,7 +83,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
8383
const segmentMs = Math.max(60000, Math.floor(totalMs / params.segmentCount))
8484
const startTimeIso = startTime.toISOString()
8585

86-
const statsQuery = await db
86+
const statsQuery = await dbReplica
8787
.select({
8888
workflowId: sql<string>`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`,
8989
workflowName: sql<string>`COALESCE(${workflow.name}, 'Deleted Workflow')`,

apps/sim/app/api/v1/audit-logs/query.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AuditResourceType } from '@sim/audit'
2-
import { db } from '@sim/db'
2+
import { dbReplica } from '@sim/db'
33
import { auditLog, workspace } from '@sim/db/schema'
44
import type { InferSelectModel } from 'drizzle-orm'
55
import { and, desc, eq, gte, ilike, inArray, isNull, lt, lte, or, type SQL, sql } from 'drizzle-orm'
@@ -73,7 +73,7 @@ export function buildFilterConditions(params: AuditLogFilterParams): SQL<unknown
7373
* Returns the IDs of all workspaces attached to the organization.
7474
*/
7575
export async function getOrgWorkspaceIds(organizationId: string): Promise<string[]> {
76-
const rows = await db
76+
const rows = await dbReplica
7777
.select({ id: workspace.id })
7878
.from(workspace)
7979
.where(eq(workspace.organizationId, organizationId))
@@ -156,7 +156,7 @@ export async function queryAuditLogs(
156156
if (cursorCondition) allConditions.push(cursorCondition)
157157
}
158158

159-
const rows = await db
159+
const rows = await dbReplica
160160
.select()
161161
.from(auditLog)
162162
.where(allConditions.length > 0 ? and(...allConditions) : undefined)

apps/sim/hooks/use-inline-rename.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface UseInlineRenameProps {
99
* `mutateAsync(...)`) — NOT a fire-and-forget `mutate(...)` — so `isSaving`
1010
* spans the in-flight request and a rejection can revive the edit session.
1111
*/
12-
onSave: (id: string, newName: string) => void | Promise<unknown>
12+
onSave: (id: string, newName: string) => undefined | Promise<unknown>
1313
}
1414

1515
/**

apps/sim/lib/billing/core/usage-log.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ vi.mock('@sim/db', () => ({
2828
insert: mockInsert,
2929
transaction: mockTransaction,
3030
},
31+
dbReplica: {
32+
insert: mockInsert,
33+
transaction: mockTransaction,
34+
},
3135
}))
3236

3337
vi.mock('@sim/db/schema', () => ({

apps/sim/lib/billing/core/usage-log.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createHash } from 'node:crypto'
2-
import { db } from '@sim/db'
2+
import { db, dbReplica } from '@sim/db'
33
import { usageLog, workspace } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
55
import { toError } from '@sim/utils/errors'
@@ -579,7 +579,7 @@ export async function getUserUsageLogs(
579579
}
580580

581581
if (cursor) {
582-
const cursorLog = await db
582+
const cursorLog = await dbReplica
583583
.select({ createdAt: usageLog.createdAt })
584584
.from(usageLog)
585585
.where(eq(usageLog.id, cursor))
@@ -592,7 +592,7 @@ export async function getUserUsageLogs(
592592
}
593593
}
594594

595-
const logs = await db
595+
const logs = await dbReplica
596596
.select()
597597
.from(usageLog)
598598
.where(and(...conditions))
@@ -621,7 +621,7 @@ export async function getUserUsageLogs(
621621
if (startDate) summaryConditions.push(gte(usageLog.createdAt, startDate))
622622
if (endDate) summaryConditions.push(lte(usageLog.createdAt, endDate))
623623

624-
const summaryResult = await db
624+
const summaryResult = await dbReplica
625625
.select({
626626
source: usageLog.source,
627627
totalCost: sql<string>`SUM(${usageLog.cost})`,

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const env = createEnv({
1919
server: {
2020
// Core Database & Authentication
2121
DATABASE_URL: z.string().url(), // Primary database connection string
22+
DATABASE_REPLICA_URL: z.string().url().optional(), // Read-replica connection string; opt-in reads fall back to the primary when unset
2223
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
2324
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
2425
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration

apps/sim/lib/data-drains/sources/audit-logs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { db } from '@sim/db'
1+
import { dbReplica } from '@sim/db'
22
import { auditLog } from '@sim/db/schema'
33
import { and, inArray, isNull, or, sql } from 'drizzle-orm'
44
import {
@@ -35,7 +35,7 @@ async function* pages(input: SourcePageInput): AsyncIterable<AuditLogRow[]> {
3535
while (!input.signal.aborted) {
3636
const cursorClause = timeCursorPredicate(auditLog.createdAt, auditLog.id, cursor)
3737

38-
const rows = await db
38+
const rows = await dbReplica
3939
.select()
4040
.from(auditLog)
4141
.where(and(scopeClause, cursorClause))

apps/sim/lib/data-drains/sources/copilot-chats.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { db } from '@sim/db'
1+
import { dbReplica } from '@sim/db'
22
import { copilotChats, copilotMessages } from '@sim/db/schema'
33
import { and, asc, inArray, isNull, sql } from 'drizzle-orm'
44
import {
@@ -55,7 +55,7 @@ async function* pages(input: SourcePageInput): AsyncIterable<CopilotChatRow[]> {
5555
while (!input.signal.aborted) {
5656
const cursorClause = timeCursorPredicate(copilotChats.createdAt, copilotChats.id, cursor)
5757

58-
const metaRows = await db
58+
const metaRows = await dbReplica
5959
.select(chatColumns)
6060
.from(copilotChats)
6161
.where(and(inArray(copilotChats.workspaceId, workspaceIds), cursorClause))
@@ -65,7 +65,7 @@ async function* pages(input: SourcePageInput): AsyncIterable<CopilotChatRow[]> {
6565
if (metaRows.length === 0) return
6666

6767
const chatIds = metaRows.map((r) => r.id)
68-
const messageRows = await db
68+
const messageRows = await dbReplica
6969
.select({ chatId: copilotMessages.chatId, content: copilotMessages.content })
7070
.from(copilotMessages)
7171
.where(and(inArray(copilotMessages.chatId, chatIds), isNull(copilotMessages.deletedAt)))

0 commit comments

Comments
 (0)