Skip to content

Commit d257bfb

Browse files
feat(billing): bypass plan limits for platform admins
Extends the platform-admin bypass across usage caps, storage quota, table limits, log retention, and workspace invites. Hoists isPlatformAdmin into a shared helper for reuse.
1 parent 673ae39 commit d257bfb

8 files changed

Lines changed: 144 additions & 25 deletions

File tree

apps/sim/app/api/workspaces/route.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { listWorkspacesQuerySchema } from '@/lib/api/contracts'
99
import { createWorkspaceContract } from '@/lib/api/contracts/workspaces'
1010
import { parseRequest } from '@/lib/api/server'
1111
import { getSession } from '@/lib/auth'
12+
import { getPlatformAdminUserIds } from '@/lib/auth/platform-admin'
1213
import { PlatformEvents } from '@/lib/core/telemetry'
1314
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1415
import { captureServerEvent } from '@/lib/posthog/server'
@@ -124,18 +125,25 @@ export const GET = withRouteHandler(async (request: Request) => {
124125
.map(({ workspace: ws }) => ws.billedAccountUserId)
125126
),
126127
]
128+
const allBilledUserIds = [
129+
...new Set(userWorkspaces.map(({ workspace: ws }) => ws.billedAccountUserId)),
130+
]
127131
const teamOrEnterpriseByUser = new Map<string, boolean>()
128-
await Promise.all(
129-
grandfatheredBilledUserIds.map(async (userId) => {
130-
teamOrEnterpriseByUser.set(userId, await hasActiveTeamOrEnterpriseSubscription(userId))
131-
})
132-
)
132+
const [, adminBilledUserIds] = await Promise.all([
133+
Promise.all(
134+
grandfatheredBilledUserIds.map(async (userId) => {
135+
teamOrEnterpriseByUser.set(userId, await hasActiveTeamOrEnterpriseSubscription(userId))
136+
})
137+
),
138+
getPlatformAdminUserIds(allBilledUserIds),
139+
])
133140

134141
const workspacesWithPermissions = userWorkspaces.map(
135142
({ workspace: workspaceDetails, permissionType }) => {
136143
const invitePolicy = evaluateWorkspaceInvitePolicy(workspaceDetails, {
137144
billedUserHasTeamOrEnterprise:
138145
teamOrEnterpriseByUser.get(workspaceDetails.billedAccountUserId) ?? false,
146+
billedUserIsPlatformAdmin: adminBilledUserIds.has(workspaceDetails.billedAccountUserId),
139147
})
140148
const callerIsBilledUser = workspaceDetails.billedAccountUserId === session.user.id
141149

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { db } from '@sim/db'
2+
import { user } from '@sim/db/schema'
3+
import { and, eq, inArray } from 'drizzle-orm'
4+
5+
/**
6+
* Returns true when the user has the platform-level `admin` role. Platform
7+
* admins are Sim employees with elevated access; many subscription gates are
8+
* bypassed for them so internal usage isn't paywalled.
9+
*/
10+
export async function isPlatformAdmin(userId: string): Promise<boolean> {
11+
const [row] = await db.select({ role: user.role }).from(user).where(eq(user.id, userId)).limit(1)
12+
return row?.role === 'admin'
13+
}
14+
15+
/**
16+
* Bulk variant. Returns the set of userIds (from the input) that are platform
17+
* admins. Used by callers that need to evaluate many users at once (e.g.
18+
* listing every workspace a user can see and resolving invite policy).
19+
*/
20+
export async function getPlatformAdminUserIds(userIds: string[]): Promise<Set<string>> {
21+
if (userIds.length === 0) return new Set()
22+
const rows = await db
23+
.select({ id: user.id })
24+
.from(user)
25+
.where(and(inArray(user.id, userIds), eq(user.role, 'admin')))
26+
return new Set(rows.map((r) => r.id))
27+
}

apps/sim/lib/billing/calculations/usage-monitor.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { member, userStats } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { toError } from '@sim/utils/errors'
55
import { and, eq } from 'drizzle-orm'
6+
import { isPlatformAdmin } from '@/lib/auth/platform-admin'
67
import {
78
getHighestPrioritySubscription,
89
type HighestPrioritySubscription,
@@ -337,6 +338,15 @@ export async function checkServerSideUsageLimits(
337338
}
338339
}
339340

341+
if (await isPlatformAdmin(userId)) {
342+
logger.info('Bypassing usage cap for platform admin', { userId, currentUsage })
343+
return {
344+
isExceeded: false,
345+
currentUsage,
346+
limit: Number.MAX_SAFE_INTEGER,
347+
}
348+
}
349+
340350
const usageData = await checkUsageStatus(userId, preloadedSubscription)
341351

342352
const formattedUsage = (usageData.currentUsage ?? 0).toFixed(2)

apps/sim/lib/billing/cleanup-dispatcher.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { db } from '@sim/db'
2-
import { organization, subscription, workspace } from '@sim/db/schema'
2+
import { organization, subscription, user, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { toError } from '@sim/utils/errors'
55
import { tasks } from '@trigger.dev/sdk'
6-
import { and, eq, inArray, isNotNull, isNull, sql } from 'drizzle-orm'
6+
import { and, eq, inArray, isNotNull, isNull, ne, or, sql } from 'drizzle-orm'
77
import { type PlanCategory, sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers'
88
import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils'
99
import { getJobQueue } from '@/lib/core/async-jobs'
@@ -78,7 +78,14 @@ export async function resolveWorkspaceIdsForPlan(plan: NonEnterprisePlan): Promi
7878
sqlIsPaid(subscription.plan)
7979
)
8080
)
81-
.where(and(isNull(subscription.id), isNull(workspace.archivedAt)))
81+
.leftJoin(user, eq(user.id, workspace.billedAccountUserId))
82+
.where(
83+
and(
84+
isNull(subscription.id),
85+
isNull(workspace.archivedAt),
86+
or(isNull(user.role), ne(user.role, 'admin'))
87+
)
88+
)
8289

8390
return rows.map((r) => r.id)
8491
}
@@ -95,7 +102,8 @@ export async function resolveWorkspaceIdsForPlan(plan: NonEnterprisePlan): Promi
95102
planPredicate!
96103
)
97104
)
98-
.where(isNull(workspace.archivedAt))
105+
.leftJoin(user, eq(user.id, workspace.billedAccountUserId))
106+
.where(and(isNull(workspace.archivedAt), or(isNull(user.role), ne(user.role, 'admin'))))
99107
.groupBy(workspace.id)
100108

