Skip to content

Commit c2180bf

Browse files
improvement(enterprise): feature flagging + runtime checks consolidation (#2730)
* improvement(enterprise): enterprise checks code consolidation * update docs * revert isHosted check * add unique index to prevent multiple orgs per user * address greptile comments * ui bug
1 parent fdac431 commit c2180bf

File tree

29 files changed

+10218
-137
lines changed

29 files changed

+10218
-137
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
title: Enterprise
3+
description: Enterprise features for organizations with advanced security and compliance requirements
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
8+
Sim Studio Enterprise provides advanced features for organizations with enhanced security, compliance, and management requirements.
9+
10+
---
11+
12+
## Bring Your Own Key (BYOK)
13+
14+
Use your own API keys for AI model providers instead of Sim Studio's hosted keys.
15+
16+
### Supported Providers
17+
18+
| Provider | Usage |
19+
|----------|-------|
20+
| OpenAI | Knowledge Base embeddings, Agent block |
21+
| Anthropic | Agent block |
22+
| Google | Agent block |
23+
| Mistral | Knowledge Base OCR |
24+
25+
### Setup
26+
27+
1. Navigate to **Settings****BYOK** in your workspace
28+
2. Click **Add Key** for your provider
29+
3. Enter your API key and save
30+
31+
<Callout type="warn">
32+
BYOK keys are encrypted at rest. Only organization admins and owners can manage keys.
33+
</Callout>
34+
35+
When configured, workflows use your key instead of Sim Studio's hosted keys. If removed, workflows automatically fall back to hosted keys.
36+
37+
---
38+
39+
## Single Sign-On (SSO)
40+
41+
Enterprise authentication with SAML 2.0 and OIDC support for centralized identity management.
42+
43+
### Supported Providers
44+
45+
- Okta
46+
- Azure AD / Entra ID
47+
- Google Workspace
48+
- OneLogin
49+
- Any SAML 2.0 or OIDC provider
50+
51+
### Setup
52+
53+
1. Navigate to **Settings****SSO** in your workspace
54+
2. Choose your identity provider
55+
3. Configure the connection using your IdP's metadata
56+
4. Enable SSO for your organization
57+
58+
<Callout type="info">
59+
Once SSO is enabled, team members authenticate through your identity provider instead of email/password.
60+
</Callout>
61+
62+
---
63+
64+
## Self-Hosted
65+
66+
For self-hosted deployments, enterprise features can be enabled via environment variables:
67+
68+
| Variable | Description |
69+
|----------|-------------|
70+
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC |
71+
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers |
72+
73+
<Callout type="warn">
74+
BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables.
75+
</Callout>

apps/docs/content/docs/en/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"permissions",
1616
"sdks",
1717
"self-hosting",
18+
"./enterprise/index",
1819
"./keyboard-shortcuts/index"
1920
],
2021
"defaultOpen": false

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import { auth } from '@/lib/auth'
4+
import { auth, getSession } from '@/lib/auth'
5+
import { hasSSOAccess } from '@/lib/billing'
56
import { env } from '@/lib/core/config/env'
67
import { REDACTED_MARKER } from '@/lib/core/security/redaction'
78

@@ -63,10 +64,22 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
6364

6465
export async function POST(request: NextRequest) {
6566
try {
67+
// SSO plugin must be enabled in Better Auth
6668
if (!env.SSO_ENABLED) {
6769
return NextResponse.json({ error: 'SSO is not enabled' }, { status: 400 })
6870
}
6971

72+
// Check plan access (enterprise) or env var override
73+
const session = await getSession()
74+
if (!session?.user?.id) {
75+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
76+
}
77+
78+
const hasAccess = await hasSSOAccess(session.user.id)
79+
if (!hasAccess) {
80+
return NextResponse.json({ error: 'SSO requires an Enterprise plan' }, { status: 403 })
81+
}
82+
7083
const rawBody = await request.json()
7184

7285
const parseResult = ssoRegistrationSchema.safeParse(rawBody)

apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
77
import { getSession } from '@/lib/auth'
8+
import { hasCredentialSetsAccess } from '@/lib/billing'
89
import { getBaseUrl } from '@/lib/core/utils/urls'
910
import { sendEmail } from '@/lib/messaging/email/mailer'
1011

@@ -45,6 +46,15 @@ export async function POST(
4546
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
4647
}
4748

49+
// Check plan access (team/enterprise) or env var override
50+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
51+
if (!hasAccess) {
52+
return NextResponse.json(
53+
{ error: 'Credential sets require a Team or Enterprise plan' },
54+
{ status: 403 }
55+
)
56+
}
57+
4858
const { id, invitationId } = await params
4959

5060
try {

apps/sim/app/api/credential-sets/[id]/invite/route.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
88
import { getSession } from '@/lib/auth'
9+
import { hasCredentialSetsAccess } from '@/lib/billing'
910
import { getBaseUrl } from '@/lib/core/utils/urls'
1011
import { sendEmail } from '@/lib/messaging/email/mailer'
1112

@@ -47,6 +48,15 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
4748
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
4849
}
4950

51+
// Check plan access (team/enterprise) or env var override
52+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
53+
if (!hasAccess) {
54+
return NextResponse.json(
55+
{ error: 'Credential sets require a Team or Enterprise plan' },
56+
{ status: 403 }
57+
)
58+
}
59+
5060
const { id } = await params
5161
const result = await getCredentialSetWithAccess(id, session.user.id)
5262

@@ -69,6 +79,15 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
6979
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
7080
}
7181

82+
// Check plan access (team/enterprise) or env var override
83+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
84+
if (!hasAccess) {
85+
return NextResponse.json(
86+
{ error: 'Credential sets require a Team or Enterprise plan' },
87+
{ status: 403 }
88+
)
89+
}
90+
7291
const { id } = await params
7392

7493
try {
@@ -178,6 +197,15 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
178197
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
179198
}
180199

200+
// Check plan access (team/enterprise) or env var override
201+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
202+
if (!hasAccess) {
203+
return NextResponse.json(
204+
{ error: 'Credential sets require a Team or Enterprise plan' },
205+
{ status: 403 }
206+
)
207+
}
208+
181209
const { id } = await params
182210
const { searchParams } = new URL(req.url)
183211
const invitationId = searchParams.get('invitationId')

apps/sim/app/api/credential-sets/[id]/members/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, eq, inArray } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
7+
import { hasCredentialSetsAccess } from '@/lib/billing'
78
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
89

