Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/docs/content/docs/platform/connected-accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Each organization has at most one pool. Creating it does not give any workspace

## Availability

Connected accounts must be enabled for your organization. Sim Cloud also requires an active Enterprise plan. Organization owners and admins manage the pool, subject to the organization's permission settings. A workspace admin who is not an organization admin cannot change the pool or its workspace access.
Sim Cloud requires an active Enterprise plan. Organization owners and admins manage the pool, subject to the organization's permission settings. A workspace admin who is not an organization admin cannot change the pool or its workspace access.

For self-hosted deployments using environment-based feature flags, set `CREDENTIAL_GROUPS=true`. Availability is organization-scoped; personal workspaces cannot use an organization pool.
For self-hosted deployments, set `CREDENTIAL_GROUPS=true`, or `ENTERPRISE_ENABLED=true` to enable the whole enterprise suite. Availability is organization-scoped; personal workspaces cannot use an organization pool.

Manage the pool under **Settings → Credential Groups**. It remains available independently of Search. Search administrators use **Settings → Sources**, while members connect personal Search accounts under **Integrations** in the main sidebar. These surfaces can share grants without sharing their purpose or resource settings.

Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/platform/enterprise/self-hosted.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Three features do not need a flag at all: **custom branding**, **session policie
| Session policies — on by default | `SESSION_POLICIES_ENABLED` | `NEXT_PUBLIC_SESSION_POLICIES_ENABLED` |
| Data retention deletion | `DATA_RETENTION_ENABLED` | `NEXT_PUBLIC_DATA_RETENTION_ENABLED` |
| Data drains | `DATA_DRAINS_ENABLED` | `NEXT_PUBLIC_DATA_DRAINS_ENABLED` |
| Credential Groups | `CREDENTIAL_GROUPS` | — |
| Workspace forks | `FORKING_ENABLED` | `NEXT_PUBLIC_FORKING_ENABLED` |
| Sim Mailer inbox — on by default | `INBOX_ENABLED` | `NEXT_PUBLIC_INBOX_ENABLED` |
| Sandboxes | `SANDBOXES_ENABLED` | `NEXT_PUBLIC_SANDBOXES_ENABLED` |
Expand Down
1 change: 0 additions & 1 deletion apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# DATA_DRAINS_ENABLED= / NEXT_PUBLIC_DATA_DRAINS_ENABLED= # Export streams
# FORKING_ENABLED= # Workspace forks
# CREDENTIAL_GROUPS= # Enterprise managed OAuth collections
# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup
# KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval
# ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only

Expand Down
7 changes: 0 additions & 7 deletions apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { createMockRequest } from '@sim/testing'
import { asyncJobsMock, asyncJobsMockFns } from '@sim/testing/mocks/async-jobs.mock'
import { authInternalMock, authInternalMockFns } from '@sim/testing/mocks/auth-internal.mock'
import {
tableTtlAvailabilityMock,
tableTtlAvailabilityMockFns,
} from '@sim/testing/mocks/table-ttl-availability.mock'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('@/lib/auth/internal', () => authInternalMock)
vi.mock('@/lib/core/async-jobs', () => asyncJobsMock)
vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock)

import { GET } from '@/app/api/cron/cleanup-table-row-ttl/route'

const { mockIsTableRowTtlEnabled } = tableTtlAvailabilityMockFns
const { mockVerifyCronAuth } = authInternalMockFns

const mockEnqueue = asyncJobsMockFns.mockJobQueue.enqueue
Expand All @@ -23,7 +17,6 @@ describe('table row TTL cleanup route', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-08-22T17:01:00Z'))
mockVerifyCronAuth.mockReturnValue(null)
mockIsTableRowTtlEnabled.mockResolvedValue(true)
mockEnqueue.mockResolvedValue('job-ttl-1')
})

Expand Down
6 changes: 0 additions & 6 deletions apps/sim/app/api/cron/cleanup-table-row-ttl/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { getJobQueue } from '@/lib/core/async-jobs'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability'

export const dynamic = 'force-dynamic'

Expand All @@ -15,11 +14,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
const authError = verifyCronAuth(request, 'table row TTL cleanup')
if (authError) return authError

if (!(await isTableRowTtlEnabled())) {
logger.info('Table row TTL cleanup skipped because the feature is disabled')
return NextResponse.json({ triggered: false, reason: 'feature-disabled' })
}