101109
return rows.map((r) => r.id)

apps/sim/lib/billing/storage/limits.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { organization, subscription, userStats } from '@sim/db/schema'
1414
import { createLogger } from '@sim/logger'
1515
import { eq } from 'drizzle-orm'
16+
import { isPlatformAdmin } from '@/lib/auth/platform-admin'
1617
import { getPlanTypeForLimits, isEnterprise, isFree } from '@/lib/billing/plan-helpers'
1718
import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils'
1819
import { getEnv } from '@/lib/core/config/env'
@@ -172,11 +173,21 @@ export async function checkStorageQuota(
172173
}
173174

174175
try {
175-
const [currentUsage, limit] = await Promise.all([
176+
const [adminBypass, currentUsage, limit] = await Promise.all([
177+
isPlatformAdmin(userId),
176178
getUserStorageUsage(userId),
177179
getUserStorageLimit(userId),
178180
])
179181

182+
if (adminBypass) {
183+
logger.info('Bypassing storage quota for platform admin', { userId })
184+
return {
185+
allowed: true,
186+
currentUsage,
187+
limit: Number.MAX_SAFE_INTEGER,
188+
}
189+
}
190+
180191
const newUsage = currentUsage + additionalBytes
181192
const allowed = newUsage <= limit
182193

apps/sim/lib/table/billing.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { createLogger } from '@sim/logger'
8+
import { isPlatformAdmin } from '@/lib/auth/platform-admin'
89
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
910
import { getPlanTypeForLimits } from '@/lib/billing/plan-helpers'
1011
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
@@ -32,7 +33,22 @@ export async function getWorkspaceTableLimits(workspaceId: string): Promise<Tabl
3233
return planLimits.free
3334
}
3435

35-
const subscription = await getHighestPrioritySubscription(billedAccountUserId)
36+
const [adminBypass, subscription] = await Promise.all([
37+
isPlatformAdmin(billedAccountUserId),
38+
getHighestPrioritySubscription(billedAccountUserId),
39+
])
40+
41+
if (adminBypass) {
42+
logger.info('Bypassing table limits for platform-admin-owned workspace', {
43+
workspaceId,
44+
billedAccountUserId,
45+
})
46+
return {
47+
maxTables: Number.MAX_SAFE_INTEGER,
48+
maxRowsPerTable: Number.MAX_SAFE_INTEGER,
49+
}
50+
}
51+
3652
const planName = getPlanTypeForLimits(subscription?.plan) as PlanName
3753

3854
const limits = planLimits[planName] ?? planLimits.free

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ describe('getWorkspaceInvitePolicy', () => {
305305
vi.clearAllMocks()
306306
mockFeatureFlags.isBillingEnabled = true
307307
mockGetHighestPrioritySubscription.mockResolvedValue(null)
308+
mockDbResults.value = [[{ role: 'user' }]]
308309
})
309310

310311
const baseState = {
@@ -414,4 +415,27 @@ describe('getWorkspaceInvitePolicy', () => {
414415
expect(result.allowed).toBe(false)
415416
expect(result.upgradeRequired).toBe(true)
416417
})
418+
419+
it('allows invites on a personal workspace billed to a platform admin', async () => {
420+
mockDbResults.value = [[{ role: 'admin' }]]
421+
422+
const result = await getWorkspaceInvitePolicy(baseState)
423+
424+
expect(result.allowed).toBe(true)
425+
expect(result.upgradeRequired).toBe(false)
426+
expect(result.reason).toBeNull()
427+
})
428+
429+
it('allows invites on a grandfathered workspace billed to a platform admin on a free plan', async () => {
430+
mockGetHighestPrioritySubscription.mockResolvedValueOnce(null)
431+
mockDbResults.value = [[{ role: 'admin' }]]
432+
433+
const result = await getWorkspaceInvitePolicy({
434+
...baseState,
435+
workspaceMode: WORKSPACE_MODE.GRANDFATHERED_SHARED,
436+
})
437+
438+
expect(result.allowed).toBe(true)
439+
expect(result.upgradeRequired).toBe(false)
440+
})
417441
})

apps/sim/lib/workspaces/policy.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { db } from '@sim/db'
2-
import { member, user, type WorkspaceMode, workspace } from '@sim/db/schema'
2+
import { member, type WorkspaceMode, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, count, eq, isNull } from 'drizzle-orm'
5+
import { isPlatformAdmin } from '@/lib/auth/platform-admin'
56
import { getOrganizationSubscription } from '@/lib/billing/core/billing'
67
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
78
import { getUserOrganization } from '@/lib/billing/organizations/membership'
@@ -84,12 +85,22 @@ export function isOrganizationWorkspace(
8485
export async function getWorkspaceInvitePolicy(
8586
workspaceState: WorkspaceOwnershipState
8687
): Promise<WorkspaceInvitePolicy> {
87-
const requiresSubscriptionLookup =
88-
isBillingEnabled && workspaceState.workspaceMode === WORKSPACE_MODE.GRANDFATHERED_SHARED
89-
const billedUserHasTeamOrEnterprise = requiresSubscriptionLookup
90-
? await hasActiveTeamOrEnterpriseSubscription(workspaceState.billedAccountUserId)
91-
: false
92-
return evaluateWorkspaceInvitePolicy(workspaceState, { billedUserHasTeamOrEnterprise })
88+
if (!isBillingEnabled) {
89+
return evaluateWorkspaceInvitePolicy(workspaceState, {
90+
billedUserHasTeamOrEnterprise: false,
91+
billedUserIsPlatformAdmin: false,
92+
})
93+
}
94+
const [billedUserHasTeamOrEnterprise, billedUserIsPlatformAdmin] = await Promise.all([
95+
workspaceState.workspaceMode === WORKSPACE_MODE.GRANDFATHERED_SHARED
96+
? hasActiveTeamOrEnterpriseSubscription(workspaceState.billedAccountUserId)
97+
: Promise.resolve(false),
98+
isPlatformAdmin(workspaceState.billedAccountUserId),
99+
])
100+
return evaluateWorkspaceInvitePolicy(workspaceState, {
101+
billedUserHasTeamOrEnterprise,
102+
billedUserIsPlatformAdmin,
103+
})
93104
}
94105

95106
/**
@@ -100,7 +111,7 @@ export async function getWorkspaceInvitePolicy(
100111
*/
101112
export function evaluateWorkspaceInvitePolicy(
102113
workspaceState: WorkspaceOwnershipState,
103-
context: { billedUserHasTeamOrEnterprise: boolean }
114+
context: { billedUserHasTeamOrEnterprise: boolean; billedUserIsPlatformAdmin: boolean }
104115
): WorkspaceInvitePolicy {
105116
if (!isBillingEnabled) {
106117
return {
@@ -112,6 +123,16 @@ export function evaluateWorkspaceInvitePolicy(
112123
}
113124
}
114125

126+
if (context.billedUserIsPlatformAdmin) {
127+
return {
128+
allowed: true,
129+
reason: null,
130+
requiresSeat: workspaceState.workspaceMode === WORKSPACE_MODE.ORGANIZATION,
131+
organizationId: workspaceState.organizationId,
132+
upgradeRequired: false,
133+
}
134+
}
135+
115136
if (workspaceState.workspaceMode === WORKSPACE_MODE.ORGANIZATION) {
116137
if (workspaceState.organizationId === null) {
117138
return {
@@ -357,12 +378,6 @@ export async function getOrganizationOwnerId(organizationId: string): Promise<st
357378
return ownerMembership?.userId ?? null
358379
}
359380

360-
async function isPlatformAdmin(userId: string): Promise<boolean> {
361-
const [row] = await db.select({ role: user.role }).from(user).where(eq(user.id, userId)).limit(1)
362-
363-
return row?.role === 'admin'
364-
}
365-
366381
/**
367382
* Like `getOrganizationOwnerId` but throws when no owner row exists.
368383
* Use when the caller needs a guaranteed billed-account userId — every

0 commit comments

Comments
 (0)