diff --git a/apps/sim/.env.example b/apps/sim/.env.example
index 54451772873..c7313cbaa07 100644
--- a/apps/sim/.env.example
+++ b/apps/sim/.env.example
@@ -17,6 +17,7 @@ BETTER_AUTH_URL=http://localhost:3000
# NextJS (Required)
NEXT_PUBLIC_APP_URL=http://localhost:3000
+# NEXT_PUBLIC_STATUS_NOTICE_PREVIEW=true # Force the sidebar service-status notice into its critical preview state for testing
# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL
# TRUSTED_ORIGINS=https://www.example.com,https://app.example.com # Optional: comma-separated additional public origins to trust for auth (apex+www, alias domains). Merged into Better Auth trustedOrigins.
# AUTH_TRUSTED_PROXIES=10.0.0.0/24,192.0.2.10 # Optional: reverse-proxy IPs/CIDRs in front of the app. Better Auth walks x-forwarded-for right to left, skips these hops, and uses the first untrusted address as the client IP (prevents forwarded-header spoofing). Use your proxies' actual addresses, not broad private ranges that also cover clients.
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts
index e1b2ba50b3a..69b503f2cc4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts
@@ -14,5 +14,6 @@ export { SidebarFooter } from './sidebar-footer'
export type { SidebarNavItemData } from './sidebar-nav-chip'
export { SidebarNavChip } from './sidebar-nav-chip'
export { SidebarSection } from './sidebar-section'
+export { StatusNotice } from './status-notice'
export { WorkflowList } from './workflow-list'
export { WorkspaceHeader } from './workspace-header'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/index.ts
new file mode 100644
index 00000000000..56bb122f4b6
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/index.ts
@@ -0,0 +1 @@
+export { StatusNotice } from './status-notice'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice.test.tsx
new file mode 100644
index 00000000000..2a63210da6e
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice.test.tsx
@@ -0,0 +1,106 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const { mockUseStatusPage } = vi.hoisted(() => ({
+ mockUseStatusPage: vi.fn(),
+}))
+
+vi.mock('@/hooks/queries/status-page', () => ({
+ useStatusPage: mockUseStatusPage,
+}))
+
+import { StatusNotice } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice'
+
+let container: HTMLDivElement
+let root: Root
+
+beforeEach(() => {
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ vi.clearAllMocks()
+ mockUseStatusPage.mockReturnValue({ data: undefined, error: null })
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+
+afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+ vi.restoreAllMocks()
+})
+
+function render() {
+ act(() => root.render())
+}
+
+describe('StatusNotice', () => {
+ it('shows the local status alert without fetching live status in preview mode', () => {
+ act(() => root.render())
+
+ const notice = container.querySelector('[role="alert"]')
+ expect(notice?.textContent).toContain('Sim is having issues')
+ expect(notice?.className).toContain('bg-[var(--terminal-status-error-bg)]')
+ expect(notice?.className).toContain('border-[var(--terminal-status-error-border)]')
+ expect(container.querySelector('svg')?.classList.contains('text-[var(--text-icon)]')).toBe(true)
+ expect(mockUseStatusPage).toHaveBeenCalledWith({ enabled: false })
+ })
+
+ it('stays hidden while loading and for operational or minor incidents', () => {
+ render()
+ expect(container.textContent).toBe('')
+
+ mockUseStatusPage.mockReturnValue({
+ data: { status: { description: 'All Systems Operational', indicator: 'none' } },
+ error: null,
+ })
+ render()
+
+ expect(container.textContent).toBe('')
+
+ mockUseStatusPage.mockReturnValue({
+ data: { status: { description: 'Minor Service Outage', indicator: 'minor' } },
+ error: null,
+ })
+ render()
+
+ expect(container.textContent).toBe('')
+ })
+
+ it('shows the notice for a major incident and opens the status page', () => {
+ mockUseStatusPage.mockReturnValue({
+ data: { status: { description: 'Major Service Outage', indicator: 'major' } },
+ error: null,
+ })
+
+ render()
+
+ const notice = container.querySelector('[role="alert"]')
+ const action = container.querySelector('a')
+ expect(notice?.className).toContain('border-[var(--terminal-status-error-border)]')
+ expect(notice?.className).toContain('shadow-[var(--shadow-overlay)]')
+ expect(notice?.className).toContain(
+ '[--surface-hover:color-mix(in_srgb,var(--text-error)_8%,transparent)]'
+ )
+ expect(action?.textContent).toContain('View status')
+ expect(action?.className).not.toContain('bg-[var(--text-error)]')
+ expect(action?.getAttribute('href')).toBe('https://status.sim.ai')
+ expect(action?.getAttribute('target')).toBe('_blank')
+ expect(action?.getAttribute('rel')).toBe('noopener noreferrer')
+ })
+
+ it('stays hidden when the optional status query fails', () => {
+ mockUseStatusPage.mockReturnValue({
+ data: undefined,
+ error: new Error('status unavailable'),
+ })
+
+ render()
+
+ expect(container.textContent).toBe('')
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice.tsx
new file mode 100644
index 00000000000..5e8bfa3e72c
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/status-notice/status-notice.tsx
@@ -0,0 +1,51 @@
+'use client'
+
+import { ChipLink } from '@sim/emcn'
+import { CircleAlert } from '@sim/emcn/icons'
+import { STATUS_PAGE_URL } from '@/lib/status-page'
+import { useStatusPage } from '@/hooks/queries/status-page'
+
+const PREVIEW_STATUS = {
+ description: 'Major Service Outage',
+ indicator: 'critical',
+} as const
+
+interface StatusNoticeProps {
+ preview?: boolean
+}
+
+function StatusAlert() {
+ return (
+
+
+
+
Sim is having issues
+
+
+ View status
+
+
+ )
+}
+
+export function StatusNotice({ preview = false }: StatusNoticeProps) {
+ const { data } = useStatusPage({ enabled: !preview })
+
+ const status = preview ? PREVIEW_STATUS : data?.status
+
+ if (status?.indicator !== 'major' && status?.indicator !== 'critical') {
+ return null
+ }
+
+ return
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index 47fc7095c30..cbec949a489 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -38,7 +38,7 @@ import { usePostHog } from 'posthog-js/react'
import { useSession } from '@/lib/auth/auth-client'
import { focusVisibleBrowserOmnibox } from '@/lib/browser-agent/renderer-shortcuts'
import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
-import { isChatEnabled } from '@/lib/core/config/env-flags'
+import { isChatEnabled, isHosted, isStatusNoticePreviewEnabled } from '@/lib/core/config/env-flags'
import { isMacPlatform } from '@/lib/core/utils/platform'
import { buildFolderTree, getFolderPathNames } from '@/lib/folders/tree'
import { captureEvent } from '@/lib/posthog/client'
@@ -62,6 +62,7 @@ import {
SidebarNavChip,
type SidebarNavItemData,
SidebarSection,
+ StatusNotice,
TablesRailFlyout,
WorkflowList,
WorkspaceHeader,
@@ -1479,7 +1480,7 @@ export const Sidebar = memo(function Sidebar({
ref={isCollapsed ? undefined : scrollContainerRef}
className={cn(
SIDEBAR_DIVIDER_PAD_BELOW_CLASS,
- 'flex flex-1 flex-col overflow-y-auto overflow-x-hidden border-t transition-colors duration-150',
+ 'flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden border-t transition-colors duration-150',
!hasOverflowTop && 'border-transparent'
)}
>
@@ -1807,6 +1808,12 @@ export const Sidebar = memo(function Sidebar({
+ {(isHosted || isStatusNoticePreviewEnabled) && !isCollapsed ? (
+
+
+
+ ) : null}
+
[...statusPageKeys.all, 'summary'] as const,
+ summary: () => [...statusPageKeys.summaries(), 'sim'] as const,
+}
+
+interface UseStatusPageOptions {
+ enabled?: boolean
+}
+
+/** Polls Sim's public status while a hosted workspace is open. */
+export function useStatusPage({ enabled = true }: UseStatusPageOptions = {}) {
+ return useQuery({
+ queryKey: statusPageKeys.summary(),
+ queryFn: ({ signal }) => fetchStatusPageSummary(signal),
+ enabled,
+ staleTime: STATUS_PAGE_STALE_TIME,
+ refetchInterval: STATUS_PAGE_POLL_INTERVAL,
+ refetchOnWindowFocus: true,
+ retry: false,
+ })
+}
diff --git a/apps/sim/lib/core/config/env-flags.ts b/apps/sim/lib/core/config/env-flags.ts
index 135607925f1..95aa7b13720 100644
--- a/apps/sim/lib/core/config/env-flags.ts
+++ b/apps/sim/lib/core/config/env-flags.ts
@@ -85,6 +85,13 @@ export const isCopilotBillingProtocolRequired = isTruthy(env.COPILOT_BILLING_PRO
*/
export const isChatEnabled = !isTruthy(getEnv('NEXT_PUBLIC_CHAT_DISABLED'))
+/**
+ * Forces the sidebar service-status notice into its critical preview state.
+ * This is an explicit testing override; when unset, hosted deployments read
+ * the live status page and other deployments do not mount the notice.
+ */
+export const isStatusNoticePreviewEnabled = isTruthy(getEnv('NEXT_PUBLIC_STATUS_NOTICE_PREVIEW'))
+
/**
* Holds tools the catalog marks `requiresApproval` — shell commands, workflow
* runs, sandboxed code, deployments, integration calls — behind an explicit
diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts
index 2dbf8ab149f..d41d35f4a72 100644
--- a/apps/sim/lib/core/config/env.ts
+++ b/apps/sim/lib/core/config/env.ts
@@ -710,6 +710,7 @@ export const env = createEnv({
NEXT_PUBLIC_DISABLE_PUBLIC_API: z.boolean().optional(), // Disable public API access UI toggle globally
NEXT_PUBLIC_INBOX_ENABLED: z.boolean().optional(), // Enable inbox (Sim Mailer) on self-hosted
NEXT_PUBLIC_CHAT_DISABLED: z.boolean().optional(), // Hide the Chat module (Chat is shown when unset)
+ NEXT_PUBLIC_STATUS_NOTICE_PREVIEW: z.boolean().optional(), // Force the sidebar service-status notice into its critical preview state
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: z.boolean().optional().default(true), // Control visibility of email/password login forms
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().min(1).optional(), // Cloudflare Turnstile site key for captcha widget
},
@@ -754,6 +755,7 @@ export const env = createEnv({
NEXT_PUBLIC_DISABLE_PUBLIC_API: process.env.NEXT_PUBLIC_DISABLE_PUBLIC_API,
NEXT_PUBLIC_INBOX_ENABLED: process.env.NEXT_PUBLIC_INBOX_ENABLED,
NEXT_PUBLIC_CHAT_DISABLED: process.env.NEXT_PUBLIC_CHAT_DISABLED,
+ NEXT_PUBLIC_STATUS_NOTICE_PREVIEW: process.env.NEXT_PUBLIC_STATUS_NOTICE_PREVIEW,
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED,
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
diff --git a/apps/sim/lib/core/security/csp.test.ts b/apps/sim/lib/core/security/csp.test.ts
index 1cf199dc08c..9dcf30850e0 100644
--- a/apps/sim/lib/core/security/csp.test.ts
+++ b/apps/sim/lib/core/security/csp.test.ts
@@ -272,6 +272,10 @@ describe('buildTimeCSPDirectives', () => {
expect(buildTimeCSPDirectives['font-src']).toContain('https://fonts.gstatic.com')
})
+ it('allows the hosted app to read the Sim status page', () => {
+ expect(getMainCSPPolicy()).toMatch(/connect-src[^;]*https:\/\/status\.sim\.ai/)
+ })
+
it('should allow data: and blob: for images', () => {
expect(buildTimeCSPDirectives['img-src']).toContain('data:')
expect(buildTimeCSPDirectives['img-src']).toContain('blob:')
diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts
index ca54d1afa9a..ec03a394445 100644
--- a/apps/sim/lib/core/security/csp.ts
+++ b/apps/sim/lib/core/security/csp.ts
@@ -109,6 +109,7 @@ const STATIC_CONNECT_SRC = [
'https://*.supabase.co',
'https://api.github.com',
'https://github.com/*',
+ 'https://status.sim.ai',
'https://challenges.cloudflare.com',
// Cal.com booking embed (landing /demo) — embed XHR/availability calls
'https://app.cal.com',
diff --git a/apps/sim/lib/status-page.test.ts b/apps/sim/lib/status-page.test.ts
new file mode 100644
index 00000000000..02675fd74b5
--- /dev/null
+++ b/apps/sim/lib/status-page.test.ts
@@ -0,0 +1,56 @@
+/**
+ * @vitest-environment node
+ */
+
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { fetchStatusPageSummary } from '@/lib/status-page'
+
+afterEach(() => {
+ vi.unstubAllGlobals()
+})
+
+describe('fetchStatusPageSummary', () => {
+ it('returns a validated public status summary and forwards cancellation', async () => {
+ const signal = new AbortController().signal
+ const fetchMock = vi.fn().mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ page: { name: 'Sim' },
+ status: { description: 'Minor Service Outage', indicator: 'minor' },
+ }),
+ { status: 200 }
+ )
+ )
+ vi.stubGlobal('fetch', fetchMock)
+
+ await expect(fetchStatusPageSummary(signal)).resolves.toEqual({
+ status: { description: 'Minor Service Outage', indicator: 'minor' },
+ })
+ expect(fetchMock).toHaveBeenCalledWith(
+ 'https://status.sim.ai/api/v2/status.json',
+ expect.objectContaining({ signal })
+ )
+ })
+
+ it('throws when the status endpoint fails', async () => {
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 503 })))
+
+ await expect(fetchStatusPageSummary()).rejects.toThrow('Status page request failed with 503')
+ })
+
+ it('throws when the provider returns an unknown indicator', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn().mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ status: { description: 'Unexpected status', indicator: 'unknown' },
+ }),
+ { status: 200 }
+ )
+ )
+ )
+
+ await expect(fetchStatusPageSummary()).rejects.toThrow()
+ })
+})
diff --git a/apps/sim/lib/status-page.ts b/apps/sim/lib/status-page.ts
new file mode 100644
index 00000000000..e2044a4c551
--- /dev/null
+++ b/apps/sim/lib/status-page.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod'
+
+export const STATUS_PAGE_URL = 'https://status.sim.ai' as const
+const STATUS_PAGE_API_URL = `${STATUS_PAGE_URL}/api/v2/status.json` as const
+
+const statusPageIndicatorSchema = z.enum(['none', 'minor', 'major', 'critical'])
+
+const statusPageSummarySchema = z.object({
+ status: z.object({
+ description: z
+ .string()
+ .min(1, 'Status description cannot be empty')
+ .max(200, 'Status description cannot exceed 200 characters'),
+ indicator: statusPageIndicatorSchema,
+ }),
+})
+
+export type StatusPageIndicator = z.output
+export type StatusPageSummary = z.output
+
+/** Loads and validates Sim's public service status. */
+export async function fetchStatusPageSummary(signal?: AbortSignal): Promise {
+ // boundary-raw-fetch: external Incident.io status API, not a same-origin Sim API
+ const response = await fetch(STATUS_PAGE_API_URL, {
+ cache: 'no-store',
+ headers: { Accept: 'application/json' },
+ signal,
+ })
+
+ if (!response.ok) {
+ throw new Error(`Status page request failed with ${response.status}`)
+ }
+
+ return statusPageSummarySchema.parse(await response.json())
+}
diff --git a/packages/testing/src/mocks/env-flags.mock.ts b/packages/testing/src/mocks/env-flags.mock.ts
index 3ede0b82876..404a297fb73 100644
--- a/packages/testing/src/mocks/env-flags.mock.ts
+++ b/packages/testing/src/mocks/env-flags.mock.ts
@@ -15,6 +15,7 @@ export interface EnvFlagsMockState {
isCopilotBillingAttributionV1Enabled: boolean
isCopilotBillingProtocolRequired: boolean
isChatEnabled: boolean
+ isStatusNoticePreviewEnabled: boolean
isCopilotToolPermissionsEnabled: boolean
isBillingEnabled: boolean
isEmailVerificationEnabled: boolean
@@ -63,6 +64,7 @@ const defaultEnvFlagsState: EnvFlagsMockState = {
isCopilotBillingAttributionV1Enabled: false,
isCopilotBillingProtocolRequired: false,
isChatEnabled: true,
+ isStatusNoticePreviewEnabled: false,
isCopilotToolPermissionsEnabled: false,
isBillingEnabled: false,
isEmailVerificationEnabled: false,