Skip to content

Commit 0501fb8

Browse files
feat(workspaces): bypass personal workspace limit for platform admins
1 parent bdc42a2 commit 0501fb8

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

apps/sim/lib/workspaces/policy.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('getWorkspaceCreationPolicy', () => {
8282
})
8383

8484
it('blocks free users once they already own one non-organization workspace', async () => {
85-
mockDbResults.value = [[{ value: 1 }]]
85+
mockDbResults.value = [[], [{ value: 1 }]]
8686

8787
const result = await getWorkspaceCreationPolicy({ userId: 'user-1' })
8888

@@ -98,7 +98,7 @@ describe('getWorkspaceCreationPolicy', () => {
9898
plan: 'pro_6000',
9999
status: 'active',
100100
})
101-
mockDbResults.value = [[{ value: 2 }]]
101+
mockDbResults.value = [[], [{ value: 2 }]]
102102

103103
const result = await getWorkspaceCreationPolicy({ userId: 'user-1' })
104104

@@ -114,7 +114,7 @@ describe('getWorkspaceCreationPolicy', () => {
114114
plan: 'pro_25000',
115115
status: 'active',
116116
})
117-
mockDbResults.value = [[{ value: 5 }]]
117+
mockDbResults.value = [[], [{ value: 5 }]]
118118

119119
const result = await getWorkspaceCreationPolicy({ userId: 'user-1' })
120120

@@ -130,7 +130,7 @@ describe('getWorkspaceCreationPolicy', () => {
130130
plan: 'pro_25000',
131131
status: 'active',
132132
})
133-
mockDbResults.value = [[{ value: 10 }]]
133+
mockDbResults.value = [[], [{ value: 10 }]]
134134

135135
const result = await getWorkspaceCreationPolicy({ userId: 'user-1' })
136136

@@ -220,6 +220,28 @@ describe('getWorkspaceCreationPolicy', () => {
220220
expect(result.reason).toContain('owners and admins')
221221
})
222222

223+
it('grants platform admins unlimited personal workspaces regardless of plan', async () => {
224+
mockDbResults.value = [[{ role: 'admin' }], [{ value: 25 }]]
225+
226+
const result = await getWorkspaceCreationPolicy({ userId: 'admin-user' })
227+
228+
expect(result.canCreate).toBe(true)
229+
expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL)
230+
expect(result.maxWorkspaces).toBeNull()
231+
expect(result.currentWorkspaceCount).toBe(25)
232+
expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled()
233+
})
234+
235+
it('still enforces plan limits for non-admin users', async () => {
236+
mockDbResults.value = [[{ role: 'user' }], [{ value: 1 }]]
237+
238+
const result = await getWorkspaceCreationPolicy({ userId: 'regular-user' })
239+
240+
expect(result.canCreate).toBe(false)
241+
expect(result.maxWorkspaces).toBe(1)
242+
expect(result.currentWorkspaceCount).toBe(1)
243+
})
244+
223245
it('blocks users without org membership from creating workspaces in the active org context', async () => {
224246
mockDbResults.value = [[], [{ userId: 'owner-1' }]]
225247

apps/sim/lib/workspaces/policy.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { member, type WorkspaceMode, workspace } from '@sim/db/schema'
2+
import { member, user, type WorkspaceMode, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, count, eq, isNull } from 'drizzle-orm'
55
import { getOrganizationSubscription } from '@/lib/billing/core/billing'
@@ -276,6 +276,19 @@ export async function getWorkspaceCreationPolicy({
276276
}
277277
}
278278

279+
if (await isPlatformAdmin(userId)) {
280+
return {
281+
canCreate: true,
282+
workspaceMode: WORKSPACE_MODE.PERSONAL,
283+
organizationId: null,
284+
billedAccountUserId: userId,
285+
maxWorkspaces: null,
286+
currentWorkspaceCount: await countNonOrganizationOwnedWorkspaces(userId),
287+
reason: null,
288+
status: 200,
289+
}
290+
}
291+
279292
const highestPrioritySubscription = await getHighestPrioritySubscription(userId)
280293
const plan = highestPrioritySubscription?.plan
281294
const maxWorkspaces = isMax(plan) ? 10 : isPro(plan) ? 3 : 1
@@ -331,6 +344,12 @@ export async function getOrganizationOwnerId(organizationId: string): Promise<st
331344
return ownerMembership?.userId ?? null
332345
}
333346

347+
async function isPlatformAdmin(userId: string): Promise<boolean> {
348+
const [row] = await db.select({ role: user.role }).from(user).where(eq(user.id, userId)).limit(1)
349+
350+
return row?.role === 'admin'
351+
}
352+
334353
/**
335354
* Like `getOrganizationOwnerId` but throws when no owner row exists.
336355
* Use when the caller needs a guaranteed billed-account userId — every

0 commit comments

Comments
 (0)