feat(whitelabel): add in the ability to whitelabel via envvars (#887)

* feat(whitelabel): add in the ability to whitelabel via envvars

* restore site.webmanifest

* fix(dynamic): remove force-dynamic from routes that don't need it (#888)

* Reinstall dependencies

* Update docs
This commit is contained in:
Waleed Latif
2025-08-05 19:01:12 -07:00
committed by GitHub
parent 1b0d304a87
commit e43e78fb48
26 changed files with 583 additions and 205 deletions

View File

@@ -537,7 +537,7 @@ This visibility system ensures clean user interfaces while maintaining full flex
### Guidelines & Best Practices
- **Code Style:** Follow the project's ESLint and Prettier configurations. Use meaningful variable names and small, focused functions.
- **Code Style:** Follow the project's Biome configurations. Use meaningful variable names and small, focused functions.
- **Documentation:** Clearly document the purpose, inputs, outputs, and any special behavior for your block/tool.
- **Error Handling:** Implement robust error handling and provide user-friendly error messages.
- **Parameter Visibility:** Always specify the appropriate visibility level for each parameter to ensure proper UI behavior and LLM integration.

View File

@@ -50,7 +50,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
## Usage Instructions
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). Upload files directly. Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). You can either provide a URL to a file or upload files directly. Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.

View File

@@ -38,6 +38,7 @@ With Hunter.io, you can:
In Sim, the Hunter.io integration enables your agents to programmatically search for and verify email addresses, discover companies, and enrich contact data using Hunter.ios API. This allows you to automate lead generation, contact enrichment, and email verification directly within your workflows. Your agents can leverage Hunter.ios tools to streamline outreach, keep your CRM up-to-date, and power intelligent automation scenarios for sales, recruiting, and more.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Search for email addresses, verify their deliverability, discover companies, and enrich contact data using Hunter.io's powerful email finding capabilities.

View File

@@ -64,7 +64,7 @@ Search for similar content in a knowledge base using vector similarity
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to search in |
| `query` | string | Yes | Search query text |
| `query` | string | No | Search query text \(optional when using tag filters\) |
| `topK` | number | No | Number of most similar results to return \(1-100\) |
| `tagFilters` | any | No | Array of tag filters with tagName and tagValue properties |

View File

@@ -79,7 +79,7 @@ The Mistral Parse tool is particularly useful for scenarios where your agents ne
## Usage Instructions
Extract text and structure from PDF documents using Mistral's OCR API. Configure processing options and get the content in your preferred format. For URLs, they must be publicly accessible and point to a valid PDF file. Note: Google Drive, Dropbox, and other cloud storage links are not supported; use a direct download URL from a web server instead.
Extract text and structure from PDF documents using Mistral's OCR API. Either enter a URL to a PDF document or upload a PDF file directly. Configure processing options and get the content in your preferred format. For URLs, they must be publicly accessible and point to a valid PDF file. Note: Google Drive, Dropbox, and other cloud storage links are not supported; use a direct download URL from a web server instead.

View File

@@ -158,6 +158,10 @@ Send emails using Outlook
| `to` | string | Yes | Recipient email address |
| `subject` | string | Yes | Email subject |
| `body` | string | Yes | Email body content |
| `replyToMessageId` | string | No | Message ID to reply to \(for threading\) |
| `conversationId` | string | No | Conversation ID for threading |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
#### Output

View File

@@ -2,9 +2,12 @@
import Image from 'next/image'
import Link from 'next/link'
import { useBrandConfig } from '@/lib/branding/branding'
import { GridPattern } from '@/app/(landing)/components/grid-pattern'
export default function AuthLayout({ children }: { children: React.ReactNode }) {
const brand = useBrandConfig()
return (
<main className='relative flex min-h-screen flex-col bg-[#0C0C0C] font-geist-sans text-white'>
{/* Background pattern */}
@@ -21,7 +24,17 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
<div className='relative z-10 px-6 pt-9'>
<div className='mx-auto max-w-7xl'>
<Link href='/' className='inline-flex'>
<Image src='/sim.svg' alt='Sim Logo' width={42} height={42} />
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={`${brand.name} Logo`}
width={42}
height={42}
className='h-[42px] w-[42px] object-contain'
/>
) : (
<Image src='/sim.svg' alt={`${brand.name} Logo`} width={42} height={42} />
)}
</Link>
</div>
</div>

View File

