Skip to content

Commit 01d67c0

Browse files
committed
improvement(repo): restructuring to make realtime image narrower scoped
1 parent 41a1b50 commit 01d67c0

286 files changed

Lines changed: 2315 additions & 1470 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/sim-testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ vi.useFakeTimers()
144144
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
145145
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
146146
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
147-
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
147+
| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` |
148148
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
149149
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
150150
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |

.cursor/rules/sim-testing.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ vi.useFakeTimers()
144144
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
145145
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
146146
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
147-
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
147+
| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` |
148148
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
149149
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
150150
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |

.github/workflows/test-build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ jobs:
103103
- name: Lint code
104104
run: bun run lint:check
105105

106+
- name: Enforce monorepo boundaries
107+
run: bun run scripts/check-monorepo-boundaries.ts
108+
109+
- name: Verify realtime prune graph
110+
run: bun run scripts/check-realtime-prune-graph.ts
111+
106112
- name: Run tests with coverage
107113
env:
108114
NODE_OPTIONS: '--no-warnings --max-old-space-size=8192'

AGENTS.md

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,42 @@ You are a professional software engineer. All code must follow best practices: a
2020

2121
### Root Structure
2222
```
23-
apps/sim/
24-
├── app/ # Next.js app router (pages, API routes)
25-
├── blocks/ # Block definitions and registry
26-
├── components/ # Shared UI (emcn/, ui/)
27-
├── executor/ # Workflow execution engine
28-
├── hooks/ # Shared hooks (queries/, selectors/)
29-
├── lib/ # App-wide utilities
30-
├── providers/ # LLM provider integrations
31-
├── stores/ # Zustand stores
32-
├── tools/ # Tool definitions
33-
└── triggers/ # Trigger definitions
23+
apps/
24+
├── sim/ # Next.js app (UI + API routes + workflow editor)
25+
│ ├── app/ # Next.js app router (pages, API routes)
26+
│ ├── blocks/ # Block definitions and registry
27+
│ ├── components/ # Shared UI (emcn/, ui/)
28+
│ ├── executor/ # Workflow execution engine
29+
│ ├── hooks/ # Shared hooks (queries/, selectors/)
30+
│ ├── lib/ # App-wide utilities
31+
│ ├── providers/ # LLM provider integrations
32+
│ ├── stores/ # Zustand stores
33+
│ ├── tools/ # Tool definitions
34+
│ └── triggers/ # Trigger definitions
35+
└── realtime/ # Bun Socket.IO server (collaborative canvas)
36+
└── src/ # auth, config, database, handlers, middleware,
37+
# rooms, routes, internal/webhook-cleanup.ts
38+
39+
packages/
40+
├── audit/ # @sim/audit — recordAudit + AuditAction + AuditResourceType
41+
├── auth/ # @sim/auth — @sim/auth/verify (shared Better Auth verifier)
42+
├── db/ # @sim/db — drizzle schema + client
43+
├── logger/ # @sim/logger
44+
├── realtime-protocol/ # @sim/realtime-protocol — socket operation constants + zod schemas
45+
├── security/ # @sim/security — safeCompare
46+
├── tsconfig/ # shared tsconfig presets
47+
├── utils/ # @sim/utils
48+
├── workflow-authz/ # @sim/workflow-authz — authorizeWorkflowByWorkspacePermission
49+
├── workflow-persistence/ # @sim/workflow-persistence — raw load/save + subflow helpers
50+
└── workflow-types/ # @sim/workflow-types — pure BlockState/Loop/Parallel/... types
3451
```
3552

