diff --git a/apps/docs/content/docs/en/enterprise/access-control.mdx b/apps/docs/content/docs/en/enterprise/access-control.mdx new file mode 100644 index 0000000000..2e07b677ff --- /dev/null +++ b/apps/docs/content/docs/en/enterprise/access-control.mdx @@ -0,0 +1,216 @@ +--- +title: Access Control +description: Restrict which models, blocks, and platform features each group of users can access +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { FAQ } from '@/components/ui/faq' +import { Image } from '@/components/ui/image' + +Access Control lets organization admins define permission groups that restrict what each set of users can do — which AI model providers they can use, which workflow blocks they can place, and which platform features are visible to them. Restrictions are enforced both in the workflow executor and in Mothership. + +--- + +## How it works + +Access control is built around **permission groups**. Each group has a name, an optional description, and a configuration that defines what its members can and cannot do. A user can belong to at most one permission group at a time. + +When a user runs a workflow or uses Mothership, Sim reads their group's configuration and applies it: + +- **In the executor:** If a workflow uses a disallowed block type or model provider, execution halts immediately with an error. This applies to both manual runs and scheduled or API-triggered deployments. +- **In Mothership:** Disallowed blocks are filtered out of the block list so they cannot be added to a workflow. Disallowed tool types (MCP, custom tools, skills) are skipped if Mothership attempts to use them. + +--- + +## Setup + +### 1. Open Access Control settings + +Go to **Settings → Enterprise → Access Control** in your workspace. + +Access Control settings showing a list of permission groups: Contractors, Sales, Engineering, and Marketing, each with Details and Delete actions + +### 2. Create a permission group + +Click **+ Create** and enter a name (required) and optional description. You can also enable **Auto-add new members** — when active, any new member who joins the organization is automatically added to this group. Only one group per organization can have this setting enabled at a time. + +### 3. Configure permissions + +Click **Details** on a group, then open **Configure Permissions**. There are three tabs. + +#### Model Providers + +Controls which AI model providers members of this group can use. + +Model Providers tab showing a grid of AI providers including Ollama, vLLM, OpenAI, Anthropic, Google, Azure OpenAI, and others with checkboxes to allow or restrict access The list shows all providers available in Sim. + +- **All checked (default):** All providers are allowed. +- **Subset checked:** Only the selected providers are allowed. Any workflow block or agent using a provider not on the list will fail at execution time. + +#### Blocks + +Controls which workflow blocks members can place and execute. + +Blocks tab showing Core Blocks (Agent, API, Condition, Function, Knowledge, etc.) and Tools (integrations like 1Password, A2A, Ahrefs, Airtable, and more) with checkboxes to allow or restrict each Blocks are split into two sections: **Core Blocks** (Agent, API, Condition, Function, etc.) and **Tools** (all integration blocks). + +- **All checked (default):** All blocks are allowed. +- **Subset checked:** Only the selected blocks are allowed. Workflows that already contain a disallowed block will fail when run — they are not automatically modified. + + + The `start_trigger` block (the entry point of every workflow) is always allowed and cannot be restricted. + + +#### Platform + +Controls visibility of platform features and modules. + +Platform tab showing feature toggles grouped by category: Sidebar (Knowledge Base, Tables, Templates), Workflow Panel (Copilot), Settings Tabs, Tools, Deploy Tabs, Features, Logs, and Collaboration Each checkbox maps to a specific feature; checking it hides or disables that feature for group members. + +**Sidebar** + +| Feature | Effect when checked | +|---------|-------------------| +| Knowledge Base | Hides the Knowledge Base section from the sidebar | +| Tables | Hides the Tables section from the sidebar | +| Templates | Hides the Templates section from the sidebar | + +**Workflow Panel** + +| Feature | Effect when checked | +|---------|-------------------| +| Copilot | Hides the Copilot panel inside the workflow editor | + +**Settings Tabs** + +| Feature | Effect when checked | +|---------|-------------------| +| Integrations | Hides the Integrations tab in Settings | +| Secrets | Hides the Secrets tab in Settings | +| API Keys | Hides the Sim Keys tab in Settings | +| Files | Hides the Files tab in Settings | + +**Tools** + +| Feature | Effect when checked | +|---------|-------------------| +| MCP Tools | Disables the use of MCP tools in workflows and agents | +| Custom Tools | Disables the use of custom tools in workflows and agents | +| Skills | Disables the use of Sim Skills in workflows and agents | + +**Deploy Tabs** + +| Feature | Effect when checked | +|---------|-------------------| +| API | Hides the API deployment tab | +| MCP | Hides the MCP deployment tab | +| A2A | Hides the A2A deployment tab | +| Chat | Hides the Chat deployment tab | +| Template | Hides the Template deployment tab | + +**Features** + +| Feature | Effect when checked | +|---------|-------------------| +| Sim Mailer | Hides the Sim Mailer (Inbox) feature | +| Public API | Disables public API access for deployed workflows | + +**Logs** + +| Feature | Effect when checked | +|---------|-------------------| +| Trace Spans | Hides trace span details in execution logs | + +**Collaboration** + +| Feature | Effect when checked | +|---------|-------------------| +| Invitations | Disables the ability to invite new members to the workspace | + +### 4. Add members + +Open the group's **Details** view and add members by searching for users by name or email. Users can only belong to one group at a time — adding a user to a new group removes them from their current one. + +--- + +## Enforcement + +### Workflow execution + +Restrictions are enforced at the point of execution, not at save time. If a group's configuration changes after a workflow is built: + +- **Block restrictions:** Any workflow run that reaches a disallowed block halts immediately with an error. The workflow is not modified — only execution is blocked. +- **Model provider restrictions:** Any block or agent that uses a disallowed provider halts immediately with an error. +- **Tool restrictions (MCP, custom tools, skills):** Agents that use a disallowed tool type halt immediately with an error. + +This applies regardless of how the workflow is triggered — manually, via API, via schedule, or via webhook. + +### Mothership + +When a user opens Mothership, their permission group is read before any block or tool suggestions are made: + +- Blocks not in the allowed list are filtered out of the block picker entirely — they do not appear as options. +- If Mothership generates a workflow step that would use a disallowed tool (MCP, custom, or skills), that step is skipped and the reason is noted. + +--- + +## User membership rules + +- A user can belong to **at most one** permission group at a time. +- Moving a user to a new group automatically removes them from their current group. +- Users not assigned to any group have no restrictions applied (all blocks, providers, and features are available to them). +- If **Auto-add new members** is enabled on a group, new organization members are automatically placed in that group. Only one group per organization can have this setting active. + +--- + + + +--- + +## Self-hosted setup + +Self-hosted deployments use environment variables instead of the billing/plan check. + +### Environment variables + +```bash +ACCESS_CONTROL_ENABLED=true +NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true +``` + +You can also set a server-level block allowlist using the `ALLOWED_INTEGRATIONS` environment variable. This is applied as an additional constraint on top of any permission group configuration — a block must be allowed by both the environment allowlist and the user's group to be usable. + +```bash +# Only these block types are available across the entire instance +ALLOWED_INTEGRATIONS=slack,gmail,agent,function,condition +``` + +Once enabled, permission groups are managed through **Settings → Enterprise → Access Control** the same way as Sim Cloud. diff --git a/apps/docs/content/docs/en/enterprise/audit-logs.mdx b/apps/docs/content/docs/en/enterprise/audit-logs.mdx new file mode 100644 index 0000000000..ebd9be41a1 --- /dev/null +++ b/apps/docs/content/docs/en/enterprise/audit-logs.mdx @@ -0,0 +1,146 @@ +--- +title: Audit Logs +description: Track every action taken across your organization's workspaces +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { FAQ } from '@/components/ui/faq' +import { Image } from '@/components/ui/image' + +Audit logs give your organization a tamper-evident record of every significant action taken across workspaces — who did what, when, and on which resource. Use them for security reviews, compliance investigations, and incident response. + +--- + +## Viewing audit logs + +### In the UI + +Go to **Settings → Enterprise → Audit Logs** in your workspace. Logs are displayed in a table with the following columns: + +Audit Logs settings showing a table of events with columns for Timestamp, Event, Description, and Actor, along with search and filter controls + +| Column | Description | +|--------|-------------| +| **Timestamp** | When the action occurred. | +| **Event** | The action taken, e.g. `workflow.created`. | +| **Description** | A human-readable summary of the action. | +| **Actor** | The email address of the user who performed the action. | + +Use the search bar, event type filter, and date range selector to narrow results. + +### Via API + +Audit logs are also accessible through the Sim API for integration with external SIEM or log management tools. + +```http +GET /api/v1/audit-logs +Authorization: Bearer +``` + +**Query parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `action` | string | Filter by event type (e.g. `workflow.created`) | +| `resourceType` | string | Filter by resource type (e.g. `workflow`) | +| `resourceId` | string | Filter by a specific resource ID | +| `workspaceId` | string | Filter by workspace | +| `actorId` | string | Filter by user ID (must be an org member) | +| `startDate` | string | ISO 8601 date — return logs on or after this date | +| `endDate` | string | ISO 8601 date — return logs on or before this date | +| `includeDeparted` | boolean | Include logs from members who have since left the organization (default `false`) | +| `limit` | number | Results per page (1–100, default 50) | +| `cursor` | string | Opaque cursor for fetching the next page | + +**Example response:** + +```json +{ + "data": [ + { + "id": "abc123", + "action": "workflow.created", + "resourceType": "workflow", + "resourceId": "wf_xyz", + "resourceName": "Customer Onboarding", + "description": "Created workflow \"Customer Onboarding\"", + "actorId": "usr_abc", + "actorName": "Alice Smith", + "actorEmail": "alice@company.com", + "workspaceId": "ws_def", + "metadata": {}, + "createdAt": "2026-04-20T21:16:00.000Z" + } + ], + "nextCursor": "eyJpZCI6ImFiYzEyMyJ9" +} +``` + +Paginate by passing the `nextCursor` value as the `cursor` parameter in the next request. When `nextCursor` is absent, you have reached the last page. + + + The API accepts both personal and workspace-scoped API keys. Rate limits apply — the response includes `X-RateLimit-*` headers with your current limit and remaining quota. + + +--- + +## Event types + +Audit log events follow a `resource.action` naming pattern. The table below lists the main categories. + +| Category | Example events | +|----------|---------------| +| **Workflows** | `workflow.created`, `workflow.deleted`, `workflow.deployed`, `workflow.locked` | +| **Workspaces** | `workspace.created`, `workspace.updated`, `workspace.deleted` | +| **Members** | `member.invited`, `member.removed`, `member.role_changed` | +| **Permission groups** | `permission_group.created`, `permission_group.updated`, `permission_group.deleted` | +| **Environments** | `environment.updated`, `environment.deleted` | +| **Knowledge bases** | `knowledge_base.created`, `knowledge_base.deleted`, `connector.synced` | +| **Tables** | `table.created`, `table.updated`, `table.deleted` | +| **API keys** | `api_key.created`, `api_key.revoked` | +| **Credentials** | `credential.created`, `credential.deleted`, `oauth.disconnected` | +| **Organization** | `organization.updated`, `org_member.added`, `org_member.role_changed` | + +--- + + + +--- + +## Self-hosted setup + +Self-hosted deployments use environment variables instead of the billing/plan check. + +### Environment variables + +```bash +AUDIT_LOGS_ENABLED=true +NEXT_PUBLIC_AUDIT_LOGS_ENABLED=true +``` + +Once enabled, audit logs are viewable in **Settings → Enterprise → Audit Logs** and accessible via the API. diff --git a/apps/docs/content/docs/en/enterprise/data-retention.mdx b/apps/docs/content/docs/en/enterprise/data-retention.mdx new file mode 100644 index 0000000000..2590d32408 --- /dev/null +++ b/apps/docs/content/docs/en/enterprise/data-retention.mdx @@ -0,0 +1,131 @@ +--- +title: Data Retention +description: Control how long execution logs, deleted resources, and copilot data are kept before permanent deletion +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { FAQ } from '@/components/ui/faq' +import { Image } from '@/components/ui/image' + +Data Retention lets workspace admins on Enterprise plans configure how long three categories of data are kept before they are permanently deleted. Each workspace in your organization can have its own independent configuration. + +--- + +## Setup + +Go to **Settings → Enterprise → Data Retention** in your workspace. + +Data Retention settings showing three dropdowns — Log retention, Soft deletion cleanup, and Task cleanup — each set to Forever + +You will see three independent settings, each with the same set of options: **1 day, 3 days, 7 days, 14 days, 30 days, 60 days, 90 days, 180 days, 1 year, 5 years,** or **Forever**. + +Setting a period to **Forever** means that category of data is never automatically deleted. + +--- + +## Settings + +### Log retention + +Controls how long **workflow execution logs** are kept. + +When the retention period expires, execution log records are permanently deleted, along with any files associated with those executions stored in cloud storage. + +### Soft deletion cleanup + +Controls how long **soft-deleted resources** remain recoverable before permanent removal. + +When you delete a workflow, folder, knowledge base, table, or file, it is initially soft-deleted and can be recovered from Recently Deleted. Once the soft deletion cleanup period expires, those resources are permanently removed and cannot be recovered. + +Resources covered: + +- Workflows +- Workflow folders +- Knowledge bases +- Tables +- Files +- MCP server configurations +- Agent memory + +### Task cleanup + +Controls how long **Mothership data** is kept, including: + +- Copilot chats and run history +- Run checkpoints and async tool calls +- Inbox tasks (Sim Mailer) + + + Each setting is independent. You can configure a short log retention period alongside a long soft deletion cleanup period, or set any combination that fits your compliance requirements. + + +--- + +## Per-workspace configuration + +Retention is configured at the **workspace level**, not organization-wide. Each workspace in your organization can have a different configuration. Changes to one workspace's settings do not affect other workspaces. + +--- + +## Plan defaults + +Non-enterprise workspaces use the following automatic defaults. These cannot be changed. + +| Setting | Free | Pro | Team | +|---------|------|-----|------| +| Log retention | 30 days | Not configured | Not configured | +| Soft deletion cleanup | 30 days | 90 days | 90 days | +| Task cleanup | Not configured | Not configured | Not configured | + +"Not configured" means that category of data is not automatically deleted on that plan. + +Enterprise workspaces have no defaults — retention only runs for a setting once you configure it. Until configured, that category of data is not automatically deleted. + + + On Enterprise, setting a period to **Forever** explicitly keeps data indefinitely. Leaving a setting unconfigured has the same effect, but setting it to Forever makes the intent explicit and allows you to change it later without needing to save from scratch. + + +--- + + + +--- + +## Self-hosted setup + +### Environment variables + +```bash +NEXT_PUBLIC_DATA_RETENTION_ENABLED=true +``` + +Once enabled, data retention settings are configurable through **Settings → Enterprise → Data Retention** the same way as Sim Cloud. diff --git a/apps/docs/content/docs/en/enterprise/meta.json b/apps/docs/content/docs/en/enterprise/meta.json index 86316a8d2f..05e1a74e09 100644 --- a/apps/docs/content/docs/en/enterprise/meta.json +++ b/apps/docs/content/docs/en/enterprise/meta.json @@ -1,5 +1,5 @@ { "title": "Enterprise", - "pages": ["index", "sso"], + "pages": ["index", "sso", "access-control", "whitelabeling", "audit-logs", "data-retention"], "defaultOpen": false } diff --git a/apps/docs/content/docs/en/enterprise/sso.mdx b/apps/docs/content/docs/en/enterprise/sso.mdx index 174adb0a3f..8cc264d7dc 100644 --- a/apps/docs/content/docs/en/enterprise/sso.mdx +++ b/apps/docs/content/docs/en/enterprise/sso.mdx @@ -6,19 +6,12 @@ description: Configure SAML 2.0 or OIDC-based single sign-on for your organizati import { Callout } from 'fumadocs-ui/components/callout' import { Tab, Tabs } from 'fumadocs-ui/components/tabs' import { FAQ } from '@/components/ui/faq' +import { Image } from '@/components/ui/image' Single Sign-On lets your team sign in to Sim through your company's identity provider instead of managing separate passwords. Sim supports both OIDC and SAML 2.0. --- -## Requirements - -**Sim Cloud:** Enterprise plan. You must be a workspace owner or admin. - -**Self-hosted:** Set `SSO_ENABLED=true` and `NEXT_PUBLIC_SSO_ENABLED=true` in your environment. No plan requirement. - ---- - ## Setup ### 1. Open SSO settings @@ -34,6 +27,8 @@ Go to **Settings → Enterprise → Single Sign-On** in your workspace. ### 3. Fill in the form +Single Sign-On configuration form showing Provider Type (OIDC), Provider ID, Issuer URL, Domain, Client ID, Client Secret, Scopes, and Callback URL fields + **Fields required for both protocols:** | Field | What to enter | diff --git a/apps/docs/content/docs/en/enterprise/whitelabeling.mdx b/apps/docs/content/docs/en/enterprise/whitelabeling.mdx new file mode 100644 index 0000000000..a4c5f527f2 --- /dev/null +++ b/apps/docs/content/docs/en/enterprise/whitelabeling.mdx @@ -0,0 +1,106 @@ +--- +title: Whitelabeling +description: Replace Sim branding with your own logo, colors, and links +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { FAQ } from '@/components/ui/faq' +import { Image } from '@/components/ui/image' + +Whitelabeling lets you replace Sim's default branding — logo, colors, and support links — with your own. Members of your organization see your brand instead of Sim's throughout the workspace. + +--- + +## Setup + +### 1. Open Whitelabeling settings + +Go to **Settings → Enterprise → Whitelabeling** in your workspace. + +Whitelabeling settings showing brand identity fields (Logo, Wordmark, Brand name), color pickers for primary and accent colors, and link fields for support email and documentation URL + +### 2. Configure brand identity + +| Field | Description | +|-------|-------------| +| **Logo** | Shown in the collapsed sidebar. Square image (PNG, JPEG, SVG, or WebP). Max 5 MB. | +| **Wordmark** | Shown in the expanded sidebar. Wide image (PNG, JPEG, SVG, or WebP). Max 5 MB. | +| **Brand name** | Replaces "Sim" in the sidebar and select UI elements. Max 64 characters. | + + +### 3. Configure colors + +All colors must be valid hex values (e.g. `#701ffc`). + +| Field | Description | +|-------|-------------| +| **Primary color** | Main accent color used for buttons and active states. | +| **Primary hover color** | Color shown when hovering over primary elements. | +| **Accent color** | Secondary accent for highlights and secondary interactive elements. | +| **Accent hover color** | Color shown when hovering over accent elements. | + +### 4. Configure links + +Replace Sim's default support and legal links with your own. + +| Field | Description | +|-------|-------------| +| **Support email** | Shown in help prompts. Must be a valid email address. | +| **Documentation URL** | Link to your internal documentation. Must be a valid URL. | +| **Terms of service URL** | Link to your terms page. Must be a valid URL. | +| **Privacy policy URL** | Link to your privacy page. Must be a valid URL. | + +### 5. Save + +Click **Save changes**. The new branding is applied immediately for all members of your organization. + +--- + +## What gets replaced + +Whitelabeling replaces the following visual elements: + +- **Sidebar logo and wordmark** — your uploaded images replace the Sim logo +- **Brand name** — appears in the sidebar and select UI labels +- **Primary and accent colors** — applied to buttons, active states, and highlights +- **Support and legal links** — help prompts and footer links point to your URLs + + + Whitelabeling applies only to members of your organization. Public-facing pages (login, marketing) are not affected. + + +--- + + + +--- + +## Self-hosted setup + +Self-hosted deployments use environment variables instead of the billing/plan check. + +### Environment variables + +```bash +WHITELABELING_ENABLED=true +NEXT_PUBLIC_WHITELABELING_ENABLED=true +``` + +Once enabled, configure branding through **Settings → Enterprise → Whitelabeling** the same way as Sim Cloud. diff --git a/apps/docs/public/static/enterprise/access-control-blocks.png b/apps/docs/public/static/enterprise/access-control-blocks.png new file mode 100644 index 0000000000..709be54f7e Binary files /dev/null and b/apps/docs/public/static/enterprise/access-control-blocks.png differ diff --git a/apps/docs/public/static/enterprise/access-control-groups.png b/apps/docs/public/static/enterprise/access-control-groups.png new file mode 100644 index 0000000000..7e1a264b42 Binary files /dev/null and b/apps/docs/public/static/enterprise/access-control-groups.png differ diff --git a/apps/docs/public/static/enterprise/access-control-model-providers.png b/apps/docs/public/static/enterprise/access-control-model-providers.png new file mode 100644 index 0000000000..5779702171 Binary files /dev/null and b/apps/docs/public/static/enterprise/access-control-model-providers.png differ diff --git a/apps/docs/public/static/enterprise/access-control-platform.png b/apps/docs/public/static/enterprise/access-control-platform.png new file mode 100644 index 0000000000..2eb5cf5304 Binary files /dev/null and b/apps/docs/public/static/enterprise/access-control-platform.png differ diff --git a/apps/docs/public/static/enterprise/audit-logs.png b/apps/docs/public/static/enterprise/audit-logs.png new file mode 100644 index 0000000000..5838898f6f Binary files /dev/null and b/apps/docs/public/static/enterprise/audit-logs.png differ diff --git a/apps/docs/public/static/enterprise/data-retention.png b/apps/docs/public/static/enterprise/data-retention.png new file mode 100644 index 0000000000..1b8d559e76 Binary files /dev/null and b/apps/docs/public/static/enterprise/data-retention.png differ diff --git a/apps/docs/public/static/enterprise/sso-form.png b/apps/docs/public/static/enterprise/sso-form.png index 78c54bd9f7..f44f5f80d1 100644 Binary files a/apps/docs/public/static/enterprise/sso-form.png and b/apps/docs/public/static/enterprise/sso-form.png differ diff --git a/apps/docs/public/static/enterprise/whitelabeling.png b/apps/docs/public/static/enterprise/whitelabeling.png new file mode 100644 index 0000000000..f643dd2f98 Binary files /dev/null and b/apps/docs/public/static/enterprise/whitelabeling.png differ diff --git a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts index d2308f16a1..bd388cb2f4 100644 --- a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts +++ b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts @@ -10,6 +10,7 @@ import { CLEANUP_CONFIG } from '@/lib/billing/cleanup-dispatcher' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { isEnterprisePlan } from '@/lib/billing/core/subscription' import { getPlanType, type PlanCategory } from '@/lib/billing/plan-helpers' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' @@ -79,7 +80,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ const plan = await resolveWorkspacePlan(ws.billedAccountUserId) const defaults = getPlanDefaults(plan) - const isEnterpriseWorkspace = plan === 'enterprise' + const isEnterpriseWorkspace = !isBillingEnabled || plan === 'enterprise' return NextResponse.json({ success: true, @@ -135,12 +136,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) } - const hasEnterprise = await isEnterprisePlan(billedAccountUserId) - if (!hasEnterprise) { - return NextResponse.json( - { error: 'Data Retention configuration is available on Enterprise plans only' }, - { status: 403 } - ) + if (isBillingEnabled) { + const hasEnterprise = await isEnterprisePlan(billedAccountUserId) + if (!hasEnterprise) { + return NextResponse.json( + { error: 'Data Retention configuration is available on Enterprise plans only' }, + { status: 403 } + ) + } } const body = await request.json() diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index e9743b1fa2..20f6e9d354 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -29,6 +29,7 @@ import { isCredentialSetsEnabled, } from '@/app/workspace/[workspaceId]/settings/navigation' import { AuditLogsSkeleton } from '@/ee/audit-logs/components/audit-logs-skeleton' +import { DataRetentionSkeleton } from '@/ee/data-retention/components/data-retention-skeleton' /** * Generic skeleton fallback for sections without a dedicated skeleton. @@ -174,7 +175,7 @@ const DataRetentionSettings = dynamic( import('@/ee/data-retention/components/data-retention-settings').then( (m) => m.DataRetentionSettings ), - { loading: () => } + { loading: () => } ) const WhitelabelingSettings = dynamic( () => @@ -215,7 +216,12 @@ export function SettingsPage({ section }: SettingsPageProps) { }, [effectiveSection, sessionLoading, posthog]) return ( -
+