@@ -15,6 +15,7 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import { useBrandConfig } from '@/lib/branding/branding'
import { usePrefetchOnHover } from '@/app/(landing)/utils/prefetch'
// --- Framer Motion Variants ---
@@ -165,6 +166,7 @@ export default function NavClient({
const [isMobile, setIsMobile] = useState(initialIsMobile ?? false)
const [isSheetOpen, setIsSheetOpen] = useState(false)
const _router = useRouter()
const brand = useBrandConfig()
useEffect(() => {
setMounted(true)
@@ -199,7 +201,17 @@ export default function NavClient({
<div className='flex flex-1 items-center'>
<div className='inline-block'>
<Link href='/' className='inline-flex'>
<Image src='/sim.svg' alt='Sim Logo' width={42} height={42} />
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={`${brand.name} Logo`}
width={42}
height={42}
className='h-[42px] w-[42px] object-contain'
/>
) : (
<Image src='/sim.svg' alt={`${brand.name} Logo`} width={42} height={42} />
)}
</Link>
</div>
</div>

View File

@@ -1,8 +1,10 @@
import { Analytics } from '@vercel/analytics/next'
import { SpeedInsights } from '@vercel/speed-insights/next'
import { GeistSans } from 'geist/font/sans'
import type { Metadata, Viewport } from 'next'
import { PublicEnvScript } from 'next-runtime-env'
import { BrandedLayout } from '@/components/branded-layout'
import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/metadata'
import { env } from '@/lib/env'
import { isHosted } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getAssetUrl } from '@/lib/utils'
@@ -51,149 +53,20 @@ export const viewport: Viewport = {
userScalable: false,
}
export const metadata: Metadata = {
title: {
template: '',
default: 'Sim',
},
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
applicationName: 'Sim',
authors: [{ name: 'Sim' }],
generator: 'Next.js',
keywords: [
'AI agent',
'AI agent builder',
'AI agent workflow',
'AI workflow automation',
'visual workflow editor',
'AI agents',
'workflow canvas',
'intelligent automation',
'AI tools',
'workflow designer',
'artificial intelligence',
'business automation',
'AI agent workflows',
'visual programming',
],
referrer: 'origin-when-cross-origin',
creator: 'Sim',
publisher: 'Sim',
metadataBase: new URL('https://sim.ai'),
alternates: {
canonical: '/',
languages: {
'en-US': '/en-US',
},
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-video-preview': -1,
'max-snippet': -1,
},
},
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://sim.ai',
title: 'Sim',
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
siteName: 'Sim',
images: [
{
url: getAssetUrl('social/facebook.png'),
width: 1200,
height: 630,
alt: 'Sim',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Sim',
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
images: [getAssetUrl('social/twitter.png')],
creator: '@simstudioai',
site: '@simstudioai',
},
manifest: '/favicon/site.webmanifest',
icons: {
icon: [
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
{
url: '/favicon/favicon-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
url: '/favicon/favicon-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{ url: '/sim.png', sizes: 'any', type: 'image/png' },
],
apple: '/favicon/apple-touch-icon.png',
shortcut: '/favicon/favicon.ico',
},
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'Sim',
},
formatDetection: {
telephone: false,
},
category: 'technology',
other: {
'apple-mobile-web-app-capable': 'yes',
'mobile-web-app-capable': 'yes',
'msapplication-TileColor': '#ffffff',
'msapplication-config': '/favicon/browserconfig.xml',
},
}
// Generate dynamic metadata based on brand configuration
export const metadata: Metadata = generateBrandedMetadata()
export default function RootLayout({ children }: { children: React.ReactNode }) {
const structuredData = generateStructuredData()
return (
<html lang='en' suppressHydrationWarning className={GeistSans.className}>
<html lang='en' suppressHydrationWarning>
<head>
{/* Structured Data for SEO */}
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Sim',
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
url: 'https://sim.ai',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
category: 'SaaS',
},
creator: {
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
},
featureList: [
'Visual AI Agent Builder',
'Workflow Canvas Interface',
'AI Agent Automation',
'Custom AI Workflows',
],
}),
__html: JSON.stringify(structuredData),
}}
/>
@@ -226,24 +99,26 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<PublicEnvScript />
{/* RB2B Script - Only load on hosted version */}
{/* {isHosted && env.NEXT_PUBLIC_RB2B_KEY && (
{isHosted && env.NEXT_PUBLIC_RB2B_KEY && (
<script
dangerouslySetInnerHTML={{
__html: `!function () {var reb2b = window.reb2b = window.reb2b || [];if (reb2b.invoked) return;reb2b.invoked = true;reb2b.methods = ["identify", "collect"];reb2b.factory = function (method) {return function () {var args = Array.prototype.slice.call(arguments);args.unshift(method);reb2b.push(args);return reb2b;};};for (var i = 0; i < reb2b.methods.length; i++) {var key = reb2b.methods[i];reb2b[key] = reb2b.factory(key);}reb2b.load = function (key) {var script = document.createElement("script");script.type = "text/javascript";script.async = true;script.src = "https://b2bjsstore.s3.us-west-2.amazonaws.com/b/" + key + "/${env.NEXT_PUBLIC_RB2B_KEY}.js.gz";var first = document.getElementsByTagName("script")[0];first.parentNode.insertBefore(script, first);};reb2b.SNIPPET_VERSION = "1.0.1";reb2b.load("${env.NEXT_PUBLIC_RB2B_KEY}");}();`,
}}
/>
)} */}
)}
</head>
<body suppressHydrationWarning>
<ZoomPrevention />
<TelemetryConsentDialog />
{children}
{isHosted && (
<>
<SpeedInsights />
<Analytics />
</>
)}
<BrandedLayout>
<ZoomPrevention />
<TelemetryConsentDialog />
{children}
{isHosted && (
<>
<SpeedInsights />
<Analytics />
</>
)}
</BrandedLayout>
</body>
</html>
)