910
const logger = createLogger('CredentialSetMembers')
@@ -39,6 +40,15 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
3940
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
4041
}
4142

43+
// Check plan access (team/enterprise) or env var override
44+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
45+
if (!hasAccess) {
46+
return NextResponse.json(
47+
{ error: 'Credential sets require a Team or Enterprise plan' },
48+
{ status: 403 }
49+
)
50+
}
51+
4252
const { id } = await params
4353
const result = await getCredentialSetWithAccess(id, session.user.id)
4454

@@ -110,6 +120,15 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
110120
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
111121
}
112122

123+
// Check plan access (team/enterprise) or env var override
124+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
125+
if (!hasAccess) {
126+
return NextResponse.json(
127+
{ error: 'Credential sets require a Team or Enterprise plan' },
128+
{ status: 403 }
129+
)
130+
}
131+
113132
const { id } = await params
114133
const { searchParams } = new URL(req.url)
115134
const memberId = searchParams.get('memberId')

apps/sim/app/api/credential-sets/[id]/route.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { getSession } from '@/lib/auth'
8+
import { hasCredentialSetsAccess } from '@/lib/billing'
89

910
const logger = createLogger('CredentialSet')
1011

@@ -49,6 +50,15 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
4950
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
5051
}
5152

53+
// Check plan access (team/enterprise) or env var override
54+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
55+
if (!hasAccess) {
56+
return NextResponse.json(
57+
{ error: 'Credential sets require a Team or Enterprise plan' },
58+
{ status: 403 }
59+
)
60+
}
61+
5262
const { id } = await params
5363
const result = await getCredentialSetWithAccess(id, session.user.id)
5464

@@ -66,6 +76,15 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
6676
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
6777
}
6878

79+
// Check plan access (team/enterprise) or env var override
80+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
81+
if (!hasAccess) {
82+
return NextResponse.json(
83+
{ error: 'Credential sets require a Team or Enterprise plan' },
84+
{ status: 403 }
85+
)
86+
}
87+
6988
const { id } = await params
7089

7190
try {
@@ -129,6 +148,15 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
129148
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
130149
}
131150

151+
// Check plan access (team/enterprise) or env var override
152+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
153+
if (!hasAccess) {
154+
return NextResponse.json(
155+
{ error: 'Credential sets require a Team or Enterprise plan' },
156+
{ status: 403 }
157+
)
158+
}
159+
132160
const { id } = await params
133161

134162
try {

apps/sim/app/api/credential-sets/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { and, count, desc, eq } from 'drizzle-orm'
55
import { NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { getSession } from '@/lib/auth'
8+
import { hasCredentialSetsAccess } from '@/lib/billing'
89

910
const logger = createLogger('CredentialSets')
1011

@@ -22,6 +23,15 @@ export async function GET(req: Request) {
2223
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
2324
}
2425

26+
// Check plan access (team/enterprise) or env var override
27+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
28+
if (!hasAccess) {
29+
return NextResponse.json(
30+
{ error: 'Credential sets require a Team or Enterprise plan' },
31+
{ status: 403 }
32+
)
33+
}
34+
2535
const { searchParams } = new URL(req.url)
2636
const organizationId = searchParams.get('organizationId')
2737

@@ -85,6 +95,15 @@ export async function POST(req: Request) {
8595
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
8696
}
8797

98+
// Check plan access (team/enterprise) or env var override
99+
const hasAccess = await hasCredentialSetsAccess(session.user.id)
100+
if (!hasAccess) {
101+
return NextResponse.json(
102+
{ error: 'Credential sets require a Team or Enterprise plan' },
103+
{ status: 403 }
104+
)
105+
}
106+
88107
try {
89108
const body = await req.json()
90109
const { organizationId, name, description, providerId } = createCredentialSetSchema.parse(body)

0 commit comments

Comments
 (0)