{label}

{effectiveSection === 'general' && } {effectiveSection === 'integrations' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx index c8b2e49f7b..64bb68da2a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx @@ -4,8 +4,14 @@ import { useMemo, useState } from 'react' import { formatDate } from '@sim/utils/formatting' import { Folder, Search } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' -import { Button, Combobox, SModalTabs, SModalTabsList, SModalTabsTrigger } from '@/components/emcn' -import { Input } from '@/components/ui' +import { + Button, + Combobox, + Input, + SModalTabs, + SModalTabsList, + SModalTabsTrigger, +} from '@/components/emcn' import { workflowBorderColor } from '@/lib/workspaces/colors' import { RESOURCE_REGISTRY } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' import type { MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types' @@ -356,7 +362,7 @@ export function RecentlyDeleted() {
{error ? (
-

+

{error instanceof Error ? error.message : 'Failed to load deleted items'}

diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index 28c3e11d0c..f2b69552a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -79,6 +79,7 @@ const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABL const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED')) const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED')) const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED')) +const isDataRetentionEnabled = isTruthy(getEnv('NEXT_PUBLIC_DATA_RETENTION_ENABLED')) export const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) export { isCredentialSetsEnabled } @@ -187,6 +188,7 @@ export const allNavigationItems: NavigationItem[] = [ section: 'enterprise', requiresHosted: true, requiresEnterprise: true, + selfHostedOverride: isDataRetentionEnabled, showWhenLocked: true, }, { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx index f9ccbf6ed2..99fe359c79 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -58,14 +58,16 @@ export function SlackSetupWizard({ onClick={() => setOpen(true)} disabled={launcherDisabled} className={cn( - 'flex w-full items-center justify-between rounded-md border border-[var(--border-muted)] bg-[var(--surface-1)] px-3 py-2 text-left transition-colors', + 'flex w-full items-center justify-between rounded-sm border border-[var(--border-1)] bg-[var(--surface-5)] px-2 py-1.5 text-left transition-colors', launcherDisabled ? 'cursor-not-allowed opacity-70' - : 'cursor-pointer hover-hover:bg-[var(--surface-hover)]' + : 'cursor-pointer hover-hover:bg-[var(--surface-6)]' )} > - Setup Slack App - + + Setup Slack App + + {canCopy && (copied ? ( - + ) : ( - + ))} @@ -432,15 +434,11 @@ function StepDone({ hasSigningSecret, hasBotToken }: StepDoneProps) { Your Slack app is set up. Save the workflow and Slack will verify the webhook URL automatically.

- - - - - - - - Click Done and save this workflow. - +
+ + +
+

Click Done and save this workflow.

) } diff --git a/apps/sim/components/emcn/components/wizard/wizard.tsx b/apps/sim/components/emcn/components/wizard/wizard.tsx index ee68ee3742..ed446fb6be 100644 --- a/apps/sim/components/emcn/components/wizard/wizard.tsx +++ b/apps/sim/components/emcn/components/wizard/wizard.tsx @@ -147,25 +147,23 @@ const WizardRoot: React.FC = ({ return ( - -
- {activeStep?.props.title} - - Step {clamped + 1} of {total} - + {activeStep?.props.title} + +
+
+ {steps.map((_step, i) => ( +
+ ))}
- - -
- {steps.map((_step, i) => ( -
- ))} + + {clamped + 1}/{total} +
{activeStep} diff --git a/apps/sim/ee/components/setting-row.tsx b/apps/sim/ee/components/setting-row.tsx new file mode 100644 index 0000000000..d237e7d14f --- /dev/null +++ b/apps/sim/ee/components/setting-row.tsx @@ -0,0 +1,17 @@ +import { Label } from '@/components/emcn' + +interface SettingRowProps { + label: string + description?: string + children: React.ReactNode +} + +export function SettingRow({ label, description, children }: SettingRowProps) { + return ( +
+ + {description &&

{description}

} + {children} +
+ ) +} diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index b3fb991864..896140d36e 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -1,19 +1,14 @@ 'use client' -import { useCallback, useState } from 'react' +import { useEffect, useState } from 'react' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { Loader2 } from 'lucide-react' import { useParams } from 'next/navigation' -import { Button, Label } from '@/components/emcn' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' +import { Button, Combobox, toast } from '@/components/emcn' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { SettingRow } from '@/ee/components/setting-row' +import { DataRetentionSkeleton } from '@/ee/data-retention/components/data-retention-skeleton' import { useUpdateWorkspaceRetention, useWorkspaceRetention, @@ -45,26 +40,6 @@ function daysToHours(days: string): number | null { return Number(days) * 24 } -interface SettingRowProps { - label: string - description?: string - children: React.ReactNode -} - -function SettingRow({ label, description, children }: SettingRowProps) { - return ( -
- - {description &&

{description}

} - {children} -
- ) -} - -function SectionTitle({ children }: { children: React.ReactNode }) { - return

{children}

-} - interface RetentionSelectProps { value: string onChange: (value: string) => void @@ -73,22 +48,22 @@ interface RetentionSelectProps { function RetentionSelect({ value, onChange }: RetentionSelectProps) { const standard = DAY_OPTIONS.find((o) => o.value === value) const options = standard - ? DAY_OPTIONS - : [...DAY_OPTIONS, { value, label: `${value} days (custom)` } as const] + ? DAY_OPTIONS.map((o) => ({ value: o.value, label: o.label })) + : [ + ...DAY_OPTIONS.map((o) => ({ value: o.value, label: o.label })), + { value, label: `${value} days (custom)` }, + ] return ( - +
+ +
) } @@ -103,22 +78,31 @@ export function DataRetentionSettings() { const [logDays, setLogDays] = useState('') const [softDeleteDays, setSoftDeleteDays] = useState('') const [taskCleanupDays, setTaskCleanupDays] = useState('') + const [savedLogDays, setSavedLogDays] = useState('') + const [savedSoftDeleteDays, setSavedSoftDeleteDays] = useState('') + const [savedTaskCleanupDays, setSavedTaskCleanupDays] = useState('') const [formInitialized, setFormInitialized] = useState(false) - const [saveError, setSaveError] = useState(null) - const [saveSuccess, setSaveSuccess] = useState(false) - - if (data && !formInitialized) { - setLogDays(hoursToDisplayDays(data.effective.logRetentionHours)) - setSoftDeleteDays(hoursToDisplayDays(data.effective.softDeleteRetentionHours)) - setTaskCleanupDays(hoursToDisplayDays(data.effective.taskCleanupHours)) + useEffect(() => { + if (!data || formInitialized) return + const log = hoursToDisplayDays(data.effective.logRetentionHours) + const soft = hoursToDisplayDays(data.effective.softDeleteRetentionHours) + const task = hoursToDisplayDays(data.effective.taskCleanupHours) + setLogDays(log) + setSoftDeleteDays(soft) + setTaskCleanupDays(task) + setSavedLogDays(log) + setSavedSoftDeleteDays(soft) + setSavedTaskCleanupDays(task) setFormInitialized(true) - } + }, [data, formInitialized]) - const handleSave = useCallback(async () => { - setSaveError(null) - setSaveSuccess(false) + const hasChanges = + logDays !== savedLogDays || + softDeleteDays !== savedSoftDeleteDays || + taskCleanupDays !== savedTaskCleanupDays + async function handleSave() { try { await updateMutation.mutateAsync({ workspaceId, @@ -128,27 +112,19 @@ export function DataRetentionSettings() { taskCleanupHours: daysToHours(taskCleanupDays), }, }) - setSaveSuccess(true) - setTimeout(() => setSaveSuccess(false), 3000) + setSavedLogDays(logDays) + setSavedSoftDeleteDays(softDeleteDays) + setSavedTaskCleanupDays(taskCleanupDays) + toast.success('Data retention settings saved.') } catch (error) { - logger.error('Failed to save data retention settings', { error }) - setSaveError(toError(error).message) + const msg = toError(error).message + logger.error('Failed to save data retention settings', { error: msg }) + toast.error(msg) } - }, [workspaceId, logDays, softDeleteDays, taskCleanupDays]) - - if (isLoading) { - return ( -
- {[...Array(3)].map((_, i) => ( -
-
-
-
- ))} -
- ) } + if (isLoading) return + if (!data) { return (
@@ -157,7 +133,7 @@ export function DataRetentionSettings() { ) } - if (!data.isEnterprise) { + if (isBillingEnabled && !data.isEnterprise) { return (
Data retention is available on Enterprise plans only. @@ -176,7 +152,9 @@ export function DataRetentionSettings() { return (
- Retention Periods +

+ Retention Periods +

-
- - {saveSuccess && ( - Settings saved successfully. - )} - {saveError && {saveError}}
) diff --git a/apps/sim/ee/data-retention/components/data-retention-skeleton.tsx b/apps/sim/ee/data-retention/components/data-retention-skeleton.tsx new file mode 100644 index 0000000000..d908e6a1d6 --- /dev/null +++ b/apps/sim/ee/data-retention/components/data-retention-skeleton.tsx @@ -0,0 +1,23 @@ +import { Skeleton } from '@/components/emcn' + +export function DataRetentionSkeleton() { + return ( +
+
+ +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ + + +
+ ))} +
+
+
+ +
+
+ ) +} diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index deb777bf41..c42b210bea 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -1,11 +1,12 @@ 'use client' -import { useCallback, useState } from 'react' +import { useEffect, useState } from 'react' import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { Loader2, X } from 'lucide-react' import Image from 'next/image' import { useParams } from 'next/navigation' -import { Button, Input, Label } from '@/components/emcn' +import { Button, Input, Label, Skeleton, toast } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client/utils' import { HEX_COLOR_REGEX } from '@/lib/branding' @@ -13,6 +14,7 @@ import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { cn } from '@/lib/core/utils/cn' import { getUserRole } from '@/lib/workspaces/organization/utils' import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload' +import { SettingRow } from '@/ee/components/setting-row' import { useUpdateWhitelabelSettings, useWhitelabelSettings, @@ -32,39 +34,28 @@ interface DropZoneProps { function DropZone({ onDrop, children, className }: DropZoneProps) { const [isDragging, setIsDragging] = useState(false) - const handleDragOver = useCallback((e: React.DragEvent) => { - if (e.dataTransfer.types.includes('Files')) { - e.preventDefault() - setIsDragging(true) - } - }, []) - - const handleDragLeave = useCallback((e: React.DragEvent) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setIsDragging(false) - } - }, []) - - const handleDrop = useCallback( - (e: React.DragEvent) => { - setIsDragging(false) - onDrop(e) - }, - [onDrop] - ) - return (
{ + if (e.dataTransfer.types.includes('Files')) { + e.preventDefault() + setIsDragging(true) + } + }} + onDragLeave={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setIsDragging(false) + } + }} + onDrop={(e) => { + setIsDragging(false) + onDrop(e) + }} > {children} {isDragging && ( -
- Drop image -
+
)}
) @@ -80,22 +71,6 @@ interface ColorInputProps { function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorInputProps) { const isValidHex = !value || HEX_COLOR_REGEX.test(value) - const handleChange = useCallback( - (e: React.ChangeEvent) => { - let v = e.target.value.trim() - if (v && !v.startsWith('#')) { - v = `#${v}` - } - v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') - onChange(v.slice(0, 7)) - }, - [onChange] - ) - - const handleFocus = useCallback((e: React.FocusEvent) => { - e.target.select() - }, []) - return (
@@ -109,39 +84,32 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
{ + let v = e.target.value.trim() + if (v && !v.startsWith('#')) { + v = `#${v}` + } + v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') + onChange(v.slice(0, 7)) + }} + onFocus={(e) => e.target.select()} placeholder={placeholder} className={cn( 'h-[36px] font-mono text-[13px]', - !isValidHex && 'border-red-500 focus-visible:ring-red-500' + !isValidHex && 'border-[var(--text-error)] focus-visible:ring-[var(--text-error)]' )} maxLength={7} />
{!isValidHex && ( -

Must be a valid hex color (e.g. #701ffc)

+

+ Must be a valid hex color (e.g. #701ffc) +

)}
) } -interface SettingRowProps { - label: string - description?: string - children: React.ReactNode -} - -function SettingRow({ label, description, children }: SettingRowProps) { - return ( -
- - {description &&

{description}

} - {children} -
- ) -} - function SectionTitle({ children }: { children: React.ReactNode }) { return

{children}

} @@ -176,29 +144,60 @@ export function WhitelabelingSettings() { const [logoUrl, setLogoUrl] = useState(null) const [wordmarkUrl, setWordmarkUrl] = useState(null) const [formInitialized, setFormInitialized] = useState(false) - - const [saveError, setSaveError] = useState(null) - const [saveSuccess, setSaveSuccess] = useState(false) - - if (savedSettings && !formInitialized) { - setBrandName(savedSettings.brandName ?? '') - setPrimaryColor(savedSettings.primaryColor ?? '') - setPrimaryHoverColor(savedSettings.primaryHoverColor ?? '') - setAccentColor(savedSettings.accentColor ?? '') - setAccentHoverColor(savedSettings.accentHoverColor ?? '') - setSupportEmail(savedSettings.supportEmail ?? '') - setDocumentationUrl(savedSettings.documentationUrl ?? '') - setTermsUrl(savedSettings.termsUrl ?? '') - setPrivacyUrl(savedSettings.privacyUrl ?? '') - setLogoUrl(savedSettings.logoUrl ?? null) - setWordmarkUrl(savedSettings.wordmarkUrl ?? null) + const [savedBrandName, setSavedBrandName] = useState('') + const [savedPrimaryColor, setSavedPrimaryColor] = useState('') + const [savedPrimaryHoverColor, setSavedPrimaryHoverColor] = useState('') + const [savedAccentColor, setSavedAccentColor] = useState('') + const [savedAccentHoverColor, setSavedAccentHoverColor] = useState('') + const [savedSupportEmail, setSavedSupportEmail] = useState('') + const [savedDocumentationUrl, setSavedDocumentationUrl] = useState('') + const [savedTermsUrl, setSavedTermsUrl] = useState('') + const [savedPrivacyUrl, setSavedPrivacyUrl] = useState('') + const [savedLogoUrl, setSavedLogoUrl] = useState(null) + const [savedWordmarkUrl, setSavedWordmarkUrl] = useState(null) + + useEffect(() => { + if (!savedSettings || formInitialized) return + const brand = savedSettings.brandName ?? '' + const primary = savedSettings.primaryColor ?? '' + const primaryHover = savedSettings.primaryHoverColor ?? '' + const accent = savedSettings.accentColor ?? '' + const accentHover = savedSettings.accentHoverColor ?? '' + const support = savedSettings.supportEmail ?? '' + const docs = savedSettings.documentationUrl ?? '' + const terms = savedSettings.termsUrl ?? '' + const privacy = savedSettings.privacyUrl ?? '' + const logo = savedSettings.logoUrl ?? null + const wordmark = savedSettings.wordmarkUrl ?? null + setBrandName(brand) + setPrimaryColor(primary) + setPrimaryHoverColor(primaryHover) + setAccentColor(accent) + setAccentHoverColor(accentHover) + setSupportEmail(support) + setDocumentationUrl(docs) + setTermsUrl(terms) + setPrivacyUrl(privacy) + setLogoUrl(logo) + setWordmarkUrl(wordmark) + setSavedBrandName(brand) + setSavedPrimaryColor(primary) + setSavedPrimaryHoverColor(primaryHover) + setSavedAccentColor(accent) + setSavedAccentHoverColor(accentHover) + setSavedSupportEmail(support) + setSavedDocumentationUrl(docs) + setSavedTermsUrl(terms) + setSavedPrivacyUrl(privacy) + setSavedLogoUrl(logo) + setSavedWordmarkUrl(wordmark) setFormInitialized(true) - } + }, [savedSettings, formInitialized]) const logoUpload = useProfilePictureUpload({ currentImage: logoUrl, onUpload: (url) => setLogoUrl(url), - onError: (error) => setSaveError(error), + onError: (error) => toast.error(error), context: 'workspace-logos', workspaceId: params.workspaceId, }) @@ -206,17 +205,28 @@ export function WhitelabelingSettings() { const wordmarkUpload = useProfilePictureUpload({ currentImage: wordmarkUrl, onUpload: (url) => setWordmarkUrl(url), - onError: (error) => setSaveError(error), + onError: (error) => toast.error(error), context: 'workspace-logos', workspaceId: params.workspaceId, }) - const handleSave = useCallback(async () => { + const hasChanges = + formInitialized && + (brandName !== savedBrandName || + primaryColor !== savedPrimaryColor || + primaryHoverColor !== savedPrimaryHoverColor || + accentColor !== savedAccentColor || + accentHoverColor !== savedAccentHoverColor || + supportEmail !== savedSupportEmail || + documentationUrl !== savedDocumentationUrl || + termsUrl !== savedTermsUrl || + privacyUrl !== savedPrivacyUrl || + (logoUpload.previewUrl || null) !== savedLogoUrl || + (wordmarkUpload.previewUrl || null) !== savedWordmarkUrl) + + async function handleSave() { if (!orgId) return - setSaveError(null) - setSaveSuccess(false) - const colorFields: Array<[string, string]> = [ ['Primary color', primaryColor], ['Primary hover color', primaryHoverColor], @@ -226,7 +236,7 @@ export function WhitelabelingSettings() { for (const [fieldName, value] of colorFields) { if (value && !HEX_COLOR_REGEX.test(value)) { - setSaveError(`${fieldName} must be a valid hex color (e.g. #701ffc)`) + toast.error(`${fieldName} must be a valid hex color (e.g. #701ffc)`) return } } @@ -247,26 +257,23 @@ export function WhitelabelingSettings() { try { await updateSettings.mutateAsync({ orgId, settings }) - setSaveSuccess(true) - setTimeout(() => setSaveSuccess(false), 3000) + setSavedBrandName(brandName) + setSavedPrimaryColor(primaryColor) + setSavedPrimaryHoverColor(primaryHoverColor) + setSavedAccentColor(accentColor) + setSavedAccentHoverColor(accentHoverColor) + setSavedSupportEmail(supportEmail) + setSavedDocumentationUrl(documentationUrl) + setSavedTermsUrl(termsUrl) + setSavedPrivacyUrl(privacyUrl) + setSavedLogoUrl(logoUpload.previewUrl || null) + setSavedWordmarkUrl(wordmarkUpload.previewUrl || null) + toast.success('Whitelabeling settings saved.') } catch (error) { logger.error('Failed to save whitelabel settings', { error }) - setSaveError(error instanceof Error ? error.message : 'Failed to save settings') + toast.error(toError(error).message) } - }, [ - orgId, - brandName, - logoUpload.previewUrl, - wordmarkUpload.previewUrl, - primaryColor, - primaryHoverColor, - accentColor, - accentHoverColor, - supportEmail, - documentationUrl, - termsUrl, - privacyUrl, - ]) + } if (isBillingEnabled) { if (!activeOrganization) { @@ -297,10 +304,10 @@ export function WhitelabelingSettings() { if (isLoading) { return (
- {[...Array(3)].map((_, i) => ( + {Array.from({ length: 3 }).map((_, i) => (
-
-
+ +
))}
@@ -319,27 +326,29 @@ export function WhitelabelingSettings() { label='Logo' description='Shown in the collapsed sidebar. Square image recommended (PNG, JPEG, or SVG, max 5MB).' > - - +
+ + +
- - +
+ + +
@@ -515,25 +526,14 @@ export function WhitelabelingSettings() {
-
+
- {saveSuccess && ( - Settings saved successfully. - )} - {saveError && {saveError}}
) diff --git a/apps/sim/lib/billing/cleanup-dispatcher.ts b/apps/sim/lib/billing/cleanup-dispatcher.ts index dc600169be..5f0e17424c 100644 --- a/apps/sim/lib/billing/cleanup-dispatcher.ts +++ b/apps/sim/lib/billing/cleanup-dispatcher.ts @@ -44,11 +44,11 @@ const DAY = 24 export const CLEANUP_CONFIG = { 'cleanup-logs': { column: 'logRetentionHours', - defaults: { free: 7 * DAY, pro: null, team: null, enterprise: null }, + defaults: { free: 30 * DAY, pro: null, team: null, enterprise: null }, }, 'cleanup-soft-deletes': { column: 'softDeleteRetentionHours', - defaults: { free: 7 * DAY, pro: 30 * DAY, team: 30 * DAY, enterprise: null }, + defaults: { free: 30 * DAY, pro: 90 * DAY, team: 90 * DAY, enterprise: null }, }, 'cleanup-tasks': { column: 'taskCleanupHours', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 7d8de4a78d..ea14dcad18 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -338,6 +338,7 @@ export const env = createEnv({ // Enterprise Feature Overrides - for self-hosted deployments WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements) AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements) + DATA_RETENTION_ENABLED: z.boolean().optional(), // Enable data retention settings on self-hosted (bypasses hosted requirements) // Organizations - for self-hosted deployments ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) @@ -432,6 +433,7 @@ export const env = createEnv({ NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted NEXT_PUBLIC_WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements) + NEXT_PUBLIC_DATA_RETENTION_ENABLED: z.boolean().optional(), // Enable data retention settings on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) NEXT_PUBLIC_DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments) NEXT_PUBLIC_DISABLE_PUBLIC_API: z.boolean().optional(), // Disable public API access UI toggle globally @@ -468,6 +470,7 @@ export const env = createEnv({ NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED, NEXT_PUBLIC_WHITELABELING_ENABLED: process.env.NEXT_PUBLIC_WHITELABELING_ENABLED, NEXT_PUBLIC_AUDIT_LOGS_ENABLED: process.env.NEXT_PUBLIC_AUDIT_LOGS_ENABLED, + NEXT_PUBLIC_DATA_RETENTION_ENABLED: process.env.NEXT_PUBLIC_DATA_RETENTION_ENABLED, NEXT_PUBLIC_ORGANIZATIONS_ENABLED: process.env.NEXT_PUBLIC_ORGANIZATIONS_ENABLED, NEXT_PUBLIC_DISABLE_INVITATIONS: process.env.NEXT_PUBLIC_DISABLE_INVITATIONS, NEXT_PUBLIC_DISABLE_PUBLIC_API: process.env.NEXT_PUBLIC_DISABLE_PUBLIC_API, diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 8e3820c778..c593c2b3ed 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -129,6 +129,12 @@ export const isWhitelabelingEnabled = isTruthy(env.WHITELABELING_ENABLED) */ export const isAuditLogsEnabled = isTruthy(env.AUDIT_LOGS_ENABLED) +/** + * Is data retention enabled via env var override + * This bypasses hosted requirements for self-hosted deployments + */ +export const isDataRetentionEnabled = isTruthy(env.DATA_RETENTION_ENABLED) + /** * Is E2B enabled for remote code execution */ diff --git a/packages/testing/src/mocks/feature-flags.mock.ts b/packages/testing/src/mocks/feature-flags.mock.ts index 9a108d3cde..813f109246 100644 --- a/packages/testing/src/mocks/feature-flags.mock.ts +++ b/packages/testing/src/mocks/feature-flags.mock.ts @@ -28,6 +28,7 @@ export const featureFlagsMock = { isInboxEnabled: false, isWhitelabelingEnabled: false, isAuditLogsEnabled: false, + isDataRetentionEnabled: false, isE2bEnabled: false, isOllamaConfigured: false, isAzureConfigured: false,