29
apps/sim/app/manifest.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { MetadataRoute } from 'next'
import { getBrandConfig } from '@/lib/branding/branding'
export default function manifest(): MetadataRoute.Manifest {
const brand = getBrandConfig()
return {
name: brand.name,
short_name: brand.name,
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
start_url: '/',
display: 'standalone',
background_color: brand.primaryColor || '#ffffff',
theme_color: brand.primaryColor || '#ffffff',
icons: [
{
src: '/favicon/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/favicon/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
}
}

View File

@@ -4,6 +4,7 @@ import { Suspense, useEffect, useState } from 'react'
import { CheckCircle, Heart, Info, Loader2, XCircle } from 'lucide-react'
import { useSearchParams } from 'next/navigation'
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
import { useBrandConfig } from '@/lib/branding/branding'
interface UnsubscribeData {
success: boolean
@@ -26,6 +27,7 @@ function UnsubscribeContent() {
const [error, setError] = useState<string | null>(null)
const [processing, setProcessing] = useState(false)
const [unsubscribed, setUnsubscribed] = useState(false)
const brand = useBrandConfig()
const email = searchParams.get('email')
const token = searchParams.get('token')
@@ -160,7 +162,7 @@ function UnsubscribeContent() {
<Button
onClick={() =>
window.open(
'mailto:help@sim.ai?subject=Unsubscribe%20Help&body=Hi%2C%20I%20need%20help%20unsubscribing%20from%20emails.%20My%20unsubscribe%20link%20is%20not%20working.',
`mailto:${brand.supportEmail}?subject=Unsubscribe%20Help&body=Hi%2C%20I%20need%20help%20unsubscribing%20from%20emails.%20My%20unsubscribe%20link%20is%20not%20working.`,
'_blank'
)
}
@@ -176,8 +178,8 @@ function UnsubscribeContent() {
<div className='mt-4 text-center'>
<p className='text-muted-foreground text-xs'>
Need immediate help? Email us at{' '}
<a href='mailto:help@sim.ai' className='text-primary hover:underline'>
help@sim.ai
<a href={`mailto:${brand.supportEmail}`} className='text-primary hover:underline'>
{brand.supportEmail}
</a>
</p>
</div>
@@ -222,7 +224,7 @@ function UnsubscribeContent() {
<Button
onClick={() =>
window.open(
'mailto:help@sim.ai?subject=Account%20Help&body=Hi%2C%20I%20need%20help%20with%20my%20account%20emails.',
`mailto:${brand.supportEmail}?subject=Account%20Help&body=Hi%2C%20I%20need%20help%20with%20my%20account%20emails.`,
'_blank'
)
}
@@ -256,8 +258,8 @@ function UnsubscribeContent() {
<p className='text-muted-foreground text-sm'>
If you change your mind, you can always update your email preferences in your account
settings or contact us at{' '}
<a href='mailto:help@sim.ai' className='text-primary hover:underline'>
help@sim.ai
<a href={`mailto:${brand.supportEmail}`} className='text-primary hover:underline'>
{brand.supportEmail}
</a>
</p>
</CardContent>
@@ -369,8 +371,8 @@ function UnsubscribeContent() {
<p className='text-center text-muted-foreground text-xs'>
Questions? Contact us at{' '}
<a href='mailto:help@sim.ai' className='text-primary hover:underline'>
help@sim.ai
<a href={`mailto:${brand.supportEmail}`} className='text-primary hover:underline'>
{brand.supportEmail}
</a>
</p>
</div>

View File

@@ -7,10 +7,14 @@ import { BookOpen, Building2, LibraryBig, ScrollText, Search, Shapes, Workflow }
import { useParams, useRouter } from 'next/navigation'
import { Dialog, DialogOverlay, DialogPortal, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { useBrandConfig } from '@/lib/branding/branding'
import { cn } from '@/lib/utils'
import {
TemplateCard,
TemplateCardSkeleton,
} from '@/app/workspace/[workspaceId]/templates/components/template-card'
import { getKeyboardShortcutText } from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
import { getAllBlocks } from '@/blocks'
import { TemplateCard, TemplateCardSkeleton } from '../../../templates/components/template-card'
import { getKeyboardShortcutText } from '../../hooks/use-keyboard-shortcuts'
import { type NavigationSection, useSearchNavigation } from './hooks/use-search-navigation'
interface SearchModalProps {
@@ -100,6 +104,7 @@ export function SearchModal({
const params = useParams()
const router = useRouter()
const workspaceId = params.workspaceId as string
const brand = useBrandConfig()
// Local state for templates to handle star changes
const [localTemplates, setLocalTemplates] = useState<TemplateData[]>(templates)
@@ -182,7 +187,7 @@ export function SearchModal({
id: 'docs',
name: 'Docs',
icon: BookOpen,
href: 'https://docs.simstudio.ai/',
href: brand.documentationUrl || 'https://docs.sim.ai/',
},
],
[workspaceId]

View File

@@ -0,0 +1,55 @@
'use client'
import { useEffect } from 'react'
import { generateBrandCSS, getBrandConfig } from '@/lib/branding/branding'
interface BrandedLayoutProps {
children: React.ReactNode
}
export function BrandedLayout({ children }: BrandedLayoutProps) {
useEffect(() => {
const config = getBrandConfig()
// Update document title
if (config.name !== 'Sim') {
document.title = config.name
}
// Update favicon
if (config.faviconUrl) {
const faviconLink = document.querySelector("link[rel*='icon']") as HTMLLinkElement
if (faviconLink) {
faviconLink.href = config.faviconUrl
}
}
// Inject brand CSS
const brandStyleId = 'brand-styles'
let brandStyleElement = document.getElementById(brandStyleId) as HTMLStyleElement
if (!brandStyleElement) {
brandStyleElement = document.createElement('style')
brandStyleElement.id = brandStyleId
document.head.appendChild(brandStyleElement)
}
brandStyleElement.textContent = generateBrandCSS(config)
// Load custom CSS if provided
if (config.customCssUrl) {
const customCssId = 'custom-brand-css'
let customCssLink = document.getElementById(customCssId) as HTMLLinkElement
if (!customCssLink) {
customCssLink = document.createElement('link')
customCssLink.id = customCssId
customCssLink.rel = 'stylesheet'
customCssLink.href = config.customCssUrl
document.head.appendChild(customCssLink)
}
}
}, [])
return <>{children}</>
}

View File

@@ -11,6 +11,7 @@ import {
Section,
Text,
} from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
interface WorkspaceInvitation {
workspaceId: string
@@ -57,6 +58,7 @@ export const BatchInvitationEmail = ({
workspaceInvitations = [],
acceptUrl,
}: BatchInvitationEmailProps) => {
const brand = getBrandConfig()
const hasWorkspaces = workspaceInvitations.length > 0
return (
@@ -69,7 +71,13 @@ export const BatchInvitationEmail = ({
<Body style={main}>
<Container style={container}>
<Section style={logoContainer}>
<Img src='https://sim.ai/logo.png' width='120' height='36' alt='Sim' style={logo} />
<Img
src={brand.logoUrl || 'https://sim.ai/logo.png'}
width='120'
height='36'
alt={brand.name}
style={logo}
/>
</Section>
<Heading style={h1}>You're invited to join {organizationName}!</Heading>

View File

@@ -1,4 +1,5 @@
import { Container, Img, Link, Section, Text } from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
@@ -16,6 +17,8 @@ export const EmailFooter = ({
baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai',
unsubscribe,
}: EmailFooterProps) => {
const brand = getBrandConfig()
return (
<Container>
<Section style={{ maxWidth: '580px', margin: '0 auto', padding: '20px 0' }}>
@@ -62,11 +65,11 @@ export const EmailFooter = ({
margin: '8px 0 0 0',
}}
>
© {new Date().getFullYear()} Sim, All Rights Reserved
© {new Date().getFullYear()} {brand.name}, All Rights Reserved
<br />
If you have any questions, please contact us at{' '}
<a
href='mailto:help@sim.ai'
href={`mailto:${brand.supportEmail}`}
style={{
color: '#706a7b !important',
textDecoration: 'underline',
@@ -74,7 +77,7 @@ export const EmailFooter = ({
fontFamily: 'HelveticaNeue, Helvetica, Arial, sans-serif',
}}
>
help@sim.ai
{brand.supportEmail}
</a>
</Text>
<table cellPadding={0} cellSpacing={0} style={{ width: '100%', marginTop: '4px' }}>
@@ -118,7 +121,7 @@ export const EmailFooter = ({
href={
unsubscribe?.unsubscribeToken && unsubscribe?.email
? `${baseUrl}/unsubscribe?token=${unsubscribe.unsubscribeToken}&email=${encodeURIComponent(unsubscribe.email)}`
: `mailto:help@sim.ai?subject=Unsubscribe%20Request&body=Please%20unsubscribe%20me%20from%20all%20emails.`
: `mailto:${brand.supportEmail}?subject=Unsubscribe%20Request&body=Please%20unsubscribe%20me%20from%20all%20emails.`
}
style={{
color: '#706a7b !important',

View File

@@ -12,6 +12,7 @@ import {
Text,
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
@@ -34,6 +35,8 @@ export const InvitationEmail = ({
invitedEmail = '',
updatedDate = new Date(),
}: InvitationEmailProps) => {
const brand = getBrandConfig()
// Extract invitation ID or token from inviteLink if present
let enhancedLink = inviteLink
@@ -60,9 +63,9 @@ export const InvitationEmail = ({
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={getAssetUrl('static/sim.png')}
src={brand.logoUrl || getAssetUrl('static/sim.png')}
width='114'
alt='Sim'
alt={brand.name}
style={{
margin: '0 auto',
}}

View File

@@ -10,6 +10,7 @@ import {
Section,
Text,
} from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
@@ -24,18 +25,18 @@ interface OTPVerificationEmailProps {
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const getSubjectByType = (type: string, chatTitle?: string) => {
const getSubjectByType = (type: string, brandName: string, chatTitle?: string) => {
switch (type) {
case 'sign-in':
return 'Sign in to Sim'
return `Sign in to ${brandName}`
case 'email-verification':
return 'Verify your email for Sim'
return `Verify your email for ${brandName}`
case 'forget-password':
return 'Reset your Sim password'
return `Reset your ${brandName} password`
case 'chat-access':
return `Verification code for ${chatTitle || 'Chat'}`
default:
return 'Verification code for Sim'
return `Verification code for ${brandName}`
}
}
@@ -45,17 +46,19 @@ export const OTPVerificationEmail = ({
type = 'email-verification',
chatTitle,
}: OTPVerificationEmailProps) => {
const brand = getBrandConfig()
// Get a message based on the type
const getMessage = () => {
switch (type) {
case 'sign-in':
return 'Sign in to Sim'
return `Sign in to ${brand.name}`
case 'forget-password':
return 'Reset your password for Sim'
return `Reset your password for ${brand.name}`
case 'chat-access':
return `Access ${chatTitle || 'the chat'}`
default:
return 'Welcome to Sim'
return `Welcome to ${brand.name}`
}
}
@@ -63,15 +66,15 @@ export const OTPVerificationEmail = ({
<Html>
<Head />
<Body style={baseStyles.main}>
<Preview>{getSubjectByType(type, chatTitle)}</Preview>
<Preview>{getSubjectByType(type, brand.name, chatTitle)}</Preview>
<Container style={baseStyles.container}>
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={getAssetUrl('static/sim.png')}
src={brand.logoUrl || getAssetUrl('static/sim.png')}
width='114'
alt='Sim'
alt={brand.name}
style={{
margin: '0 auto',
}}

View File

@@ -12,6 +12,7 @@ import {
Text,
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
@@ -30,19 +31,21 @@ export const ResetPasswordEmail = ({
resetLink = '',
updatedDate = new Date(),
}: ResetPasswordEmailProps) => {
const brand = getBrandConfig()
return (
<Html>
<Head />
<Body style={baseStyles.main}>
<Preview>Reset your Sim password</Preview>
<Preview>Reset your {brand.name} password</Preview>
<Container style={baseStyles.container}>
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={getAssetUrl('static/sim.png')}
src={brand.logoUrl || getAssetUrl('static/sim.png')}
width='114'
alt='Sim'
alt={brand.name}
style={{
margin: '0 auto',
}}
@@ -62,8 +65,8 @@ export const ResetPasswordEmail = ({
<Section style={baseStyles.content}>
<Text style={baseStyles.paragraph}>Hello {username},</Text>
<Text style={baseStyles.paragraph}>
You recently requested to reset your password for your Sim account. Use the button
below to reset it. This password reset is only valid for the next 24 hours.
You recently requested to reset your password for your {brand.name} account. Use the
button below to reset it. This password reset is only valid for the next 24 hours.
</Text>
<Link href={resetLink} style={{ textDecoration: 'none' }}>
<Text style={baseStyles.button}>Reset Your Password</Text>
@@ -75,7 +78,7 @@ export const ResetPasswordEmail = ({
<Text style={baseStyles.paragraph}>
Best regards,
<br />
The Sim Team
The {brand.name} Team
</Text>
<Text
style={{

View File

@@ -11,6 +11,7 @@ import {
Section,
Text,
} from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
@@ -29,6 +30,8 @@ export const WorkspaceInvitationEmail = ({
inviterName = 'Someone',
invitationLink = '',
}: WorkspaceInvitationEmailProps) => {
const brand = getBrandConfig()
// Extract token from the link to ensure we're using the correct format
let enhancedLink = invitationLink
@@ -55,9 +58,9 @@ export const WorkspaceInvitationEmail = ({
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={getAssetUrl('static/sim.png')}
src={brand.logoUrl || getAssetUrl('static/sim.png')}
width='114'
alt='Sim'
alt={brand.name}
style={{
margin: '0 auto',
}}

View File

@@ -0,0 +1,81 @@
import { getEnv } from '@/lib/env'
export interface BrandConfig {
name: string
logoUrl?: string
faviconUrl?: string
primaryColor?: string
secondaryColor?: string
accentColor?: string
customCssUrl?: string
hideBranding?: boolean
footerText?: string
supportEmail?: string
supportUrl?: string
documentationUrl?: string
termsUrl?: string
privacyUrl?: string
}
/**
* Default brand configuration values
*/
const defaultConfig: BrandConfig = {
name: 'Sim',
logoUrl: undefined,
faviconUrl: '/favicon/favicon.ico',
primaryColor: '#000000',
secondaryColor: '#6366f1',
accentColor: '#f59e0b',
customCssUrl: undefined,
hideBranding: false,
footerText: undefined,
supportEmail: 'help@sim.ai',
supportUrl: undefined,
documentationUrl: undefined,
termsUrl: undefined,
privacyUrl: undefined,
}
/**
* Get branding configuration from environment variables
* Supports runtime configuration via Docker/Kubernetes
*/
export const getBrandConfig = (): BrandConfig => {
return {
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultConfig.name,
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultConfig.logoUrl,
faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultConfig.faviconUrl,
primaryColor: getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultConfig.primaryColor,
secondaryColor: getEnv('NEXT_PUBLIC_BRAND_SECONDARY_COLOR') || defaultConfig.secondaryColor,
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultConfig.accentColor,
customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultConfig.customCssUrl,
hideBranding: getEnv('NEXT_PUBLIC_HIDE_BRANDING') === 'true',
footerText: getEnv('NEXT_PUBLIC_CUSTOM_FOOTER_TEXT') || defaultConfig.footerText,
supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultConfig.supportEmail,
supportUrl: getEnv('NEXT_PUBLIC_SUPPORT_URL') || defaultConfig.supportUrl,
documentationUrl: getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultConfig.documentationUrl,
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultConfig.termsUrl,
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultConfig.privacyUrl,
}
}
/**
* Generate CSS custom properties for brand colors
*/
export const generateBrandCSS = (config: BrandConfig): string => {
return `
:root {
--brand-primary: ${config.primaryColor};
--brand-secondary: ${config.secondaryColor};
--brand-accent: ${config.accentColor};
}
`
}
/**
* Hook to use brand configuration in React components
*/
export const useBrandConfig = () => {
return getBrandConfig()
}

View File

@@ -0,0 +1,153 @@
import type { Metadata } from 'next'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
/**
* Generate dynamic metadata based on brand configuration
*/
export function generateBrandedMetadata(override: Partial<Metadata> = {}): Metadata {
const brand = getBrandConfig()
const defaultTitle = brand.name
const defaultDescription = `Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.`
return {
title: {
template: `%s | ${brand.name}`,
default: defaultTitle,
},
description: defaultDescription,
applicationName: brand.name,
authors: [{ name: brand.name }],
generator: 'Next.js',
keywords: [
'AI agent',
'AI agent builder',
'AI agent workflow',
'AI workflow automation',
'visual workflow editor',
'AI agents',
'workflow canvas',
'intelligent automation',
'AI tools',
'workflow designer',
'artificial intelligence',
'business automation',
'AI agent workflows',
'visual programming',
],
referrer: 'origin-when-cross-origin',
creator: brand.name,
publisher: brand.name,
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
alternates: {
canonical: '/',
languages: {
'en-US': '/en-US',
},
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-video-preview': -1,
'max-snippet': -1,
},
},
openGraph: {
type: 'website',
locale: 'en_US',
url: env.NEXT_PUBLIC_APP_URL,
title: defaultTitle,
description: defaultDescription,
siteName: brand.name,
images: [
{
url: brand.logoUrl || getAssetUrl('social/facebook.png'),
width: 1200,
height: 630,
alt: brand.name,
},
],
},
twitter: {
card: 'summary_large_image',
title: defaultTitle,
description: defaultDescription,
images: [brand.logoUrl || getAssetUrl('social/twitter.png')],
creator: '@simstudioai',
site: '@simstudioai',
},
manifest: '/manifest.webmanifest',
icons: {
icon: [
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
{
url: '/favicon/favicon-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
url: '/favicon/favicon-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{ url: brand.faviconUrl || '/sim.png', sizes: 'any', type: 'image/png' },
],
apple: '/favicon/apple-touch-icon.png',
shortcut: brand.faviconUrl || '/favicon/favicon.ico',
},
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: brand.name,
},
formatDetection: {
telephone: false,
},
category: 'technology',
other: {
'apple-mobile-web-app-capable': 'yes',
'mobile-web-app-capable': 'yes',
'msapplication-TileColor': brand.primaryColor || '#ffffff',
'msapplication-config': '/favicon/browserconfig.xml',
},
...override,
}
}
/**
* Generate static structured data for SEO
*/
export function generateStructuredData() {
return {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Sim',
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
url: 'https://sim.ai',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
category: 'SaaS',
},
creator: {
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
},
featureList: [
'Visual AI Agent Builder',
'Workflow Canvas Interface',
'AI Agent Automation',
'Custom AI Workflows',
],
}
}

View File

@@ -170,6 +170,22 @@ export const env = createEnv({
NEXT_PUBLIC_RB2B_KEY: z.string().optional(), // RB2B tracking key for B2B analytics
NEXT_PUBLIC_GOOGLE_API_KEY: z.string().optional(), // Google API key for client-side API calls
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: z.string().optional(), // Google project number for Drive picker
// UI Branding & Whitelabeling
NEXT_PUBLIC_BRAND_NAME: z.string().optional(), // Custom brand name (defaults to "Sim")
NEXT_PUBLIC_BRAND_LOGO_URL: z.string().url().optional(), // Custom logo URL
NEXT_PUBLIC_BRAND_FAVICON_URL: z.string().url().optional(), // Custom favicon URL
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: z.string().optional(), // Primary brand color (hex)
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: z.string().optional(), // Secondary brand color (hex)
NEXT_PUBLIC_BRAND_ACCENT_COLOR: z.string().optional(), // Accent brand color (hex)
NEXT_PUBLIC_CUSTOM_CSS_URL: z.string().url().optional(), // Custom CSS stylesheet URL
NEXT_PUBLIC_HIDE_BRANDING: z.string().optional(), // Hide "Powered by" branding
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: z.string().optional(), // Custom footer text
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
NEXT_PUBLIC_SUPPORT_URL: z.string().url().optional(), // Custom support URL
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
},
// Variables available on both server and client
@@ -188,6 +204,20 @@ export const env = createEnv({
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: process.env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
NEXT_PUBLIC_BRAND_NAME: process.env.NEXT_PUBLIC_BRAND_NAME,
NEXT_PUBLIC_BRAND_LOGO_URL: process.env.NEXT_PUBLIC_BRAND_LOGO_URL,
NEXT_PUBLIC_BRAND_FAVICON_URL: process.env.NEXT_PUBLIC_BRAND_FAVICON_URL,
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: process.env.NEXT_PUBLIC_BRAND_PRIMARY_COLOR,
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: process.env.NEXT_PUBLIC_BRAND_SECONDARY_COLOR,
NEXT_PUBLIC_BRAND_ACCENT_COLOR: process.env.NEXT_PUBLIC_BRAND_ACCENT_COLOR,
NEXT_PUBLIC_CUSTOM_CSS_URL: process.env.NEXT_PUBLIC_CUSTOM_CSS_URL,
NEXT_PUBLIC_HIDE_BRANDING: process.env.NEXT_PUBLIC_HIDE_BRANDING,
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: process.env.NEXT_PUBLIC_CUSTOM_FOOTER_TEXT,
NEXT_PUBLIC_SUPPORT_EMAIL: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
NEXT_PUBLIC_SUPPORT_URL: process.env.NEXT_PUBLIC_SUPPORT_URL,
NEXT_PUBLIC_DOCUMENTATION_URL: process.env.NEXT_PUBLIC_DOCUMENTATION_URL,
NEXT_PUBLIC_TERMS_URL: process.env.NEXT_PUBLIC_TERMS_URL,
NEXT_PUBLIC_PRIVACY_URL: process.env.NEXT_PUBLIC_PRIVACY_URL,
NODE_ENV: process.env.NODE_ENV,
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
},

View File

@@ -15,12 +15,9 @@
"devDependencies": {
"@biomejs/biome": "2.0.0-beta.5",
"@next/env": "^15.3.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"dotenv-cli": "^8.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"turbo": "2.5.5",
},
},
@@ -1260,8 +1257,6 @@
"@trigger.dev/sdk": ["@trigger.dev/sdk@3.3.17", "", { "dependencies": { "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.52.1", "@opentelemetry/semantic-conventions": "1.25.1", "@trigger.dev/core": "3.3.17", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", "terminal-link": "^3.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "uuid": "^9.0.0", "ws": "^8.11.0" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-wjIjlQWKybYWw/J7LxFIOO1pXzxXoj9lxbFMvjb51JtfebxnQnh6aExN47nOGhVhV38wHYstfBI/8ClWwBnFYw=="],
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
@@ -2210,8 +2205,6 @@
"jaeger-client": ["jaeger-client@3.19.0", "", { "dependencies": { "node-int64": "^0.4.0", "opentracing": "^0.14.4", "thriftrw": "^3.5.0", "uuid": "^8.3.2", "xorshift": "^1.1.1" } }, "sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw=="],
"javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
"jest-diff": ["jest-diff@26.6.2", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", "jest-get-type": "^26.3.0", "pretty-format": "^26.6.2" } }, "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA=="],
"jest-get-type": ["jest-get-type@26.3.0", "", {}, "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig=="],
@@ -2670,9 +2663,7 @@
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
@@ -3474,8 +3465,6 @@
"@react-email/code-block/prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="],
"@react-email/render/prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@@ -4088,6 +4077,8 @@
"ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"resend/@react-email/render/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"sim/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],

View File

@@ -0,0 +1,91 @@
# Whitelabeled deployment example for Sim
# This configuration shows how to customize branding for Acme Corp
# Global configuration
global:
imageRegistry: "ghcr.io"
storageClass: "managed-csi-premium"
# Main application with custom branding
app:
enabled: true
replicaCount: 1
# Custom branding configuration
env:
# Application URLs (update with your domain)
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
BETTER_AUTH_URL: "https://sim.acme.ai"
SOCKET_SERVER_URL: "https://sim-ws.acme.ai"
NEXT_PUBLIC_SOCKET_URL: "https://sim-ws.acme.ai"
# Security settings (REQUIRED)
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ENCRYPTION_KEY: "your-production-encryption-key-here"
# UI Branding & Whitelabeling Configuration
NEXT_PUBLIC_BRAND_NAME: "Acme AI Studio"
NEXT_PUBLIC_BRAND_LOGO_URL: "https://acme.com/assets/logo.png"
NEXT_PUBLIC_BRAND_FAVICON_URL: "https://acme.com/assets/favicon.ico"
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: "#1a365d" # Acme blue
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: "#2d3748" # Dark gray
NEXT_PUBLIC_BRAND_ACCENT_COLOR: "#38b2ac" # Teal accent
NEXT_PUBLIC_CUSTOM_CSS_URL: "https://acme.com/assets/theme.css"
NEXT_PUBLIC_HIDE_BRANDING: "true" # Hide "Powered by Sim"
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: "© 2024 Acme Corp. All rights reserved."
NEXT_PUBLIC_SUPPORT_EMAIL: "ai-support@acme.com"
NEXT_PUBLIC_SUPPORT_URL: "https://help.acme.com/ai-studio"
NEXT_PUBLIC_DOCUMENTATION_URL: "https://docs.acme.com/ai-studio"
NEXT_PUBLIC_TERMS_URL: "https://acme.com/terms"
NEXT_PUBLIC_PRIVACY_URL: "https://acme.com/privacy"
# Realtime service
realtime:
enabled: true
replicaCount: 1
env:
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
BETTER_AUTH_URL: "https://sim.acme.ai"
NEXT_PUBLIC_SOCKET_URL: "https://sim-ws.acme.ai"
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ALLOWED_ORIGINS: "https://sim.acme.ai"
# PostgreSQL database
postgresql:
enabled: true
auth:
password: "your-secure-db-password-here"
persistence:
enabled: true
size: 20Gi
# Ingress configuration
ingress:
enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
app:
host: "sim.acme.ai"
paths:
- path: /
pathType: Prefix
realtime:
host: "sim-ws.acme.ai"
paths:
- path: /
pathType: Prefix
tls:
enabled: true
secretName: "sim-acme-tls"
# Auto-scaling
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70

View File

@@ -71,6 +71,22 @@ app:
GITHUB_CLIENT_ID: ""
GITHUB_CLIENT_SECRET: ""
RESEND_API_KEY: ""
# UI Branding & Whitelabeling Configuration
NEXT_PUBLIC_BRAND_NAME: "Sim" # Custom brand name
NEXT_PUBLIC_BRAND_LOGO_URL: "" # Custom logo URL (leave empty for default)
NEXT_PUBLIC_BRAND_FAVICON_URL: "" # Custom favicon URL (leave empty for default)
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: "#000000" # Primary brand color (hex)
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: "#6366f1" # Secondary brand color (hex)
NEXT_PUBLIC_BRAND_ACCENT_COLOR: "#f59e0b" # Accent brand color (hex)
NEXT_PUBLIC_CUSTOM_CSS_URL: "" # Custom stylesheet URL (leave empty for none)
NEXT_PUBLIC_HIDE_BRANDING: "false" # Hide "Powered by" branding (true/false)
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: "" # Custom footer text (leave empty for default)
NEXT_PUBLIC_SUPPORT_EMAIL: "help@sim.ai" # Support email address
NEXT_PUBLIC_SUPPORT_URL: "" # Support page URL (leave empty for none)
NEXT_PUBLIC_DOCUMENTATION_URL: "" # Documentation URL (leave empty for none)
NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none)
NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none)
# Service configuration
service:

View File

@@ -41,12 +41,9 @@
"devDependencies": {
"@biomejs/biome": "2.0.0-beta.5",
"@next/env": "^15.3.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"dotenv-cli": "^8.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"turbo": "2.5.5"
},
"lint-staged": {