diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index 3e5acdf5e2..9c4c937cfe 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -31,33 +31,6 @@ Define permission groups to control what features and integrations team members --- -## Bring Your Own Key (BYOK) - -Use your own API keys for AI model providers instead of Sim Studio's hosted keys. - -### Supported Providers - -| Provider | Usage | -|----------|-------| -| OpenAI | Knowledge Base embeddings, Agent block | -| Anthropic | Agent block | -| Google | Agent block | -| Mistral | Knowledge Base OCR | - -### Setup - -1. Navigate to **Settings** → **BYOK** in your workspace -2. Click **Add Key** for your provider -3. Enter your API key and save - - - BYOK keys are encrypted at rest. Only organization admins and owners can manage keys. - - -When configured, workflows use your key instead of Sim Studio's hosted keys. If removed, workflows automatically fall back to hosted keys. - ---- - ## Single Sign-On (SSO) Enterprise authentication with SAML 2.0 and OIDC support for centralized identity management. @@ -117,4 +90,3 @@ curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \ ### Notes - Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership. -- BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables. diff --git a/apps/docs/content/docs/en/execution/costs.mdx b/apps/docs/content/docs/en/execution/costs.mdx index dce00ace96..a376a97bb7 100644 --- a/apps/docs/content/docs/en/execution/costs.mdx +++ b/apps/docs/content/docs/en/execution/costs.mdx @@ -106,7 +106,28 @@ The model breakdown shows: ## Bring Your Own Key (BYOK) -You can use your own API keys for hosted models (OpenAI, Anthropic, Google, Mistral) in **Settings → BYOK** to pay base prices. Keys are encrypted and apply workspace-wide. +Use your own API keys for AI model providers instead of Sim Studio's hosted keys to pay base prices with no markup. + +### Supported Providers + +| Provider | Usage | +|----------|-------| +| OpenAI | Knowledge Base embeddings, Agent block | +| Anthropic | Agent block | +| Google | Agent block | +| Mistral | Knowledge Base OCR | + +### Setup + +1. Navigate to **Settings** → **BYOK** in your workspace +2. Click **Add Key** for your provider +3. Enter your API key and save + + + BYOK keys are encrypted at rest. Only workspace admins can manage keys. + + +When configured, workflows use your key instead of Sim Studio's hosted keys. If removed, workflows automatically fall back to hosted keys with the multiplier. ## Cost Optimization Strategies diff --git a/apps/sim/app/api/v1/admin/byok/route.ts b/apps/sim/app/api/v1/admin/byok/route.ts deleted file mode 100644 index 8144993122..0000000000 --- a/apps/sim/app/api/v1/admin/byok/route.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Admin BYOK Keys API - * - * GET /api/v1/admin/byok - * List all BYOK keys with optional filtering. - * - * Query Parameters: - * - organizationId?: string - Filter by organization ID (finds all workspaces billed to this org) - * - workspaceId?: string - Filter by specific workspace ID - * - * Response: { data: AdminBYOKKey[], pagination: PaginationMeta } - * - * DELETE /api/v1/admin/byok - * Delete BYOK keys for an organization or workspace. - * Used when an enterprise plan churns to clean up BYOK keys. - * - * Query Parameters: - * - organizationId: string - Delete all BYOK keys for workspaces billed to this org - * - workspaceId?: string - Delete keys for a specific workspace only (optional) - * - * Response: { success: true, deletedCount: number, workspacesAffected: string[] } - */ - -import { db } from '@sim/db' -import { user, workspace, workspaceBYOKKeys } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { eq, inArray, sql } from 'drizzle-orm' -import { withAdminAuth } from '@/app/api/v1/admin/middleware' -import { - badRequestResponse, - internalErrorResponse, - singleResponse, -} from '@/app/api/v1/admin/responses' - -const logger = createLogger('AdminBYOKAPI') - -export interface AdminBYOKKey { - id: string - workspaceId: string - workspaceName: string - organizationId: string - providerId: string - createdAt: string - createdByUserId: string | null - createdByEmail: string | null -} - -export const GET = withAdminAuth(async (request) => { - const url = new URL(request.url) - const organizationId = url.searchParams.get('organizationId') - const workspaceId = url.searchParams.get('workspaceId') - - try { - let workspaceIds: string[] = [] - - if (workspaceId) { - workspaceIds = [workspaceId] - } else if (organizationId) { - const workspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .where(eq(workspace.billedAccountUserId, organizationId)) - - workspaceIds = workspaces.map((w) => w.id) - } - - const query = db - .select({ - id: workspaceBYOKKeys.id, - workspaceId: workspaceBYOKKeys.workspaceId, - workspaceName: workspace.name, - organizationId: workspace.billedAccountUserId, - providerId: workspaceBYOKKeys.providerId, - createdAt: workspaceBYOKKeys.createdAt, - createdByUserId: workspaceBYOKKeys.createdBy, - createdByEmail: user.email, - }) - .from(workspaceBYOKKeys) - .innerJoin(workspace, eq(workspaceBYOKKeys.workspaceId, workspace.id)) - .leftJoin(user, eq(workspaceBYOKKeys.createdBy, user.id)) - - let keys - if (workspaceIds.length > 0) { - keys = await query.where(inArray(workspaceBYOKKeys.workspaceId, workspaceIds)) - } else { - keys = await query - } - - const formattedKeys: AdminBYOKKey[] = keys.map((k) => ({ - id: k.id, - workspaceId: k.workspaceId, - workspaceName: k.workspaceName, - organizationId: k.organizationId, - providerId: k.providerId, - createdAt: k.createdAt.toISOString(), - createdByUserId: k.createdByUserId, - createdByEmail: k.createdByEmail, - })) - - logger.info('Admin API: Listed BYOK keys', { - organizationId, - workspaceId, - count: formattedKeys.length, - }) - - return singleResponse({ - data: formattedKeys, - pagination: { - total: formattedKeys.length, - limit: formattedKeys.length, - offset: 0, - hasMore: false, - }, - }) - } catch (error) { - logger.error('Admin API: Failed to list BYOK keys', { error, organizationId, workspaceId }) - return internalErrorResponse('Failed to list BYOK keys') - } -}) - -export const DELETE = withAdminAuth(async (request) => { - const url = new URL(request.url) - const organizationId = url.searchParams.get('organizationId') - const workspaceId = url.searchParams.get('workspaceId') - const reason = url.searchParams.get('reason') || 'Enterprise plan churn cleanup' - - if (!organizationId && !workspaceId) { - return badRequestResponse('Either organizationId or workspaceId is required') - } - - try { - let workspaceIds: string[] = [] - - if (workspaceId) { - workspaceIds = [workspaceId] - } else if (organizationId) { - const workspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .where(eq(workspace.billedAccountUserId, organizationId)) - - workspaceIds = workspaces.map((w) => w.id) - } - - if (workspaceIds.length === 0) { - logger.info('Admin API: No workspaces found for BYOK cleanup', { - organizationId, - workspaceId, - }) - return singleResponse({ - success: true, - deletedCount: 0, - workspacesAffected: [], - message: 'No workspaces found for the given organization/workspace ID', - }) - } - - const countResult = await db - .select({ count: sql`count(*)` }) - .from(workspaceBYOKKeys) - .where(inArray(workspaceBYOKKeys.workspaceId, workspaceIds)) - - const totalToDelete = Number(countResult[0]?.count ?? 0) - - if (totalToDelete === 0) { - logger.info('Admin API: No BYOK keys to delete', { - organizationId, - workspaceId, - workspaceIds, - }) - return singleResponse({ - success: true, - deletedCount: 0, - workspacesAffected: [], - message: 'No BYOK keys found for the specified workspaces', - }) - } - - await db.delete(workspaceBYOKKeys).where(inArray(workspaceBYOKKeys.workspaceId, workspaceIds)) - - logger.info('Admin API: Deleted BYOK keys', { - organizationId, - workspaceId, - workspaceIds, - deletedCount: totalToDelete, - reason, - }) - - return singleResponse({ - success: true, - deletedCount: totalToDelete, - workspacesAffected: workspaceIds, - reason, - }) - } catch (error) { - logger.error('Admin API: Failed to delete BYOK keys', { error, organizationId, workspaceId }) - return internalErrorResponse('Failed to delete BYOK keys') - } -}) diff --git a/apps/sim/app/api/v1/admin/index.ts b/apps/sim/app/api/v1/admin/index.ts index ad91e0c447..82e60e0eab 100644 --- a/apps/sim/app/api/v1/admin/index.ts +++ b/apps/sim/app/api/v1/admin/index.ts @@ -53,10 +53,6 @@ * GET /api/v1/admin/subscriptions/:id - Get subscription details * DELETE /api/v1/admin/subscriptions/:id - Cancel subscription (?atPeriodEnd=true for scheduled) * - * BYOK Keys: - * GET /api/v1/admin/byok - List BYOK keys (?organizationId=X or ?workspaceId=X) - * DELETE /api/v1/admin/byok - Delete BYOK keys for org/workspace - * * Access Control (Permission Groups): * GET /api/v1/admin/access-control - List permission groups (?organizationId=X) * DELETE /api/v1/admin/access-control - Delete permission groups for org (?organizationId=X) diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts index 84be273d12..246cc6b245 100644 --- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts @@ -6,8 +6,6 @@ import { nanoid } from 'nanoid' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { isEnterpriseOrgAdminOrOwner } from '@/lib/billing/core/subscription' -import { isHosted } from '@/lib/core/config/feature-flags' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -58,15 +56,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - let byokEnabled = true - if (isHosted) { - byokEnabled = await isEnterpriseOrgAdminOrOwner(userId) - } - - if (!byokEnabled) { - return NextResponse.json({ keys: [], byokEnabled: false }) - } - const byokKeys = await db .select({ id: workspaceBYOKKeys.id, @@ -108,7 +97,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }) ) - return NextResponse.json({ keys: formattedKeys, byokEnabled: true }) + return NextResponse.json({ keys: formattedKeys }) } catch (error: unknown) { logger.error(`[${requestId}] BYOK keys GET error`, error) return NextResponse.json( @@ -131,20 +120,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const userId = session.user.id - if (isHosted) { - const canManageBYOK = await isEnterpriseOrgAdminOrOwner(userId) - if (!canManageBYOK) { - logger.warn(`[${requestId}] User not authorized to manage BYOK keys`, { userId }) - return NextResponse.json( - { - error: - 'BYOK is an Enterprise-only feature. Only organization admins and owners can manage API keys.', - }, - { status: 403 } - ) - } - } - const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'admin') { return NextResponse.json( @@ -245,20 +220,6 @@ export async function DELETE( const userId = session.user.id - if (isHosted) { - const canManageBYOK = await isEnterpriseOrgAdminOrOwner(userId) - if (!canManageBYOK) { - logger.warn(`[${requestId}] User not authorized to manage BYOK keys`, { userId }) - return NextResponse.json( - { - error: - 'BYOK is an Enterprise-only feature. Only organization admins and owners can manage API keys.', - }, - { status: 403 } - ) - } - } - const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'admin') { return NextResponse.json( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx index e06ab18c3b..867c128f60 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { createLogger } from '@sim/logger' -import { Crown, Eye, EyeOff } from 'lucide-react' +import { Eye, EyeOff } from 'lucide-react' import { useParams } from 'next/navigation' import { Button, @@ -83,7 +83,6 @@ export function BYOK() { const { data, isLoading } = useBYOKKeys(workspaceId) const keys = data?.keys ?? [] - const byokEnabled = data?.byokEnabled ?? true const upsertKey = useUpsertBYOKKey() const deleteKey = useDeleteBYOKKey() @@ -98,31 +97,6 @@ export function BYOK() { return keys.find((k) => k.providerId === providerId) } - // Show enterprise-only gate if BYOK is not enabled - if (!isLoading && !byokEnabled) { - return ( -
-
- -
-
-