const queue = await getJobQueue()
const scheduleWindow = Math.floor(Date.now() / TTL_CLEANUP_INTERVAL_MS)
const jobId = await queue.enqueue(
Expand Down
11 changes: 1 addition & 10 deletions apps/sim/app/api/table/[tableId]/query/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { tableApiMock, tableApiMockFns } from '@sim/testing/mocks/table-api.mock'
import {
MockTableV2FeatureDisabledError,
tableApplicationRowsMock,
tableApplicationRowsMockFns,
} from '@sim/testing/mocks/table-application-rows.mock'
Expand All @@ -10,15 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/table/api', () => tableApiMock)

vi.mock('@/lib/table/api/row-route-policies', () => ({
internalTableV2QueryErrorPolicy: {
project: (error: unknown) =>
error instanceof MockTableV2FeatureDisabledError
? {
status: 403,
body: { error: error.message, code: 'tables_v2_disabled' },
}
: null,
},
internalTableV2QueryErrorPolicy: { project: () => null },
}))

vi.mock('@/lib/table/application/rows', () => tableApplicationRowsMock)
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/api/table/[tableId]/query/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const POST = defineInternalJsonRoute({
includeTotal: !body.cursor,
includeRunState: false,
allowExpandedLimit: true,
requireV2Feature: true,
includePersistedSecretProvenance: negotiateTableRowsProvenance(
request,
principal.kind === 'delegated'
Expand Down
29 changes: 0 additions & 29 deletions apps/sim/app/api/table/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,10 @@ import type { ColumnDefinition, Filter, TableDefinition, TablePredicate } from '
import { buildFilterClause, getTableById, TableQueryValidationError } from '@/lib/table'
import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants'
import { TableLockedError } from '@/lib/table/mutation-locks'
import {
getTableQueryAvailability,
TABLE_QUERY_UNAVAILABLE_REASON,
} from '@/lib/table/query-availability'
import { isTablePredicate } from '@/lib/table/query-builder/converters'
import { validateStoragePredicate } from '@/lib/table/query-builder/validate'
import type { TableLockKind } from '@/lib/table/types'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { getWorkspaceOrganizationId } from '@/lib/workspaces/utils'

/**
* Gate for the internal predicate-grammar table query route (`tables-v2-api`
* flag). Runs AFTER authorization, so the caller has already proven read
* access to the table — hiding the gate behind a bare 404 at that point
* serves nobody and reads as data loss (live incident: the table_v2 block
* hard-"Not found"-ing on every query while the copilot gateway, which
* bypasses HTTP, found the rows). Authorized callers get an honest 403
* naming the gate instead.
*/
export async function tablesV2GateError(
userId: string,
workspaceId: string
): Promise<NextResponse | null> {
const orgId = await getWorkspaceOrganizationId(workspaceId)
if ((await getTableQueryAvailability({ userId, orgId })).enabled) return null
return NextResponse.json(
{
error: TABLE_QUERY_UNAVAILABLE_REASON,
code: 'tables_v2_disabled',
},
{ status: 403 }
)
}

/**
* Maps a {@link TableLockedError} thrown by the service layer to a 423 response
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/api/v1/logs/projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ const { mockGetWorkspaceBillingSettings } = workspacesUtilsMockFns
workspacesUtilsMockFns.mockGetWorkspaceBilledAccountUserId.mockImplementation(
async () => 'billed-user'
)
workspacesUtilsMockFns.mockGetWorkspaceOrganizationId.mockImplementation(async () => null)
const { mockMaterializeExecutionDataForDisplay: mockMaterialize } = traceStoreMockFns

const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions
Expand Down
6 changes: 1 addition & 5 deletions apps/sim/app/api/v1/tables/[tableId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import { permissionsMock, permissionsMockFns } from '@sim/testing/mocks/permissi
import { createMockRequest } from '@sim/testing/mocks/request.mock'
import { tableMock, tableMockFns } from '@sim/testing/mocks/table.mock'
import { v1MiddlewareMock, v1MiddlewareMockFns } from '@sim/testing/mocks/v1-middleware.mock'
import {
workspacesUtilsMock,
workspacesUtilsMockFns,
} from '@sim/testing/mocks/workspaces-utils.mock'
import { workspacesUtilsMock } from '@sim/testing/mocks/workspaces-utils.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockPerformDeleteTable } = vi.hoisted(() => ({
Expand Down Expand Up @@ -48,7 +45,6 @@ vi.mock('@/lib/table/orchestration', () => ({

import { DELETE } from '@/app/api/v1/tables/[tableId]/route'

workspacesUtilsMockFns.mockGetWorkspaceOrganizationId.mockResolvedValue(null)
const { mockGetTableById } = tableMockFns

const { mockCheckRateLimit, mockCheckWorkspaceScope, mockResolveWorkspaceRequestActor } =
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/api/v1/tables/capability-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ const { mockGetWorkspaceBillingSettings } = workspacesUtilsMockFns
workspacesUtilsMockFns.mockGetWorkspaceBilledAccountUserId.mockImplementation(
async () => 'billed-user'
)
workspacesUtilsMockFns.mockGetWorkspaceOrganizationId.mockImplementation(async () => null)
const { mockGetTableById } = tableMockFns

const mockCheckWorkspaceAccess = permissionsMockFns.mockCheckWorkspaceAccess
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ async function render(
<FeatureFlagsProvider
flags={{
dashboards: false,
'table-row-ttl': false,
'mothership-model-selector': mocks.advanced,
'mothership-plan-mode': mocks.plan,
}}
Expand Down Expand Up @@ -310,7 +309,6 @@ it.each([
<FeatureFlagsProvider
flags={{
dashboards: false,
'table-row-ttl': false,
'mothership-model-selector': false,
'mothership-plan-mode': planEnabled,
}}
Expand Down Expand Up @@ -405,7 +403,6 @@ it('keeps restored queued skills scoped when replacing a draft', async () => {
<FeatureFlagsProvider
flags={{
dashboards: false,
'table-row-ttl': false,
'mothership-model-selector': mocks.advanced,
'mothership-plan-mode': mocks.plan,
}}
Expand Down
2 changes: 0 additions & 2 deletions apps/sim/app/o/[organizationId]/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { authMockFns } from '@sim/testing'
import { authClientMock, authClientMockFns } from '@sim/testing/mocks/auth-client.mock'
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
import { reactQueryMock } from '@sim/testing/mocks/react-query.mock'
import { tableTtlAvailabilityMock } from '@sim/testing/mocks/table-ttl-availability.mock'
import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { resolveDeploymentShape } from '@/lib/core/config/deployment-shape'
Expand All @@ -20,7 +19,6 @@ const {
mockUseMothershipChatEvents: vi.fn(),
}))

vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock)
vi.mock('@/app/workspace/providers/socket-provider', () => ({
SocketProvider: ({ children }: { children: import('react').ReactNode }) => children,
}))
Expand Down
26 changes: 11 additions & 15 deletions apps/sim/app/o/[organizationId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
import { OrganizationAccessDenied } from '@/app/o/[organizationId]/components/organization-access-denied'
Expand Down Expand Up @@ -56,27 +55,24 @@ export default async function OrganizationLayout({
if (!context.mothershipAvailable && !context.searchAccess.memberScoped)
redirect(WORKSPACE_SETTINGS_PATH)

const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] =
await Promise.all([
prefetchOrganizationSidebar(
queryClient,
organizationId,
{ kind: 'session', userId: session.user.id, sessionId: session.session.id },
getActiveOrganizationId(session)
),
isTableRowTtlEnabled(),
isMothershipModelSelectorEnabled(),
isPlanModeEnabled(),
isDashboardsEnabled(organizationId),
])
const [, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] = await Promise.all([
prefetchOrganizationSidebar(
queryClient,
organizationId,
{ kind: 'session', userId: session.user.id, sessionId: session.session.id },
getActiveOrganizationId(session)
),
isMothershipModelSelectorEnabled(),
isPlanModeEnabled(),
isDashboardsEnabled(organizationId),
])
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<FeatureFlagsProvider
flags={{
dashboards: dashboardsEnabled,
'table-row-ttl': tableRowTtlEnabled,
'mothership-model-selector': modelSelectorEnabled,
'mothership-plan-mode': planModeEnabled,
}}
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from '@/lib/desktop/executor/availability'
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
import { resolveOrganizationEntryPath } from '@/lib/navigation/resolve-app-entry'
import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner'
import { SessionExpired } from '@/app/workspace/[workspaceId]/components/session-expired'
Expand Down Expand Up @@ -66,7 +65,6 @@ export default async function WorkspaceLayout({
cookieStore,
initialOrgSettings,
,
tableRowTtlEnabled,
modelSelectorEnabled,
planModeEnabled,
organizationHref,
Expand All @@ -84,7 +82,6 @@ export default async function WorkspaceLayout({
hostContext,
activeOrganizationId
),
isTableRowTtlEnabled(),
isMothershipModelSelectorEnabled(),
isPlanModeEnabled(),
resolveOrganizationEntryPath(session),
Expand All @@ -100,7 +97,6 @@ export default async function WorkspaceLayout({
<FeatureFlagsProvider
flags={{
dashboards: dashboardsEnabled,
'table-row-ttl': tableRowTtlEnabled,
'mothership-model-selector': modelSelectorEnabled,
'mothership-plan-mode': planModeEnabled,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createContext, type ReactNode, useContext } from 'react'

export interface WorkspaceFeatureFlags {
dashboards: boolean
'table-row-ttl': boolean
'mothership-model-selector': boolean
'mothership-plan-mode': boolean
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ interface ColumnConfigSidebarProps {
/** Existing column record for `mode: 'edit'`; ignored otherwise. */
existingColumn: ColumnDefinition | null
allColumns: readonly ColumnDefinition[]
tableRowTtlEnabled: boolean
workspaceId: string
tableId: string
/** Notify parent of a rename so it can rewrite local `columnOrder` /
Expand Down Expand Up @@ -124,7 +123,6 @@ function ColumnConfigBody({
onClose,
existingColumn,
allColumns,
tableRowTtlEnabled,
workspaceId,
tableId,
onColumnRename,
Expand Down Expand Up @@ -306,9 +304,7 @@ function ColumnConfigBody({
<div className='flex flex-col gap-[9.5px]'>
<RequiredLabel>Type</RequiredLabel>
<ChipCombobox
options={columnTypeOptionsForTable(allColumns, existingColumn, {
tableRowTtlEnabled,
})
options={columnTypeOptionsForTable(allColumns, existingColumn)
.filter((option) => option.type !== 'workflow')
.map((option) => ({
label: option.label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ export interface ColumnTypeOption {
disabledReason?: string
}

interface ColumnTypeAvailability {
tableRowTtlEnabled: boolean
}

/**
* Real column types come from the registry — adding one there makes it appear
* in every picker automatically. `workflow` is appended because it is a UI
Expand Down Expand Up @@ -51,13 +47,9 @@ function columnTypeLimitMessage(label: string, maxPerTable: number): string {
/** Picker entries with unavailable cardinality-limited types marked as disabled. */
export function columnTypeOptionsForTable(
columns: readonly ColumnDefinition[],
currentColumn: ColumnDefinition | null | undefined,
availability: ColumnTypeAvailability
currentColumn: ColumnDefinition | null | undefined
): ColumnTypeOption[] {
return COLUMN_TYPE_OPTIONS.filter(
(option) =>
option.type !== 'ttl' || availability.tableRowTtlEnabled || currentColumn?.type === 'ttl'
).map((option) => {
return COLUMN_TYPE_OPTIONS.map((option) => {
if (option.type === 'workflow') return option
if (currentColumn?.type === option.type) return option
if (option.maxPerTable === undefined) return option
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const CELL_HEADER =

interface ColumnDropdownProps {
columns: readonly ColumnDefinition[]
tableRowTtlEnabled: boolean
/** `'header'` renders the page-header trigger (subtle Button); `'inline-header'` renders
* the in-table column-header `<th>` trigger. Same dropdown content either way. */
trigger: 'header' | 'inline-header'
Expand Down Expand Up @@ -78,7 +77,6 @@ function ColumnTypeMenuItem({ option, onSelect }: ColumnTypeMenuItemProps) {
*/
export function ColumnDropdown({
columns,
tableRowTtlEnabled,
trigger,
disabled,
onPickType,
Expand Down Expand Up @@ -132,7 +130,7 @@ export function ColumnDropdown({
<DropdownMenu>
<DropdownMenuTrigger asChild>{triggerButton}</DropdownMenuTrigger>
<DropdownMenuContent align='start' side='bottom' sideOffset={4}>
{columnTypeOptionsForTable(columns, undefined, { tableRowTtlEnabled }).map((option) => {
{columnTypeOptionsForTable(columns, undefined).map((option) => {
const onSelect =
option.type === 'workflow'
? onPickWorkflow
Expand Down
Loading
Loading