53+
### Package boundaries
54+
- `apps/* → packages/*` only. Packages never import from `apps/*`.
55+
- Each package has explicit subpath `exports` maps; no barrels that accidentally pull in heavy halves.
56+
- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-image-size.ts`.
57+
- Auth is shared across services via the Better Auth "Shared Database Session" pattern: both apps read the same `BETTER_AUTH_SECRET` and point at the same DB via `@sim/db`.
58+
3659
### Naming Conventions
3760
- Components: PascalCase (`WorkflowList`)
3861
- Hooks: `use` prefix (`useWorkflowOperations`)

apps/realtime/package.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "@sim/realtime",
3+
"version": "0.1.0",
4+
"private": true,
5+
"license": "Apache-2.0",
6+
"type": "module",
7+
"engines": {
8+
"bun": ">=1.2.13",
9+
"node": ">=20.0.0"
10+
},
11+
"scripts": {
12+
"dev": "bun --watch src/index.ts",
13+
"start": "bun src/index.ts",
14+
"type-check": "tsc --noEmit",
15+
"lint": "biome check --write --unsafe .",
16+
"lint:check": "biome check .",
17+
"format": "biome format --write .",
18+
"format:check": "biome format .",
19+
"test": "vitest run",
20+
"test:watch": "vitest"
21+
},
22+
"dependencies": {
23+
"@sim/audit": "workspace:*",
24+
"@sim/auth": "workspace:*",
25+
"@sim/db": "workspace:*",
26+
"@sim/logger": "workspace:*",
27+
"@sim/realtime-protocol": "workspace:*",
28+
"@sim/security": "workspace:*",
29+
"@sim/utils": "workspace:*",
30+
"@sim/workflow-authz": "workspace:*",
31+
"@sim/workflow-persistence": "workspace:*",
32+
"@sim/workflow-types": "workspace:*",
33+
"@socket.io/redis-adapter": "8.3.0",
34+
"drizzle-orm": "^0.45.2",
35+
"postgres": "^3.4.5",
36+
"redis": "5.10.0",
37+
"socket.io": "^4.8.1",
38+
"socket.io-client": "4.8.1",
39+
"zod": "^3.24.2"
40+
},
41+
"devDependencies": {
42+
"@sim/testing": "workspace:*",
43+
"@sim/tsconfig": "workspace:*",
44+
"@types/node": "24.2.1",
45+
"typescript": "^5.7.3",
46+
"vitest": "^3.0.8"
47+
}
48+
}

apps/realtime/src/auth.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createVerifyAuth } from '@sim/auth/verify'
2+
import { env } from '@/env'
3+
4+
export const ANONYMOUS_USER_ID = '00000000-0000-0000-0000-000000000000'
5+
6+
export const ANONYMOUS_USER = {
7+
id: ANONYMOUS_USER_ID,
8+
name: 'Anonymous',
9+
email: 'anonymous@localhost',
10+
emailVerified: true,
11+
image: null,
12+
} as const
13+
14+
export const auth = createVerifyAuth({
15+
secret: env.BETTER_AUTH_SECRET,
16+
baseURL: env.BETTER_AUTH_URL,
17+
})
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { createLogger } from '@sim/logger'
33
import { createAdapter } from '@socket.io/redis-adapter'
44
import { createClient, type RedisClientType } from 'redis'
55
import { Server } from 'socket.io'
6-
import { env } from '@/lib/core/config/env'
7-
import { isProd } from '@/lib/core/config/feature-flags'
8-
import { getBaseUrl } from '@/lib/core/utils/urls'
6+
import { env, getBaseUrl, isProd } from '@/env'
97

108
const logger = createLogger('SocketIOConfig')
119

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1+
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
12
import * as schema from '@sim/db'
2-
import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
3+
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
34
import { createLogger } from '@sim/logger'
4-
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
5-
import { drizzle } from 'drizzle-orm/postgres-js'
6-
import postgres from 'postgres'
7-
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
8-
import { env } from '@/lib/core/config/env'
9-
import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
10-
import { getActiveWorkflowContext } from '@/lib/workflows/active-context'
11-
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
12-
import { mergeSubBlockValues } from '@/lib/workflows/subblocks'
135
import {
146
BLOCK_OPERATIONS,
157
BLOCKS_OPERATIONS,
@@ -19,7 +11,14 @@ import {
1911
SUBFLOW_OPERATIONS,
2012
VARIABLE_OPERATIONS,
2113
WORKFLOW_OPERATIONS,
22-
} from '@/socket/constants'
14+
} from '@sim/realtime-protocol/constants'
15+
import { getActiveWorkflowContext } from '@sim/workflow-authz'
16+
import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load'
17+
import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks'
18+
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
19+
import { drizzle } from 'drizzle-orm/postgres-js'
20+
import postgres from 'postgres'
21+
import { env } from '@/env'
2322

2423
const logger = createLogger('SocketDatabase')
2524

@@ -182,7 +181,7 @@ export async function getWorkflowState(workflowId: string) {
182181
throw new Error(`Workflow ${workflowId} not found`)
183182
}
184183

185-
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
184+
const normalizedData = await loadWorkflowFromNormalizedTablesRaw(workflowId)
186185

187186
if (normalizedData) {
188187
const finalState = {
@@ -915,30 +914,10 @@ async function handleBlocksOperationTx(
915914
}
916915
}
917916

918-
// Clean up external webhooks
919-
const webhooksToCleanup = await tx
920-
.select({
921-
webhook: webhook,
922-
workflow: {
923-
id: workflow.id,
924-
userId: workflow.userId,
925-
workspaceId: workflow.workspaceId,
926-
},
927-
})
928-
.from(webhook)
929-
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
930-
.where(and(eq(webhook.workflowId, workflowId), inArray(webhook.blockId, blockIdsArray)))
931-
932-
if (webhooksToCleanup.length > 0) {
933-
const requestId = `socket-batch-${workflowId}-${Date.now()}`
934-
for (const { webhook: wh, workflow: wf } of webhooksToCleanup) {
935-
try {
936-
await cleanupExternalWebhook(wh, wf, requestId)
937-
} catch (error) {
938-
logger.error(`Failed to cleanup webhook ${wh.id}:`, error)
939-
}
940-
}
941-
}
917+
// Webhook rows are only created at deploy time (saveTriggerWebhooksForDeploy in
918+
// lib/webhooks/deploy.ts) with deploymentVersionId set; their external-subscription
919+
// lifecycle is managed by deploy.ts, lifecycle.ts, and the /api/webhooks/[id] route.
920+
// Removing a trigger block from the draft canvas does not touch any webhook rows.
942921

943922
// Delete edges connected to any of the blocks
944923
await tx

apps/realtime/src/env.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { z } from 'zod'
2+
3+
const EnvSchema = z.object({
4+
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
5+
DATABASE_URL: z.string().url(),
6+
REDIS_URL: z.string().url().optional(),
7+
BETTER_AUTH_URL: z.string().url(),
8+
BETTER_AUTH_SECRET: z.string().min(32),
9+
INTERNAL_API_SECRET: z.string().min(32),
10+
NEXT_PUBLIC_APP_URL: z.string().url(),
11+
INTERNAL_API_BASE_URL: z.string().url().optional(),
12+
ALLOWED_ORIGINS: z.string().optional(),
13+
SOCKET_SERVER_URL: z.string().url().optional(),
14+
PORT: z.coerce.number().int().positive().default(3002),
15+
SOCKET_PORT: z.coerce.number().int().positive().optional(),
16+
HOSTNAME: z.string().default('0.0.0.0'),
17+
DISABLE_AUTH: z
18+
.string()
19+
.optional()
20+
.transform((value) => value === 'true' || value === '1'),
21+
})
22+
23+
function parseEnv() {
24+
const parsed = EnvSchema.safeParse(process.env)
25+
if (!parsed.success) {
26+
const formatted = parsed.error.format()
27+
throw new Error(`Invalid realtime server environment: ${JSON.stringify(formatted, null, 2)}`)
28+
}
29+
return parsed.data
30+
}
31+
32+
export const env = parseEnv()
33+
34+
export const isProd = env.NODE_ENV === 'production'
35+
export const isDev = env.NODE_ENV === 'development'
36+
export const isTest = env.NODE_ENV === 'test'
37+
38+
let appHostname = ''
39+
try {
40+
appHostname = new URL(env.NEXT_PUBLIC_APP_URL).hostname
41+
} catch {}
42+
export const isHosted = appHostname === 'sim.ai' || appHostname.endsWith('.sim.ai')
43+
44+
export const isAuthDisabled = env.DISABLE_AUTH === true && !isHosted
45+
46+
export function getBaseUrl(): string {
47+
return env.NEXT_PUBLIC_APP_URL
48+
}
49+
50+
export function getInternalApiBaseUrl(): string {
51+
return env.INTERNAL_API_BASE_URL ?? env.NEXT_PUBLIC_APP_URL
52+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { createLogger } from '@sim/logger'
2-
import { cleanupPendingSubblocksForSocket } from '@/socket/handlers/subblocks'
3-
import { cleanupPendingVariablesForSocket } from '@/socket/handlers/variables'
4-
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
5-
import type { IRoomManager } from '@/socket/rooms'
2+
import { cleanupPendingSubblocksForSocket } from '@/handlers/subblocks'
3+
import { cleanupPendingVariablesForSocket } from '@/handlers/variables'
4+
import type { AuthenticatedSocket } from '@/middleware/auth'
5+
import type { IRoomManager } from '@/rooms'
66

77
const logger = createLogger('ConnectionHandlers')
88

0 commit comments

Comments
 (0)