Enterprise Feature

-

- Bring Your Own Key (BYOK) is available exclusively on the Enterprise plan. Upgrade to - use your own API keys and eliminate the 2x cost multiplier. -

-
- -
- ) - } - const handleSave = async () => { if (!editingProvider || !apiKeyInput.trim()) return @@ -340,7 +314,7 @@ export function BYOK() { {PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name} {' '} - API key? This workspace will revert to using platform keys with the 2x multiplier. + API key? This workspace will revert to using platform hosted keys.

diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index a22df0a777..bc610fe74e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -152,9 +152,8 @@ const allNavigationItems: NavigationItem[] = [ id: 'byok', label: 'BYOK', icon: KeySquare, - section: 'enterprise', + section: 'system', requiresHosted: true, - requiresEnterprise: true, }, { id: 'copilot', diff --git a/apps/sim/hooks/queries/byok-keys.ts b/apps/sim/hooks/queries/byok-keys.ts index 36ec66827c..26d348d5a7 100644 --- a/apps/sim/hooks/queries/byok-keys.ts +++ b/apps/sim/hooks/queries/byok-keys.ts @@ -17,7 +17,6 @@ export interface BYOKKey { export interface BYOKKeysResponse { keys: BYOKKey[] - byokEnabled: boolean } export const byokKeysKeys = { @@ -33,7 +32,6 @@ async function fetchBYOKKeys(workspaceId: string): Promise { const data = await response.json() return { keys: data.keys ?? [], - byokEnabled: data.byokEnabled ?? true, } } diff --git a/apps/sim/lib/api-key/byok.ts b/apps/sim/lib/api-key/byok.ts index 34c589c21a..04a35adb42 100644 --- a/apps/sim/lib/api-key/byok.ts +++ b/apps/sim/lib/api-key/byok.ts @@ -2,7 +2,6 @@ import { db } from '@sim/db' import { workspaceBYOKKeys } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' -import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { getRotatingApiKey } from '@/lib/core/config/api-keys' import { isHosted } from '@/lib/core/config/feature-flags' import { decryptSecret } from '@/lib/core/security/encryption' @@ -91,18 +90,12 @@ export async function getApiKeyWithBYOK( logger.debug('BYOK check', { provider, model, workspaceId, isHosted, isModelHosted }) if (isModelHosted || isMistralModel) { - const hasEnterprise = await isWorkspaceOnEnterprisePlan(workspaceId) - - if (hasEnterprise) { - const byokResult = await getBYOKKey(workspaceId, byokProviderId) - if (byokResult) { - logger.info('Using BYOK key', { provider, model, workspaceId }) - return byokResult - } - logger.debug('No BYOK key found, falling back', { provider, model, workspaceId }) - } else { - logger.debug('Workspace not on enterprise plan, skipping BYOK', { workspaceId }) + const byokResult = await getBYOKKey(workspaceId, byokProviderId) + if (byokResult) { + logger.info('Using BYOK key', { provider, model, workspaceId }) + return byokResult } + logger.debug('No BYOK key found, falling back', { provider, model, workspaceId }) if (isModelHosted) { try { diff --git a/apps/sim/lib/billing/core/subscription.ts b/apps/sim/lib/billing/core/subscription.ts index d5721b7ebd..2b287da4a8 100644 --- a/apps/sim/lib/billing/core/subscription.ts +++ b/apps/sim/lib/billing/core/subscription.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { member, subscription, user, userStats, workspace } from '@sim/db/schema' +import { member, subscription, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' @@ -218,34 +218,6 @@ export async function isTeamOrgAdminOrOwner(userId: string): Promise { } } -/** - * Check if a workspace has access to enterprise features (BYOK) - * Used at execution time to determine if BYOK keys should be used - * Returns true if workspace's billed account is on enterprise plan - */ -export async function isWorkspaceOnEnterprisePlan(workspaceId: string): Promise { - try { - if (!isProd) { - return true - } - - const [ws] = await db - .select({ billedAccountUserId: workspace.billedAccountUserId }) - .from(workspace) - .where(eq(workspace.id, workspaceId)) - .limit(1) - - if (!ws) { - return false - } - - return isEnterprisePlan(ws.billedAccountUserId) - } catch (error) { - logger.error('Error checking workspace enterprise status', { error, workspaceId }) - return false - } -} - /** * Check if an organization has team or enterprise plan * Used at execution time (e.g., polling services) to check org billing directly diff --git a/apps/sim/lib/billing/index.ts b/apps/sim/lib/billing/index.ts index ddd4b8d1c5..9ec6f9cd6b 100644 --- a/apps/sim/lib/billing/index.ts +++ b/apps/sim/lib/billing/index.ts @@ -20,7 +20,6 @@ export { isProPlan as hasProPlan, isTeamOrgAdminOrOwner, isTeamPlan as hasTeamPlan, - isWorkspaceOnEnterprisePlan, sendPlanWelcomeEmail, } from '@/lib/billing/core/subscription' export * from '@/lib/billing/core/usage'