diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx
index 16298b4204..ffd6c3515b 100644
--- a/apps/sim/app/(auth)/login/login-form.tsx
+++ b/apps/sim/app/(auth)/login/login-form.tsx
@@ -304,6 +304,15 @@ export default function LoginPage({
return
}
+ const emailValidation = quickValidateEmail(forgotPasswordEmail.trim().toLowerCase())
+ if (!emailValidation.isValid) {
+ setResetStatus({
+ type: 'error',
+ message: 'Please enter a valid email address',
+ })
+ return
+ }
+
try {
setIsSubmittingReset(true)
setResetStatus({ type: null, message: '' })
@@ -321,7 +330,23 @@ export default function LoginPage({
if (!response.ok) {
const errorData = await response.json()
- throw new Error(errorData.message || 'Failed to request password reset')
+ let errorMessage = errorData.message || 'Failed to request password reset'
+
+ if (
+ errorMessage.includes('Invalid body parameters') ||
+ errorMessage.includes('invalid email')
+ ) {
+ errorMessage = 'Please enter a valid email address'
+ } else if (errorMessage.includes('Email is required')) {
+ errorMessage = 'Please enter your email address'
+ } else if (
+ errorMessage.includes('user not found') ||
+ errorMessage.includes('User not found')
+ ) {
+ errorMessage = 'No account found with this email address'
+ }
+
+ throw new Error(errorMessage)
}
setResetStatus({
@@ -497,7 +522,8 @@ export default function LoginPage({
Reset Password
- Enter your email address and we'll send you a link to reset your password.
+ Enter your email address and we'll send you a link to reset your password if your
+ account exists.
@@ -512,14 +538,20 @@ export default function LoginPage({
placeholder='Enter your email'
required
type='email'
- className='border-neutral-700/80 bg-neutral-900 text-white placeholder:text-white/60 focus:border-[var(--brand-primary-hover-hex)]/70 focus:ring-[var(--brand-primary-hover-hex)]/20'
+ className={cn(
+ 'border-neutral-700/80 bg-neutral-900 text-white placeholder:text-white/60 focus:border-[var(--brand-primary-hover-hex)]/70 focus:ring-[var(--brand-primary-hover-hex)]/20',
+ resetStatus.type === 'error' && 'border-red-500 focus-visible:ring-red-500'
+ )}
/>
+ {resetStatus.type === 'error' && (
+
+
{resetStatus.message}
+
+ )}
- {resetStatus.type && (
-
- {resetStatus.message}
+ {resetStatus.type === 'success' && (
+
)}
{
})
})
- it('should prevent submission with invalid name validation', async () => {
+ it('should automatically trim spaces from name input', async () => {
const mockSignUp = vi.mocked(client.signUp.email)
+ mockSignUp.mockResolvedValue({ data: null, error: null })
render( )
@@ -176,22 +177,20 @@ describe('SignupPage', () => {
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
const submitButton = screen.getByRole('button', { name: /create account/i })
- // Use name with leading/trailing spaces which should fail validation
fireEvent.change(nameInput, { target: { value: ' John Doe ' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
- // Should not call signUp because validation failed
- expect(mockSignUp).not.toHaveBeenCalled()
-
- // Should show validation error
await waitFor(() => {
- expect(
- screen.getByText(
- /Name cannot contain consecutive spaces|Name cannot start or end with spaces/
- )
- ).toBeInTheDocument()
+ expect(mockSignUp).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'John Doe',
+ email: 'user@company.com',
+ password: 'Password123!',
+ }),
+ expect.any(Object)
+ )
})
})
diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx
index 3c645e082d..c80f89643c 100644
--- a/apps/sim/app/(auth)/signup/signup-form.tsx
+++ b/apps/sim/app/(auth)/signup/signup-form.tsx
@@ -49,10 +49,6 @@ const NAME_VALIDATIONS = {
regex: /^(?!.*\s\s).*$/,
message: 'Name cannot contain consecutive spaces.',
},
- noLeadingTrailingSpaces: {
- test: (value: string) => value === value.trim(),
- message: 'Name cannot start or end with spaces.',
- },
}
const validateEmailField = (emailValue: string): string[] => {
@@ -175,10 +171,6 @@ function SignupFormContent({
errors.push(NAME_VALIDATIONS.noConsecutiveSpaces.message)
}
- if (!NAME_VALIDATIONS.noLeadingTrailingSpaces.test(nameValue)) {
- errors.push(NAME_VALIDATIONS.noLeadingTrailingSpaces.message)
- }
-
return errors
}
@@ -193,11 +185,10 @@ function SignupFormContent({
}
const handleNameChange = (e: React.ChangeEvent) => {
- const newName = e.target.value
- setName(newName)
+ const rawValue = e.target.value
+ setName(rawValue)
- // Silently validate but don't show errors until submit
- const errors = validateName(newName)
+ const errors = validateName(rawValue)
setNameErrors(errors)
setShowNameValidationError(false)
}
@@ -224,23 +215,21 @@ function SignupFormContent({
const formData = new FormData(e.currentTarget)
const emailValue = formData.get('email') as string
const passwordValue = formData.get('password') as string
- const name = formData.get('name') as string
+ const nameValue = formData.get('name') as string
- // Validate name on submit
- const nameValidationErrors = validateName(name)
+ const trimmedName = nameValue.trim()
+
+ const nameValidationErrors = validateName(trimmedName)
setNameErrors(nameValidationErrors)
setShowNameValidationError(nameValidationErrors.length > 0)
- // Validate email on submit
const emailValidationErrors = validateEmailField(emailValue)
setEmailErrors(emailValidationErrors)
setShowEmailValidationError(emailValidationErrors.length > 0)
- // Validate password on submit
const errors = validatePassword(passwordValue)
setPasswordErrors(errors)
- // Only show validation errors if there are any
setShowValidationError(errors.length > 0)
try {
@@ -249,7 +238,6 @@ function SignupFormContent({
emailValidationErrors.length > 0 ||
errors.length > 0
) {
- // Prioritize name errors first, then email errors, then password errors
if (nameValidationErrors.length > 0) {
setNameErrors([nameValidationErrors[0]])
setShowNameValidationError(true)
@@ -266,8 +254,6 @@ function SignupFormContent({
return
}
- // Check if name will be truncated and warn user
- const trimmedName = name.trim()
if (trimmedName.length > 100) {
setNameErrors(['Name will be truncated to 100 characters. Please shorten your name.'])
setShowNameValidationError(true)
@@ -337,7 +323,6 @@ function SignupFormContent({
logger.info('Session refreshed after successful signup')
} catch (sessionError) {
logger.error('Failed to refresh session after signup:', sessionError)
- // Continue anyway - the verification flow will handle this
}
// For new signups, always require verification
diff --git a/apps/sim/app/(auth)/verify/use-verification.ts b/apps/sim/app/(auth)/verify/use-verification.ts
index 849b2bfef1..ecaf68036f 100644
--- a/apps/sim/app/(auth)/verify/use-verification.ts
+++ b/apps/sim/app/(auth)/verify/use-verification.ts
@@ -215,20 +215,28 @@ export function useVerification({
setOtp(value)
}
+ // Auto-submit when OTP is complete
+ useEffect(() => {
+ if (otp.length === 6 && email && !isLoading && !isVerified) {
+ const timeoutId = setTimeout(() => {
+ verifyCode()
+ }, 300) // Small delay to ensure UI is ready
+
+ return () => clearTimeout(timeoutId)
+ }
+ }, [otp, email, isLoading, isVerified])
+
useEffect(() => {
if (typeof window !== 'undefined') {
if (!isProduction || !hasResendKey) {
const storedEmail = sessionStorage.getItem('verificationEmail')
- logger.info('Auto-verifying user', { email: storedEmail })
}
const isDevOrDocker = !isProduction || isTruthy(env.DOCKER_BUILD)
- // Auto-verify and redirect in development/docker environments
if (isDevOrDocker || !hasResendKey) {
setIsVerified(true)
- // Clear verification requirement cookie (same as manual verification)
document.cookie =
'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts
index cb44796865..31663e34b8 100644
--- a/apps/sim/app/api/environment/route.ts
+++ b/apps/sim/app/api/environment/route.ts
@@ -10,7 +10,6 @@ import type { EnvironmentVariable } from '@/stores/settings/environment/types'
const logger = createLogger('EnvironmentAPI')
-// Schema for environment variable updates
const EnvVarSchema = z.object({
variables: z.record(z.string()),
})
@@ -30,7 +29,6 @@ export async function POST(req: NextRequest) {
try {
const { variables } = EnvVarSchema.parse(body)
- // Encrypt all variables
const encryptedVariables = await Promise.all(
Object.entries(variables).map(async ([key, value]) => {
const { encrypted } = await encryptSecret(value)
@@ -38,7 +36,6 @@ export async function POST(req: NextRequest) {
})
).then((entries) => Object.fromEntries(entries))
- // Replace all environment variables for user
await db
.insert(environment)
.values({
@@ -78,7 +75,6 @@ export async function GET(request: Request) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
- // Get the session directly in the API route
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized environment variables access attempt`)
@@ -97,18 +93,15 @@ export async function GET(request: Request) {
return NextResponse.json({ data: {} }, { status: 200 })
}
- // Decrypt the variables for client-side use
const encryptedVariables = result[0].variables as Record
const decryptedVariables: Record = {}
- // Decrypt each variable
for (const [key, encryptedValue] of Object.entries(encryptedVariables)) {
try {
const { decrypted } = await decryptSecret(encryptedValue)
decryptedVariables[key] = { key, value: decrypted }
} catch (error) {
logger.error(`[${requestId}] Error decrypting variable ${key}`, error)
- // If decryption fails, provide a placeholder
decryptedVariables[key] = { key, value: '' }
}
}
diff --git a/apps/sim/app/api/environment/variables/route.ts b/apps/sim/app/api/environment/variables/route.ts
deleted file mode 100644
index d2a7efcdf9..0000000000
--- a/apps/sim/app/api/environment/variables/route.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import { eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { z } from 'zod'
-import { getEnvironmentVariableKeys } from '@/lib/environment/utils'
-import { createLogger } from '@/lib/logs/console/logger'
-import { decryptSecret, encryptSecret } from '@/lib/utils'
-import { getUserId } from '@/app/api/auth/oauth/utils'
-import { db } from '@/db'
-import { environment } from '@/db/schema'
-
-const logger = createLogger('EnvironmentVariablesAPI')
-
-// Schema for environment variable updates
-const EnvVarSchema = z.object({
- variables: z.record(z.string()),
-})
-
-export async function GET(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
-
- try {
- // For GET requests, check for workflowId in query params
- const { searchParams } = new URL(request.url)
- const workflowId = searchParams.get('workflowId')
-
- // Use dual authentication pattern like other copilot tools
- const userId = await getUserId(requestId, workflowId || undefined)
-
- if (!userId) {
- logger.warn(`[${requestId}] Unauthorized environment variables access attempt`)
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- // Get only the variable names (keys), not values
- const result = await getEnvironmentVariableKeys(userId)
-
- return NextResponse.json(
- {
- success: true,
- output: result,
- },
- { status: 200 }
- )
- } catch (error: any) {
- logger.error(`[${requestId}] Environment variables fetch error`, error)
- return NextResponse.json(
- {
- success: false,
- error: error.message || 'Failed to get environment variables',
- },
- { status: 500 }
- )
- }
-}
-
-export async function PUT(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
-
- try {
- const body = await request.json()
- const { workflowId, variables } = body
-
- // Use dual authentication pattern like other copilot tools
- const userId = await getUserId(requestId, workflowId)
-
- if (!userId) {
- logger.warn(`[${requestId}] Unauthorized environment variables set attempt`)
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- try {
- const { variables: validatedVariables } = EnvVarSchema.parse({ variables })
-
- // Get existing environment variables for this user
- const existingData = await db
- .select()
- .from(environment)
- .where(eq(environment.userId, userId))
- .limit(1)
-
- // Start with existing encrypted variables or empty object
- const existingEncryptedVariables =
- (existingData[0]?.variables as Record) || {}
-
- // Determine which variables are new or changed by comparing with decrypted existing values
- const variablesToEncrypt: Record = {}
- const addedVariables: string[] = []
- const updatedVariables: string[] = []
-
- for (const [key, newValue] of Object.entries(validatedVariables)) {
- if (!(key in existingEncryptedVariables)) {
- // New variable
- variablesToEncrypt[key] = newValue
- addedVariables.push(key)
- } else {
- // Check if the value has actually changed by decrypting the existing value
- try {
- const { decrypted: existingValue } = await decryptSecret(
- existingEncryptedVariables[key]
- )
-
- if (existingValue !== newValue) {
- // Value changed, needs re-encryption
- variablesToEncrypt[key] = newValue
- updatedVariables.push(key)
- }
- // If values are the same, keep the existing encrypted value
- } catch (decryptError) {
- // If we can't decrypt the existing value, treat as changed and re-encrypt
- logger.warn(
- `[${requestId}] Could not decrypt existing variable ${key}, re-encrypting`,
- {
- error: decryptError,
- }
- )
- variablesToEncrypt[key] = newValue
- updatedVariables.push(key)
- }
- }
- }
-
- // Only encrypt the variables that are new or changed
- const newlyEncryptedVariables = await Promise.all(
- Object.entries(variablesToEncrypt).map(async ([key, value]) => {
- const { encrypted } = await encryptSecret(value)
- return [key, encrypted] as const
- })
- ).then((entries) => Object.fromEntries(entries))
-
- // Merge existing encrypted variables with newly encrypted ones
- const finalEncryptedVariables = { ...existingEncryptedVariables, ...newlyEncryptedVariables }
-
- // Update or insert environment variables for user
- await db
- .insert(environment)
- .values({
- id: crypto.randomUUID(),
- userId: userId,
- variables: finalEncryptedVariables,
- updatedAt: new Date(),
- })
- .onConflictDoUpdate({
- target: [environment.userId],
- set: {
- variables: finalEncryptedVariables,
- updatedAt: new Date(),
- },
- })
-
- return NextResponse.json(
- {
- success: true,
- output: {
- message: `Successfully processed ${Object.keys(validatedVariables).length} environment variable(s): ${addedVariables.length} added, ${updatedVariables.length} updated`,
- variableCount: Object.keys(validatedVariables).length,
- variableNames: Object.keys(validatedVariables),
- totalVariableCount: Object.keys(finalEncryptedVariables).length,
- addedVariables,
- updatedVariables,
- },
- },
- { status: 200 }
- )
- } catch (validationError) {
- if (validationError instanceof z.ZodError) {
- logger.warn(`[${requestId}] Invalid environment variables data`, {
- errors: validationError.errors,
- })
- return NextResponse.json(
- { error: 'Invalid request data', details: validationError.errors },
- { status: 400 }
- )
- }
- throw validationError
- }
- } catch (error: any) {
- logger.error(`[${requestId}] Environment variables set error`, error)
- return NextResponse.json(
- {
- success: false,
- error: error.message || 'Failed to set environment variables',
- },
- { status: 500 }
- )
- }
-}
-
-export async function POST(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
-
- try {
- const body = await request.json()
- const { workflowId } = body
-
- // Use dual authentication pattern like other copilot tools
- const userId = await getUserId(requestId, workflowId)
-
- if (!userId) {
- logger.warn(`[${requestId}] Unauthorized environment variables access attempt`)
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- // Get only the variable names (keys), not values
- const result = await getEnvironmentVariableKeys(userId)
-
- return NextResponse.json(
- {
- success: true,
- output: result,
- },
- { status: 200 }
- )
- } catch (error: any) {
- logger.error(`[${requestId}] Environment variables fetch error`, error)
- return NextResponse.json(
- {
- success: false,
- error: error.message || 'Failed to get environment variables',
- },
- { status: 500 }
- )
- }
-}
diff --git a/apps/sim/app/api/files/presigned/route.test.ts b/apps/sim/app/api/files/presigned/route.test.ts
index 4702324d52..3fde4ca3ce 100644
--- a/apps/sim/app/api/files/presigned/route.test.ts
+++ b/apps/sim/app/api/files/presigned/route.test.ts
@@ -1,7 +1,13 @@
import { NextRequest } from 'next/server'
-import { beforeEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setupFileApiMocks } from '@/app/api/__test-utils__/utils'
+/**
+ * Tests for file presigned API route
+ *
+ * @vitest-environment node
+ */
+
describe('/api/files/presigned', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -19,7 +25,7 @@ describe('/api/files/presigned', () => {
})
describe('POST', () => {
- test('should return error when cloud storage is not enabled', async () => {
+ it('should return error when cloud storage is not enabled', async () => {
setupFileApiMocks({
cloudEnabled: false,
storageProvider: 's3',
@@ -39,7 +45,7 @@ describe('/api/files/presigned', () => {
const response = await POST(request)
const data = await response.json()
- expect(response.status).toBe(500) // Changed from 400 to 500 (StorageConfigError)
+ expect(response.status).toBe(500)
expect(data.error).toBe('Direct uploads are only available when cloud storage is enabled')
expect(data.code).toBe('STORAGE_CONFIG_ERROR')
expect(data.directUploadSupported).toBe(false)
diff --git a/apps/sim/app/api/logs/by-id/[id]/route.ts b/apps/sim/app/api/logs/[id]/route.ts
similarity index 100%
rename from apps/sim/app/api/logs/by-id/[id]/route.ts
rename to apps/sim/app/api/logs/[id]/route.ts
diff --git a/apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts b/apps/sim/app/api/logs/execution/[executionId]/route.ts
similarity index 82%
rename from apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts
rename to apps/sim/app/api/logs/execution/[executionId]/route.ts
index be596d034b..decfeea953 100644
--- a/apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts
+++ b/apps/sim/app/api/logs/execution/[executionId]/route.ts
@@ -4,7 +4,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
import { workflowExecutionLogs, workflowExecutionSnapshots } from '@/db/schema'
-const logger = createLogger('FrozenCanvasAPI')
+const logger = createLogger('LogsByExecutionIdAPI')
export async function GET(
_request: NextRequest,
@@ -13,7 +13,7 @@ export async function GET(
try {
const { executionId } = await params
- logger.debug(`Fetching frozen canvas data for execution: ${executionId}`)
+ logger.debug(`Fetching execution data for: ${executionId}`)
// Get the workflow execution log to find the snapshot
const [workflowLog] = await db
@@ -50,14 +50,14 @@ export async function GET(
},
}
- logger.debug(`Successfully fetched frozen canvas data for execution: ${executionId}`)
+ logger.debug(`Successfully fetched execution data for: ${executionId}`)
logger.debug(
`Workflow state contains ${Object.keys((snapshot.stateData as any)?.blocks || {}).length} blocks`
)
return NextResponse.json(response)
} catch (error) {
- logger.error('Error fetching frozen canvas data:', error)
- return NextResponse.json({ error: 'Failed to fetch frozen canvas data' }, { status: 500 })
+ logger.error('Error fetching execution data:', error)
+ return NextResponse.json({ error: 'Failed to fetch execution data' }, { status: 500 })
}
}
diff --git a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
new file mode 100644
index 0000000000..236ffd3a91
--- /dev/null
+++ b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
@@ -0,0 +1,198 @@
+import { randomUUID } from 'crypto'
+import { and, eq } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import { getSession } from '@/lib/auth'
+import { createLogger } from '@/lib/logs/console/logger'
+import { db } from '@/db'
+import {
+ invitation,
+ member,
+ organization,
+ permissions,
+ user,
+ type WorkspaceInvitationStatus,
+ workspaceInvitation,
+} from '@/db/schema'
+
+const logger = createLogger('OrganizationInvitation')
+
+// Get invitation details
+export async function GET(
+ _req: NextRequest,
+ { params }: { params: Promise<{ id: string; invitationId: string }> }
+) {
+ const { id: organizationId, invitationId } = await params
+ const session = await getSession()
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ try {
+ const orgInvitation = await db
+ .select()
+ .from(invitation)
+ .where(and(eq(invitation.id, invitationId), eq(invitation.organizationId, organizationId)))
+ .then((rows) => rows[0])
+
+ if (!orgInvitation) {
+ return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
+ }
+
+ const org = await db
+ .select()
+ .from(organization)
+ .where(eq(organization.id, organizationId))
+ .then((rows) => rows[0])
+
+ if (!org) {
+ return NextResponse.json({ error: 'Organization not found' }, { status: 404 })
+ }
+
+ return NextResponse.json({
+ invitation: orgInvitation,
+ organization: org,
+ })
+ } catch (error) {
+ logger.error('Error fetching organization invitation:', error)
+ return NextResponse.json({ error: 'Failed to fetch invitation' }, { status: 500 })
+ }
+}
+
+export async function PUT(
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string; invitationId: string }> }
+) {
+ const { id: organizationId, invitationId } = await params
+ const session = await getSession()
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ try {
+ const { status } = await req.json()
+
+ if (!status || !['accepted', 'rejected', 'cancelled'].includes(status)) {
+ return NextResponse.json(
+ { error: 'Invalid status. Must be "accepted", "rejected", or "cancelled"' },
+ { status: 400 }
+ )
+ }
+
+ const orgInvitation = await db
+ .select()
+ .from(invitation)
+ .where(and(eq(invitation.id, invitationId), eq(invitation.organizationId, organizationId)))
+ .then((rows) => rows[0])
+
+ if (!orgInvitation) {
+ return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
+ }
+
+ if (orgInvitation.status !== 'pending') {
+ return NextResponse.json({ error: 'Invitation already processed' }, { status: 400 })
+ }
+
+ if (status === 'accepted') {
+ const userData = await db
+ .select()
+ .from(user)
+ .where(eq(user.id, session.user.id))
+ .then((rows) => rows[0])
+
+ if (!userData || userData.email.toLowerCase() !== orgInvitation.email.toLowerCase()) {
+ return NextResponse.json(
+ { error: 'Email mismatch. You can only accept invitations sent to your email address.' },
+ { status: 403 }
+ )
+ }
+ }
+
+ if (status === 'cancelled') {
+ const isAdmin = await db
+ .select()
+ .from(member)
+ .where(
+ and(
+ eq(member.organizationId, organizationId),
+ eq(member.userId, session.user.id),
+ eq(member.role, 'admin')
+ )
+ )
+ .then((rows) => rows.length > 0)
+
+ if (!isAdmin) {
+ return NextResponse.json(
+ { error: 'Only organization admins can cancel invitations' },
+ { status: 403 }
+ )
+ }
+ }
+
+ await db.transaction(async (tx) => {
+ await tx.update(invitation).set({ status }).where(eq(invitation.id, invitationId))
+
+ if (status === 'accepted') {
+ await tx.insert(member).values({
+ id: randomUUID(),
+ userId: session.user.id,
+ organizationId,
+ role: orgInvitation.role,
+ createdAt: new Date(),
+ })
+
+ const linkedWorkspaceInvitations = await tx
+ .select()
+ .from(workspaceInvitation)
+ .where(
+ and(
+ eq(workspaceInvitation.orgInvitationId, invitationId),
+ eq(workspaceInvitation.status, 'pending' as WorkspaceInvitationStatus)
+ )
+ )
+
+ for (const wsInvitation of linkedWorkspaceInvitations) {
+ await tx
+ .update(workspaceInvitation)
+ .set({
+ status: 'accepted' as WorkspaceInvitationStatus,
+ updatedAt: new Date(),
+ })
+ .where(eq(workspaceInvitation.id, wsInvitation.id))
+
+ await tx.insert(permissions).values({
+ id: randomUUID(),
+ entityType: 'workspace',
+ entityId: wsInvitation.workspaceId,
+ userId: session.user.id,
+ permissionType: wsInvitation.permissions || 'read',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ }
+ } else if (status === 'cancelled') {
+ await tx
+ .update(workspaceInvitation)
+ .set({ status: 'cancelled' as WorkspaceInvitationStatus })
+ .where(eq(workspaceInvitation.orgInvitationId, invitationId))
+ }
+ })
+
+ logger.info(`Organization invitation ${status}`, {
+ organizationId,
+ invitationId,
+ userId: session.user.id,
+ email: orgInvitation.email,
+ })
+
+ return NextResponse.json({
+ success: true,
+ message: `Invitation ${status} successfully`,
+ invitation: { ...orgInvitation, status },
+ })
+ } catch (error) {
+ logger.error(`Error updating organization invitation:`, error)
+ return NextResponse.json({ error: 'Failed to update invitation' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts
index 470b1d3e0a..07bc930759 100644
--- a/apps/sim/app/api/organizations/[id]/invitations/route.ts
+++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts
@@ -17,9 +17,17 @@ import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
-import { invitation, member, organization, user, workspace, workspaceInvitation } from '@/db/schema'
+import {
+ invitation,
+ member,
+ organization,
+ user,
+ type WorkspaceInvitationStatus,
+ workspace,
+ workspaceInvitation,
+} from '@/db/schema'
-const logger = createLogger('OrganizationInvitationsAPI')
+const logger = createLogger('OrganizationInvitations')
interface WorkspaceInvitation {
workspaceId: string
@@ -40,7 +48,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const { id: organizationId } = await params
- // Verify user has access to this organization
const memberEntry = await db
.select()
.from(member)
@@ -61,7 +68,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 })
}
- // Get all pending invitations for the organization
const invitations = await db
.select({
id: invitation.id,
@@ -118,10 +124,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const body = await request.json()
const { email, emails, role = 'member', workspaceInvitations } = body
- // Handle single invitation vs batch
const invitationEmails = email ? [email] : emails
- // Validate input
if (!invitationEmails || !Array.isArray(invitationEmails) || invitationEmails.length === 0) {
return NextResponse.json({ error: 'Email or emails array is required' }, { status: 400 })
}
@@ -130,7 +134,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Invalid role' }, { status: 400 })
}
- // Verify user has admin access
const memberEntry = await db
.select()
.from(member)
@@ -148,7 +151,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 })
}
- // Handle validation-only requests
if (validateOnly) {
const validationResult = await validateBulkInvitations(organizationId, invitationEmails)
@@ -167,7 +169,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
})
}
- // Validate seat availability
const seatValidation = await validateSeatAvailability(organizationId, invitationEmails.length)
if (!seatValidation.canInvite) {
@@ -185,7 +186,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
)
}
- // Get organization details
const organizationEntry = await db
.select({ name: organization.name })
.from(organization)
@@ -196,7 +196,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Organization not found' }, { status: 404 })
}
- // Validate and normalize emails
const processedEmails = invitationEmails
.map((email: string) => {
const normalized = email.trim().toLowerCase()
@@ -209,11 +208,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'No valid emails provided' }, { status: 400 })
}
- // Handle batch workspace invitations if provided
const validWorkspaceInvitations: WorkspaceInvitation[] = []
if (isBatch && workspaceInvitations && workspaceInvitations.length > 0) {
for (const wsInvitation of workspaceInvitations) {
- // Check if user has admin permission on this workspace
const canInvite = await hasWorkspaceAdminAccess(session.user.id, wsInvitation.workspaceId)
if (!canInvite) {
@@ -229,7 +226,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
}
- // Check for existing members
const existingMembers = await db
.select({ userEmail: user.email })
.from(member)
@@ -239,7 +235,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const existingEmails = existingMembers.map((m) => m.userEmail)
const newEmails = processedEmails.filter((email: string) => !existingEmails.includes(email))
- // Check for existing pending invitations
const existingInvitations = await db
.select({ email: invitation.email })
.from(invitation)
@@ -265,7 +260,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
)
}
- // Create invitations
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
const invitationsToCreate = emailsToInvite.map((email: string) => ({
id: randomUUID(),
@@ -280,7 +274,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
await db.insert(invitation).values(invitationsToCreate)
- // Create workspace invitations if batch mode
const workspaceInvitationIds: string[] = []
if (isBatch && validWorkspaceInvitations.length > 0) {
for (const email of emailsToInvite) {
@@ -309,7 +302,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
}
- // Send invitation emails
const inviter = await db
.select({ name: user.name })
.from(user)
@@ -322,7 +314,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
let emailResult
if (isBatch && validWorkspaceInvitations.length > 0) {
- // Get workspace details for batch email
const workspaceDetails = await db
.select({
id: workspace.id,
@@ -348,7 +339,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
organizationEntry[0]?.name || 'organization',
role,
workspaceInvitationsWithNames,
- `${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`
+ `${env.NEXT_PUBLIC_APP_URL}/invite/organization?id=${orgInvitation.id}`
)
emailResult = await sendEmail({
@@ -361,7 +352,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
- `${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`,
+ `${env.NEXT_PUBLIC_APP_URL}/invite/organization?id=${orgInvitation.id}`,
email
)
@@ -448,7 +439,6 @@ export async function DELETE(
)
}
- // Verify user has admin access
const memberEntry = await db
.select()
.from(member)
@@ -466,7 +456,6 @@ export async function DELETE(
return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 })
}
- // Cancel the invitation
const result = await db
.update(invitation)
.set({ status: 'cancelled' })
@@ -486,22 +475,19 @@ export async function DELETE(
)
}
- // Also cancel any linked workspace invitations created as part of the batch
await db
.update(workspaceInvitation)
- .set({ status: 'cancelled' })
+ .set({ status: 'cancelled' as WorkspaceInvitationStatus })
.where(eq(workspaceInvitation.orgInvitationId, invitationId))
- // Legacy fallback: cancel any pending workspace invitations for the same email
- // that do not have an orgInvitationId and were created by the same inviter
await db
.update(workspaceInvitation)
- .set({ status: 'cancelled' })
+ .set({ status: 'cancelled' as WorkspaceInvitationStatus })
.where(
and(
isNull(workspaceInvitation.orgInvitationId),
eq(workspaceInvitation.email, result[0].email),
- eq(workspaceInvitation.status, 'pending'),
+ eq(workspaceInvitation.status, 'pending' as WorkspaceInvitationStatus),
eq(workspaceInvitation.inviterId, session.user.id)
)
)
diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts
index 9dda509a86..445539a001 100644
--- a/apps/sim/app/api/organizations/[id]/members/route.ts
+++ b/apps/sim/app/api/organizations/[id]/members/route.ts
@@ -260,7 +260,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
- `${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${invitationId}`,
+ `${env.NEXT_PUBLIC_APP_URL}/invite/organization?id=${invitationId}`,
normalizedEmail
)
diff --git a/apps/sim/app/api/organizations/invitations/accept/route.ts b/apps/sim/app/api/organizations/invitations/accept/route.ts
deleted file mode 100644
index b42e44c239..0000000000
--- a/apps/sim/app/api/organizations/invitations/accept/route.ts
+++ /dev/null
@@ -1,359 +0,0 @@
-import { randomUUID } from 'crypto'
-import { and, eq, isNull, or } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { getSession } from '@/lib/auth'
-import { env } from '@/lib/env'
-import { createLogger } from '@/lib/logs/console/logger'
-import { db } from '@/db'
-import { invitation, member, permissions, user, workspaceInvitation } from '@/db/schema'
-
-const logger = createLogger('OrganizationInvitationAcceptanceAPI')
-
-// Accept an organization invitation and any associated workspace invitations
-export async function GET(req: NextRequest) {
- const invitationId = req.nextUrl.searchParams.get('id')
-
- if (!invitationId) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=missing-invitation-id',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- const session = await getSession()
-
- if (!session?.user?.id) {
- // Redirect to login, user will be redirected back after login
- return NextResponse.redirect(
- new URL(
- `/invite/organization?id=${invitationId}`,
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- try {
- // Find the organization invitation
- const invitationResult = await db
- .select()
- .from(invitation)
- .where(eq(invitation.id, invitationId))
- .limit(1)
-
- if (invitationResult.length === 0) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=invalid-invitation',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- const orgInvitation = invitationResult[0]
-
- // Check if invitation has expired
- if (orgInvitation.expiresAt && new Date() > orgInvitation.expiresAt) {
- return NextResponse.redirect(
- new URL('/invite/invite-error?reason=expired', env.NEXT_PUBLIC_APP_URL || 'https://sim.ai')
- )
- }
-
- // Check if invitation is still pending
- if (orgInvitation.status !== 'pending') {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=already-processed',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Get user data to check email verification status
- const userData = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
-
- if (userData.length === 0) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=user-not-found',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Verify the email matches the current user
- if (orgInvitation.email !== session.user.email) {
- return NextResponse.redirect(
- new URL(
- `/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${orgInvitation.email}, but you're logged in as ${userData[0].email}`)}`,
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Check if user is already a member of the organization
- const existingMember = await db
- .select()
- .from(member)
- .where(
- and(
- eq(member.organizationId, orgInvitation.organizationId),
- eq(member.userId, session.user.id)
- )
- )
- .limit(1)
-
- if (existingMember.length > 0) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=already-member',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Start transaction to accept both organization and workspace invitations
- await db.transaction(async (tx) => {
- // Accept organization invitation - add user as member
- await tx.insert(member).values({
- id: randomUUID(),
- userId: session.user.id,
- organizationId: orgInvitation.organizationId,
- role: orgInvitation.role,
- createdAt: new Date(),
- })
-
- // Mark organization invitation as accepted
- await tx.update(invitation).set({ status: 'accepted' }).where(eq(invitation.id, invitationId))
-
- // Find and accept any pending workspace invitations linked to this org invite.
- // For backward compatibility, also include legacy pending invites by email with no org link.
- const workspaceInvitations = await tx
- .select()
- .from(workspaceInvitation)
- .where(
- and(
- eq(workspaceInvitation.status, 'pending'),
- or(
- eq(workspaceInvitation.orgInvitationId, invitationId),
- and(
- isNull(workspaceInvitation.orgInvitationId),
- eq(workspaceInvitation.email, orgInvitation.email)
- )
- )
- )
- )
-
- for (const wsInvitation of workspaceInvitations) {
- // Check if invitation hasn't expired
- if (
- wsInvitation.expiresAt &&
- new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
- ) {
- // Check if user doesn't already have permissions on the workspace
- const existingPermission = await tx
- .select()
- .from(permissions)
- .where(
- and(
- eq(permissions.userId, session.user.id),
- eq(permissions.entityType, 'workspace'),
- eq(permissions.entityId, wsInvitation.workspaceId)
- )
- )
- .limit(1)
-
- if (existingPermission.length === 0) {
- // Add workspace permissions
- await tx.insert(permissions).values({
- id: randomUUID(),
- userId: session.user.id,
- entityType: 'workspace',
- entityId: wsInvitation.workspaceId,
- permissionType: wsInvitation.permissions,
- createdAt: new Date(),
- updatedAt: new Date(),
- })
-
- // Mark workspace invitation as accepted
- await tx
- .update(workspaceInvitation)
- .set({ status: 'accepted' })
- .where(eq(workspaceInvitation.id, wsInvitation.id))
-
- logger.info('Accepted workspace invitation', {
- workspaceId: wsInvitation.workspaceId,
- userId: session.user.id,
- permission: wsInvitation.permissions,
- })
- }
- }
- }
- })
-
- logger.info('Successfully accepted batch invitation', {
- organizationId: orgInvitation.organizationId,
- userId: session.user.id,
- role: orgInvitation.role,
- })
-
- // Redirect to success page or main app
- return NextResponse.redirect(
- new URL('/workspaces?invite=accepted', env.NEXT_PUBLIC_APP_URL || 'https://sim.ai')
- )
- } catch (error) {
- logger.error('Failed to accept organization invitation', {
- invitationId,
- userId: session.user.id,
- error,
- })
-
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=server-error',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-}
-
-// POST endpoint for programmatic acceptance (for API use)
-export async function POST(req: NextRequest) {
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- try {
- const { invitationId } = await req.json()
-
- if (!invitationId) {
- return NextResponse.json({ error: 'Missing invitationId' }, { status: 400 })
- }
-
- // Similar logic to GET but return JSON response
- const invitationResult = await db
- .select()
- .from(invitation)
- .where(eq(invitation.id, invitationId))
- .limit(1)
-
- if (invitationResult.length === 0) {
- return NextResponse.json({ error: 'Invalid invitation' }, { status: 404 })
- }
-
- const orgInvitation = invitationResult[0]
-
- if (orgInvitation.expiresAt && new Date() > orgInvitation.expiresAt) {
- return NextResponse.json({ error: 'Invitation expired' }, { status: 400 })
- }
-
- if (orgInvitation.status !== 'pending') {
- return NextResponse.json({ error: 'Invitation already processed' }, { status: 400 })
- }
-
- // Get user data to check email verification status
- const userData = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
-
- if (userData.length === 0) {
- return NextResponse.json({ error: 'User not found' }, { status: 404 })
- }
-
- if (orgInvitation.email !== session.user.email) {
- return NextResponse.json({ error: 'Email mismatch' }, { status: 403 })
- }
-
- // Check if user is already a member
- const existingMember = await db
- .select()
- .from(member)
- .where(
- and(
- eq(member.organizationId, orgInvitation.organizationId),
- eq(member.userId, session.user.id)
- )
- )
- .limit(1)
-
- if (existingMember.length > 0) {
- return NextResponse.json({ error: 'Already a member' }, { status: 400 })
- }
-
- let acceptedWorkspaces = 0
-
- // Accept invitations in transaction
- await db.transaction(async (tx) => {
- // Accept organization invitation
- await tx.insert(member).values({
- id: randomUUID(),
- userId: session.user.id,
- organizationId: orgInvitation.organizationId,
- role: orgInvitation.role,
- createdAt: new Date(),
- })
-
- await tx.update(invitation).set({ status: 'accepted' }).where(eq(invitation.id, invitationId))
-
- // Accept workspace invitations
- const workspaceInvitations = await tx
- .select()
- .from(workspaceInvitation)
- .where(
- and(
- eq(workspaceInvitation.email, orgInvitation.email),
- eq(workspaceInvitation.status, 'pending')
- )
- )
-
- for (const wsInvitation of workspaceInvitations) {
- if (
- wsInvitation.expiresAt &&
- new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
- ) {
- const existingPermission = await tx
- .select()
- .from(permissions)
- .where(
- and(
- eq(permissions.userId, session.user.id),
- eq(permissions.entityType, 'workspace'),
- eq(permissions.entityId, wsInvitation.workspaceId)
- )
- )
- .limit(1)
-
- if (existingPermission.length === 0) {
- await tx.insert(permissions).values({
- id: randomUUID(),
- userId: session.user.id,
- entityType: 'workspace',
- entityId: wsInvitation.workspaceId,
- permissionType: wsInvitation.permissions,
- createdAt: new Date(),
- updatedAt: new Date(),
- })
-
- await tx
- .update(workspaceInvitation)
- .set({ status: 'accepted' })
- .where(eq(workspaceInvitation.id, wsInvitation.id))
-
- acceptedWorkspaces++
- }
- }
- }
- })
-
- return NextResponse.json({
- success: true,
- message: `Successfully joined organization and ${acceptedWorkspaces} workspace(s)`,
- organizationId: orgInvitation.organizationId,
- workspacesJoined: acceptedWorkspaces,
- })
- } catch (error) {
- logger.error('Failed to accept organization invitation via API', { error })
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
diff --git a/apps/sim/app/api/usage/check/route.ts b/apps/sim/app/api/usage/check/route.ts
deleted file mode 100644
index e6bb2413ae..0000000000
--- a/apps/sim/app/api/usage/check/route.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { type NextRequest, NextResponse } from 'next/server'
-import { getSession } from '@/lib/auth'
-import { checkServerSideUsageLimits } from '@/lib/billing'
-import { createLogger } from '@/lib/logs/console/logger'
-
-const logger = createLogger('UsageCheckAPI')
-
-export async function GET(_request: NextRequest) {
- const session = await getSession()
- try {
- const userId = session?.user?.id
- if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
-
- const result = await checkServerSideUsageLimits(userId)
- // Normalize to client usage shape
- return NextResponse.json({
- success: true,
- data: {
- percentUsed:
- result.limit > 0
- ? Math.min(Math.floor((result.currentUsage / result.limit) * 100), 100)
- : 0,
- isWarning:
- result.limit > 0
- ? (result.currentUsage / result.limit) * 100 >= 80 &&
- (result.currentUsage / result.limit) * 100 < 100
- : false,
- isExceeded: result.isExceeded,
- currentUsage: result.currentUsage,
- limit: result.limit,
- message: result.message,
- },
- })
- } catch (error) {
- logger.error('Failed usage check', { error })
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
diff --git a/apps/sim/app/api/usage-limits/route.ts b/apps/sim/app/api/usage/route.ts
similarity index 96%
rename from apps/sim/app/api/usage-limits/route.ts
rename to apps/sim/app/api/usage/route.ts
index 5429cef362..9d9a04147d 100644
--- a/apps/sim/app/api/usage-limits/route.ts
+++ b/apps/sim/app/api/usage/route.ts
@@ -7,11 +7,11 @@ import {
} from '@/lib/billing/core/organization'
import { createLogger } from '@/lib/logs/console/logger'
-const logger = createLogger('UnifiedUsageLimitsAPI')
+const logger = createLogger('UnifiedUsageAPI')
/**
- * Unified Usage Limits Endpoint
- * GET/PUT /api/usage-limits?context=user|organization&userId=&organizationId=
+ * Unified Usage Endpoint
+ * GET/PUT /api/usage?context=user|organization&userId=&organizationId=
*
*/
export async function GET(request: NextRequest) {
diff --git a/apps/sim/app/api/users/rate-limit/route.ts b/apps/sim/app/api/users/me/rate-limit/route.ts
similarity index 94%
rename from apps/sim/app/api/users/rate-limit/route.ts
rename to apps/sim/app/api/users/me/rate-limit/route.ts
index 06125793a5..904f37f298 100644
--- a/apps/sim/app/api/users/rate-limit/route.ts
+++ b/apps/sim/app/api/users/me/rate-limit/route.ts
@@ -11,15 +11,12 @@ const logger = createLogger('RateLimitAPI')
export async function GET(request: NextRequest) {
try {
- // Try session auth first (for web UI)
const session = await getSession()
let authenticatedUserId: string | null = session?.user?.id || null
- // If no session, check for API key auth
if (!authenticatedUserId) {
const apiKeyHeader = request.headers.get('x-api-key')
if (apiKeyHeader) {
- // Verify API key
const [apiKeyRecord] = await db
.select({ userId: apiKeyTable.userId })
.from(apiKeyTable)
@@ -36,7 +33,6 @@ export async function GET(request: NextRequest) {
return createErrorResponse('Authentication required', 401)
}
- // Get user subscription
const [subscriptionRecord] = await db
.select({ plan: subscription.plan })
.from(subscription)
diff --git a/apps/sim/app/api/wand-generate/route.ts b/apps/sim/app/api/wand-generate/route.ts
index bca08194ed..2aad903654 100644
--- a/apps/sim/app/api/wand-generate/route.ts
+++ b/apps/sim/app/api/wand-generate/route.ts
@@ -49,7 +49,6 @@ interface RequestBody {
history?: ChatMessage[]
}
-// Helper: safe stringify for error payloads that may include circular structures
function safeStringify(value: unknown): string {
try {
return JSON.stringify(value)
@@ -83,18 +82,14 @@ export async function POST(req: NextRequest) {
)
}
- // Use provided system prompt or default
const finalSystemPrompt =
systemPrompt ||
'You are a helpful AI assistant. Generate content exactly as requested by the user.'
- // Prepare messages for OpenAI API
const messages: ChatMessage[] = [{ role: 'system', content: finalSystemPrompt }]
- // Add previous messages from history
messages.push(...history.filter((msg) => msg.role !== 'system'))
- // Add the current user prompt
messages.push({ role: 'user', content: prompt })
logger.debug(
@@ -108,7 +103,6 @@ export async function POST(req: NextRequest) {
}
)
- // For streaming responses
if (stream) {
try {
logger.debug(
@@ -119,7 +113,6 @@ export async function POST(req: NextRequest) {
`[${requestId}] About to create stream with model: ${useWandAzure ? wandModelName : 'gpt-4o'}`
)
- // Use native fetch for streaming to avoid OpenAI SDK issues with Node.js runtime
const apiUrl = useWandAzure
? `${azureEndpoint}/openai/deployments/${wandModelName}/chat/completions?api-version=${azureApiVersion}`
: 'https://api.openai.com/v1/chat/completions'
@@ -161,7 +154,6 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Stream response received, starting processing`)
- // Create a TransformStream to process the SSE data
const encoder = new TextEncoder()
const decoder = new TextDecoder()
@@ -187,12 +179,10 @@ export async function POST(req: NextRequest) {
break
}
- // Decode the chunk
buffer += decoder.decode(value, { stream: true })
- // Process complete SSE messages
const lines = buffer.split('\n')
- buffer = lines.pop() || '' // Keep incomplete line in buffer
+ buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
@@ -217,25 +207,21 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Received first content chunk`)
}
- // Forward the content
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ chunk: content })}\n\n`)
)
}
- // Log usage if present
if (parsed.usage) {
logger.info(
`[${requestId}] Received usage data: ${JSON.stringify(parsed.usage)}`
)
}
- // Log progress periodically
if (chunkCount % 10 === 0) {
logger.debug(`[${requestId}] Processed ${chunkCount} chunks`)
}
} catch (parseError) {
- // Skip invalid JSON lines
logger.debug(
`[${requestId}] Skipped non-JSON line: ${data.substring(0, 100)}`
)
@@ -252,7 +238,6 @@ export async function POST(req: NextRequest) {
stack: streamError?.stack,
})
- // Send error to client
const errorData = `data: ${JSON.stringify({ error: 'Streaming failed', done: true })}\n\n`
controller.enqueue(encoder.encode(errorData))
controller.close()
@@ -262,14 +247,12 @@ export async function POST(req: NextRequest) {
},
})
- // Return Response with proper headers for Node.js runtime
return new Response(readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
- 'X-Accel-Buffering': 'no', // Disable Nginx buffering
- 'Transfer-Encoding': 'chunked', // Important for Node.js runtime
+ 'X-Accel-Buffering': 'no',
},
})
} catch (error: any) {
@@ -294,7 +277,6 @@ export async function POST(req: NextRequest) {
}
}
- // For non-streaming responses
const completion = await client.chat.completions.create({
model: useWandAzure ? wandModelName : 'gpt-4o',
messages: messages,
diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts
index f10f50b246..831eada916 100644
--- a/apps/sim/app/api/workflows/route.ts
+++ b/apps/sim/app/api/workflows/route.ts
@@ -1,10 +1,12 @@
import crypto from 'crypto'
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
-import { workflow, workflowBlocks } from '@/db/schema'
+import { workflow, workflowBlocks, workspace } from '@/db/schema'
+import { verifyWorkspaceMembership } from './utils'
const logger = createLogger('WorkflowAPI')
@@ -16,6 +18,68 @@ const CreateWorkflowSchema = z.object({
folderId: z.string().nullable().optional(),
})
+// GET /api/workflows - Get workflows for user (optionally filtered by workspaceId)
+export async function GET(request: Request) {
+ const requestId = crypto.randomUUID().slice(0, 8)
+ const startTime = Date.now()
+ const url = new URL(request.url)
+ const workspaceId = url.searchParams.get('workspaceId')
+
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ logger.warn(`[${requestId}] Unauthorized workflow access attempt`)
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const userId = session.user.id
+
+ if (workspaceId) {
+ const workspaceExists = await db
+ .select({ id: workspace.id })
+ .from(workspace)
+ .where(eq(workspace.id, workspaceId))
+ .then((rows) => rows.length > 0)
+
+ if (!workspaceExists) {
+ logger.warn(
+ `[${requestId}] Attempt to fetch workflows for non-existent workspace: ${workspaceId}`
+ )
+ return NextResponse.json(
+ { error: 'Workspace not found', code: 'WORKSPACE_NOT_FOUND' },
+ { status: 404 }
+ )
+ }
+
+ const userRole = await verifyWorkspaceMembership(userId, workspaceId)
+
+ if (!userRole) {
+ logger.warn(
+ `[${requestId}] User ${userId} attempted to access workspace ${workspaceId} without membership`
+ )
+ return NextResponse.json(
+ { error: 'Access denied to this workspace', code: 'WORKSPACE_ACCESS_DENIED' },
+ { status: 403 }
+ )
+ }
+ }
+
+ let workflows
+
+ if (workspaceId) {
+ workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, workspaceId))
+ } else {
+ workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
+ }
+
+ return NextResponse.json({ data: workflows }, { status: 200 })
+ } catch (error: any) {
+ const elapsed = Date.now() - startTime
+ logger.error(`[${requestId}] Workflow fetch error after ${elapsed}ms`, error)
+ return NextResponse.json({ error: error.message }, { status: 500 })
+ }
+}
+
// POST /api/workflows - Create a new workflow
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
@@ -36,114 +100,7 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`)
- // Create initial state with start block
- const initialState = {
- blocks: {
- [starterId]: {
- id: starterId,
- type: 'starter',
- name: 'Start',
- position: { x: 100, y: 100 },
- subBlocks: {
- startWorkflow: {
- id: 'startWorkflow',
- type: 'dropdown',
- value: 'manual',
- },
- webhookPath: {
- id: 'webhookPath',
- type: 'short-input',
- value: '',
- },
- webhookSecret: {
- id: 'webhookSecret',
- type: 'short-input',
- value: '',
- },
- scheduleType: {
- id: 'scheduleType',
- type: 'dropdown',
- value: 'daily',
- },
- minutesInterval: {
- id: 'minutesInterval',
- type: 'short-input',
- value: '',
- },
- minutesStartingAt: {
- id: 'minutesStartingAt',
- type: 'short-input',
- value: '',
- },
- hourlyMinute: {
- id: 'hourlyMinute',
- type: 'short-input',
- value: '',
- },
- dailyTime: {
- id: 'dailyTime',
- type: 'short-input',
- value: '',
- },
- weeklyDay: {
- id: 'weeklyDay',
- type: 'dropdown',
- value: 'MON',
- },
- weeklyDayTime: {
- id: 'weeklyDayTime',
- type: 'short-input',
- value: '',
- },
- monthlyDay: {
- id: 'monthlyDay',
- type: 'short-input',
- value: '',
- },
- monthlyTime: {
- id: 'monthlyTime',
- type: 'short-input',
- value: '',
- },
- cronExpression: {
- id: 'cronExpression',
- type: 'short-input',
- value: '',
- },
- timezone: {
- id: 'timezone',
- type: 'dropdown',
- value: 'UTC',
- },
- },
- outputs: {
- response: {
- type: {
- input: 'any',
- },
- },
- },
- enabled: true,
- horizontalHandles: true,
- isWide: false,
- advancedMode: false,
- triggerMode: false,
- height: 95,
- },
- },
- edges: [],
- subflows: {},
- variables: {},
- metadata: {
- version: '1.0.0',
- createdAt: now.toISOString(),
- updatedAt: now.toISOString(),
- },
- }
-
- // Create the workflow and start block in a transaction
await db.transaction(async (tx) => {
- // Create the workflow
await tx.insert(workflow).values({
id: workflowId,
userId: session.user.id,
@@ -163,7 +120,6 @@ export async function POST(req: NextRequest) {
marketplaceData: null,
})
- // Insert the start block into workflow_blocks table
await tx.insert(workflowBlocks).values({
id: starterId,
workflowId: workflowId,
diff --git a/apps/sim/app/api/workflows/sync/route.ts b/apps/sim/app/api/workflows/sync/route.ts
deleted file mode 100644
index 987e6f372f..0000000000
--- a/apps/sim/app/api/workflows/sync/route.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import crypto from 'crypto'
-import { and, eq, isNull } from 'drizzle-orm'
-import { NextResponse } from 'next/server'
-import { getSession } from '@/lib/auth'
-import { createLogger } from '@/lib/logs/console/logger'
-import { getUserEntityPermissions } from '@/lib/permissions/utils'
-import { db } from '@/db'
-import { workflow, workspace } from '@/db/schema'
-
-const logger = createLogger('WorkflowAPI')
-
-/**
- * Verifies user's workspace permissions using the permissions table
- * @param userId User ID to check
- * @param workspaceId Workspace ID to check
- * @returns Permission type if user has access, null otherwise
- */
-async function verifyWorkspaceMembership(
- userId: string,
- workspaceId: string
-): Promise {
- try {
- const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
-
- return permission
- } catch (error) {
- logger.error(`Error verifying workspace permissions for ${userId} in ${workspaceId}:`, error)
- return null
- }
-}
-
-export async function GET(request: Request) {
- const requestId = crypto.randomUUID().slice(0, 8)
- const startTime = Date.now()
- const url = new URL(request.url)
- const workspaceId = url.searchParams.get('workspaceId')
-
- try {
- // Get the session directly in the API route
- const session = await getSession()
- if (!session?.user?.id) {
- logger.warn(`[${requestId}] Unauthorized workflow access attempt`)
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const userId = session.user.id
-
- // If workspaceId is provided, verify it exists and user is a member
- if (workspaceId) {
- // Check workspace exists first
- const workspaceExists = await db
- .select({ id: workspace.id })
- .from(workspace)
- .where(eq(workspace.id, workspaceId))
- .then((rows) => rows.length > 0)
-
- if (!workspaceExists) {
- logger.warn(
- `[${requestId}] Attempt to fetch workflows for non-existent workspace: ${workspaceId}`
- )
- return NextResponse.json(
- { error: 'Workspace not found', code: 'WORKSPACE_NOT_FOUND' },
- { status: 404 }
- )
- }
-
- // Verify the user is a member of the workspace using our optimized function
- const userRole = await verifyWorkspaceMembership(userId, workspaceId)
-
- if (!userRole) {
- logger.warn(
- `[${requestId}] User ${userId} attempted to access workspace ${workspaceId} without membership`
- )
- return NextResponse.json(
- { error: 'Access denied to this workspace', code: 'WORKSPACE_ACCESS_DENIED' },
- { status: 403 }
- )
- }
-
- // Migrate any orphaned workflows to this workspace (in background)
- migrateOrphanedWorkflows(userId, workspaceId).catch((error) => {
- logger.error(`[${requestId}] Error migrating orphaned workflows:`, error)
- })
- }
-
- // Fetch workflows for the user
- let workflows
-
- if (workspaceId) {
- // Filter by workspace ID only, not user ID
- // This allows sharing workflows across workspace members
- workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, workspaceId))
- } else {
- // Filter by user ID only, including workflows without workspace IDs
- workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
- }
-
- const elapsed = Date.now() - startTime
-
- // Return the workflows
- return NextResponse.json({ data: workflows }, { status: 200 })
- } catch (error: any) {
- const elapsed = Date.now() - startTime
- logger.error(`[${requestId}] Workflow fetch error after ${elapsed}ms`, error)
- return NextResponse.json({ error: error.message }, { status: 500 })
- }
-}
-
-// Helper function to migrate orphaned workflows to a workspace
-async function migrateOrphanedWorkflows(userId: string, workspaceId: string) {
- try {
- // Find workflows without workspace IDs for this user
- const orphanedWorkflows = await db
- .select({ id: workflow.id })
- .from(workflow)
- .where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId)))
-
- if (orphanedWorkflows.length === 0) {
- return // No orphaned workflows to migrate
- }
-
- logger.info(
- `Migrating ${orphanedWorkflows.length} orphaned workflows to workspace ${workspaceId}`
- )
-
- // Update workflows in batch if possible
- try {
- // Batch update all orphaned workflows
- await db
- .update(workflow)
- .set({
- workspaceId: workspaceId,
- updatedAt: new Date(),
- })
- .where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId)))
-
- logger.info(
- `Successfully migrated ${orphanedWorkflows.length} workflows to workspace ${workspaceId}`
- )
- } catch (batchError) {
- logger.warn('Batch migration failed, falling back to individual updates:', batchError)
-
- // Fallback to individual updates if batch update fails
- for (const { id } of orphanedWorkflows) {
- try {
- await db
- .update(workflow)
- .set({
- workspaceId: workspaceId,
- updatedAt: new Date(),
- })
- .where(eq(workflow.id, id))
- } catch (updateError) {
- logger.error(`Failed to migrate workflow ${id}:`, updateError)
- }
- }
- }
- } catch (error) {
- logger.error('Error migrating orphaned workflows:', error)
- // Continue execution even if migration fails
- }
-}
-
-// POST method removed - workflow operations now handled by:
-// - POST /api/workflows (create)
-// - DELETE /api/workflows/[id] (delete)
-// - Socket.IO collaborative operations (real-time updates)
diff --git a/apps/sim/app/api/workflows/utils.ts b/apps/sim/app/api/workflows/utils.ts
index 75ee1ab977..10478bcfda 100644
--- a/apps/sim/app/api/workflows/utils.ts
+++ b/apps/sim/app/api/workflows/utils.ts
@@ -1,4 +1,8 @@
import { NextResponse } from 'next/server'
+import { createLogger } from '@/lib/logs/console/logger'
+import { getUserEntityPermissions } from '@/lib/permissions/utils'
+
+const logger = createLogger('WorkflowUtils')
export function createErrorResponse(error: string, status: number, code?: string) {
return NextResponse.json(
@@ -13,3 +17,23 @@ export function createErrorResponse(error: string, status: number, code?: string
export function createSuccessResponse(data: any) {
return NextResponse.json(data)
}
+
+/**
+ * Verifies user's workspace permissions using the permissions table
+ * @param userId User ID to check
+ * @param workspaceId Workspace ID to check
+ * @returns Permission type if user has access, null otherwise
+ */
+export async function verifyWorkspaceMembership(
+ userId: string,
+ workspaceId: string
+): Promise {
+ try {
+ const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
+
+ return permission
+ } catch (error) {
+ logger.error(`Error verifying workspace permissions for ${userId} in ${workspaceId}:`, error)
+ return null
+ }
+}
diff --git a/apps/sim/app/api/workspaces/invitations/[id]/route.test.ts b/apps/sim/app/api/workspaces/invitations/[id]/route.test.ts
deleted file mode 100644
index a4391b74ed..0000000000
--- a/apps/sim/app/api/workspaces/invitations/[id]/route.test.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { getSession } from '@/lib/auth'
-import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
-import { DELETE } from '@/app/api/workspaces/invitations/[id]/route'
-import { db } from '@/db'
-import { workspaceInvitation } from '@/db/schema'
-
-vi.mock('@/lib/auth', () => ({
- getSession: vi.fn(),
-}))
-
-vi.mock('@/lib/permissions/utils', () => ({
- hasWorkspaceAdminAccess: vi.fn(),
-}))
-
-vi.mock('@/db', () => ({
- db: {
- select: vi.fn(),
- delete: vi.fn(),
- },
-}))
-
-vi.mock('@/db/schema', () => ({
- workspaceInvitation: {
- id: 'id',
- workspaceId: 'workspaceId',
- email: 'email',
- inviterId: 'inviterId',
- status: 'status',
- },
-}))
-
-vi.mock('drizzle-orm', () => ({
- eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
-}))
-
-describe('DELETE /api/workspaces/invitations/[id]', () => {
- const mockSession = {
- user: {
- id: 'user123',
- email: 'user@example.com',
- name: 'Test User',
- emailVerified: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- image: null,
- stripeCustomerId: null,
- },
- session: {
- id: 'session123',
- token: 'token123',
- userId: 'user123',
- expiresAt: new Date(Date.now() + 86400000), // 1 day from now
- createdAt: new Date(),
- updatedAt: new Date(),
- ipAddress: null,
- userAgent: null,
- activeOrganizationId: null,
- },
- }
-
- const mockInvitation = {
- id: 'invitation123',
- workspaceId: 'workspace456',
- email: 'invited@example.com',
- inviterId: 'inviter789',
- status: 'pending',
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('should return 401 when user is not authenticated', async () => {
- vi.mocked(getSession).mockResolvedValue(null)
-
- const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
- method: 'DELETE',
- })
-
- const params = Promise.resolve({ id: 'invitation123' })
- const response = await DELETE(req, { params })
-
- expect(response).toBeInstanceOf(NextResponse)
- const data = await response.json()
- expect(response.status).toBe(401)
- expect(data).toEqual({ error: 'Unauthorized' })
- })
-
- it('should return 404 when invitation does not exist', async () => {
- vi.mocked(getSession).mockResolvedValue(mockSession)
-
- // Mock invitation not found
- const mockQuery = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn((callback: (rows: any[]) => any) => {
- // Simulate empty rows array
- return Promise.resolve(callback([]))
- }),
- }
- vi.mocked(db.select).mockReturnValue(mockQuery as any)
-
- const req = new NextRequest('http://localhost/api/workspaces/invitations/non-existent', {
- method: 'DELETE',
- })
-
- const params = Promise.resolve({ id: 'non-existent' })
- const response = await DELETE(req, { params })
-
- expect(response).toBeInstanceOf(NextResponse)
- const data = await response.json()
- expect(response.status).toBe(404)
- expect(data).toEqual({ error: 'Invitation not found' })
- })
-
- it('should return 403 when user does not have admin access', async () => {
- vi.mocked(getSession).mockResolvedValue(mockSession)
-
- // Mock invitation found
- const mockQuery = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn((callback: (rows: any[]) => any) => {
- // Return the first invitation from the array
- return Promise.resolve(callback([mockInvitation]))
- }),
- }
- vi.mocked(db.select).mockReturnValue(mockQuery as any)
-
- // Mock user does not have admin access
- vi.mocked(hasWorkspaceAdminAccess).mockResolvedValue(false)
-
- const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
- method: 'DELETE',
- })
-
- const params = Promise.resolve({ id: 'invitation123' })
- const response = await DELETE(req, { params })
-
- expect(response).toBeInstanceOf(NextResponse)
- const data = await response.json()
- expect(response.status).toBe(403)
- expect(data).toEqual({ error: 'Insufficient permissions' })
- expect(hasWorkspaceAdminAccess).toHaveBeenCalledWith('user123', 'workspace456')
- })
-
- it('should return 400 when trying to delete non-pending invitation', async () => {
- vi.mocked(getSession).mockResolvedValue(mockSession)
-
- // Mock invitation with accepted status
- const acceptedInvitation = { ...mockInvitation, status: 'accepted' }
- const mockQuery = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn((callback: (rows: any[]) => any) => {
- // Return the first invitation from the array
- return Promise.resolve(callback([acceptedInvitation]))
- }),
- }
- vi.mocked(db.select).mockReturnValue(mockQuery as any)
-
- // Mock user has admin access
- vi.mocked(hasWorkspaceAdminAccess).mockResolvedValue(true)
-
- const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
- method: 'DELETE',
- })
-
- const params = Promise.resolve({ id: 'invitation123' })
- const response = await DELETE(req, { params })
-
- expect(response).toBeInstanceOf(NextResponse)
- const data = await response.json()
- expect(response.status).toBe(400)
- expect(data).toEqual({ error: 'Can only delete pending invitations' })
- })
-
- it('should successfully delete pending invitation when user has admin access', async () => {
- vi.mocked(getSession).mockResolvedValue(mockSession)
-
- // Mock invitation found
- const mockQuery = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn((callback: (rows: any[]) => any) => {
- // Return the first invitation from the array
- return Promise.resolve(callback([mockInvitation]))
- }),
- }
- vi.mocked(db.select).mockReturnValue(mockQuery as any)
-
- // Mock user has admin access
- vi.mocked(hasWorkspaceAdminAccess).mockResolvedValue(true)
-
- // Mock successful deletion
- const mockDelete = {
- where: vi.fn().mockResolvedValue({ rowCount: 1 }),
- }
- vi.mocked(db.delete).mockReturnValue(mockDelete as any)
-
- const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
- method: 'DELETE',
- })
-
- const params = Promise.resolve({ id: 'invitation123' })
- const response = await DELETE(req, { params })
-
- expect(response).toBeInstanceOf(NextResponse)
- const data = await response.json()
- expect(response.status).toBe(200)
- expect(data).toEqual({ success: true })
- expect(db.delete).toHaveBeenCalledWith(workspaceInvitation)
- expect(mockDelete.where).toHaveBeenCalled()
- })
-
- it('should return 500 when database error occurs', async () => {
- vi.mocked(getSession).mockResolvedValue(mockSession)
-
- // Mock database error
- const mockQuery = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockRejectedValue(new Error('Database connection failed')),
- }
- vi.mocked(db.select).mockReturnValue(mockQuery as any)
-
- const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
- method: 'DELETE',
- })
-
- const params = Promise.resolve({ id: 'invitation123' })
- const response = await DELETE(req, { params })
-
- expect(response).toBeInstanceOf(NextResponse)
- const data = await response.json()
- expect(response.status).toBe(500)
- expect(data).toEqual({ error: 'Failed to delete invitation' })
- })
-})
diff --git a/apps/sim/app/api/workspaces/invitations/[id]/route.ts b/apps/sim/app/api/workspaces/invitations/[id]/route.ts
deleted file mode 100644
index 27d0dae84b..0000000000
--- a/apps/sim/app/api/workspaces/invitations/[id]/route.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { getSession } from '@/lib/auth'
-import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
-import { db } from '@/db'
-import { workspaceInvitation } from '@/db/schema'
-
-// DELETE /api/workspaces/invitations/[id] - Delete a workspace invitation
-export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
- const { id } = await params
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- try {
- // Get the invitation to delete
- const invitation = await db
- .select({
- id: workspaceInvitation.id,
- workspaceId: workspaceInvitation.workspaceId,
- email: workspaceInvitation.email,
- inviterId: workspaceInvitation.inviterId,
- status: workspaceInvitation.status,
- })
- .from(workspaceInvitation)
- .where(eq(workspaceInvitation.id, id))
- .then((rows) => rows[0])
-
- if (!invitation) {
- return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
- }
-
- // Check if current user has admin access to the workspace
- const hasAdminAccess = await hasWorkspaceAdminAccess(session.user.id, invitation.workspaceId)
-
- if (!hasAdminAccess) {
- return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
- }
-
- // Only allow deleting pending invitations
- if (invitation.status !== 'pending') {
- return NextResponse.json({ error: 'Can only delete pending invitations' }, { status: 400 })
- }
-
- // Delete the invitation
- await db.delete(workspaceInvitation).where(eq(workspaceInvitation.id, id))
-
- return NextResponse.json({ success: true })
- } catch (error) {
- console.error('Error deleting workspace invitation:', error)
- return NextResponse.json({ error: 'Failed to delete invitation' }, { status: 500 })
- }
-}
diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts
new file mode 100644
index 0000000000..fb6831a0f6
--- /dev/null
+++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts
@@ -0,0 +1,405 @@
+import { NextRequest } from 'next/server'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils'
+
+/**
+ * Tests for workspace invitation by ID API route
+ * Tests GET (details + token acceptance), DELETE (cancellation)
+ *
+ * @vitest-environment node
+ */
+
+describe('Workspace Invitation [invitationId] API Route', () => {
+ const mockUser = {
+ id: 'user-123',
+ email: 'test@example.com',
+ name: 'Test User',
+ }
+
+ const mockWorkspace = {
+ id: 'workspace-456',
+ name: 'Test Workspace',
+ }
+
+ const mockInvitation = {
+ id: 'invitation-789',
+ workspaceId: 'workspace-456',
+ email: 'invited@example.com',
+ inviterId: 'inviter-321',
+ status: 'pending',
+ token: 'token-abc123',
+ permissions: 'read',
+ expiresAt: new Date(Date.now() + 86400000), // 1 day from now
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ }
+
+ let mockDbResults: any[] = []
+ let mockGetSession: any
+ let mockHasWorkspaceAdminAccess: any
+ let mockTransaction: any
+
+ beforeEach(async () => {
+ vi.resetModules()
+ vi.resetAllMocks()
+
+ mockDbResults = []
+ mockConsoleLogger()
+ mockAuth(mockUser)
+
+ vi.doMock('crypto', () => ({
+ randomUUID: vi.fn().mockReturnValue('mock-uuid-1234'),
+ }))
+
+ mockGetSession = vi.fn()
+ vi.doMock('@/lib/auth', () => ({
+ getSession: mockGetSession,
+ }))
+
+ mockHasWorkspaceAdminAccess = vi.fn()
+ vi.doMock('@/lib/permissions/utils', () => ({
+ hasWorkspaceAdminAccess: mockHasWorkspaceAdminAccess,
+ }))
+
+ vi.doMock('@/lib/env', () => ({
+ env: {
+ NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
+ },
+ }))
+
+ mockTransaction = vi.fn()
+ const mockDbChain = {
+ select: vi.fn().mockReturnThis(),
+ from: vi.fn().mockReturnThis(),
+ where: vi.fn().mockReturnThis(),
+ then: vi.fn().mockImplementation((callback: any) => {
+ const result = mockDbResults.shift() || []
+ return callback ? callback(result) : Promise.resolve(result)
+ }),
+ insert: vi.fn().mockReturnThis(),
+ values: vi.fn().mockResolvedValue(undefined),
+ update: vi.fn().mockReturnThis(),
+ set: vi.fn().mockReturnThis(),
+ delete: vi.fn().mockReturnThis(),
+ transaction: mockTransaction,
+ }
+
+ vi.doMock('@/db', () => ({
+ db: mockDbChain,
+ }))
+
+ vi.doMock('@/db/schema', () => ({
+ workspaceInvitation: {
+ id: 'id',
+ workspaceId: 'workspaceId',
+ email: 'email',
+ inviterId: 'inviterId',
+ status: 'status',
+ token: 'token',
+ permissions: 'permissions',
+ expiresAt: 'expiresAt',
+ },
+ workspace: {
+ id: 'id',
+ name: 'name',
+ },
+ user: {
+ id: 'id',
+ email: 'email',
+ },
+ permissions: {
+ id: 'id',
+ entityType: 'entityType',
+ entityId: 'entityId',
+ userId: 'userId',
+ permissionType: 'permissionType',
+ },
+ }))
+
+ vi.doMock('drizzle-orm', () => ({
+ eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
+ and: vi.fn((...args) => ({ type: 'and', args })),
+ }))
+ })
+
+ describe('GET /api/workspaces/invitations/[invitationId]', () => {
+ it('should return invitation details when called without token', async () => {
+ const { GET } = await import('./route')
+
+ mockGetSession.mockResolvedValue({ user: mockUser })
+
+ mockDbResults.push([mockInvitation])
+ mockDbResults.push([mockWorkspace])
+
+ const request = new NextRequest('http://localhost/api/workspaces/invitations/invitation-789')
+ const params = Promise.resolve({ invitationId: 'invitation-789' })
+
+ const response = await GET(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(200)
+ expect(data).toMatchObject({
+ id: 'invitation-789',
+ email: 'invited@example.com',
+ status: 'pending',
+ workspaceName: 'Test Workspace',
+ })
+ })
+
+ it('should redirect to login when unauthenticated with token', async () => {
+ const { GET } = await import('./route')
+
+ mockGetSession.mockResolvedValue(null)
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
+ )
+ const params = Promise.resolve({ invitationId: 'token-abc123' })
+
+ const response = await GET(request, { params })
+
+ expect(response.status).toBe(307)
+ expect(response.headers.get('location')).toBe(
+ 'https://test.sim.ai/invite/token-abc123?token=token-abc123'
+ )
+ })
+
+ it('should accept invitation when called with valid token', async () => {
+ const { GET } = await import('./route')
+
+ mockGetSession.mockResolvedValue({
+ user: { ...mockUser, email: 'invited@example.com' },
+ })
+
+ mockDbResults.push([mockInvitation])
+ mockDbResults.push([mockWorkspace])
+ mockDbResults.push([{ ...mockUser, email: 'invited@example.com' }])
+ mockDbResults.push([])
+
+ mockTransaction.mockImplementation(async (callback: any) => {
+ await callback({
+ insert: vi.fn().mockReturnThis(),
+ values: vi.fn().mockResolvedValue(undefined),
+ update: vi.fn().mockReturnThis(),
+ set: vi.fn().mockReturnThis(),
+ where: vi.fn().mockResolvedValue(undefined),
+ })
+ })
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
+ )
+ const params = Promise.resolve({ invitationId: 'token-abc123' })
+
+ const response = await GET(request, { params })
+
+ expect(response.status).toBe(307)
+ expect(response.headers.get('location')).toBe('https://test.sim.ai/workspace/workspace-456/w')
+ })
+
+ it('should redirect to error page when invitation expired', async () => {
+ const { GET } = await import('./route')
+
+ mockGetSession.mockResolvedValue({
+ user: { ...mockUser, email: 'invited@example.com' },
+ })
+
+ const expiredInvitation = {
+ ...mockInvitation,
+ expiresAt: new Date(Date.now() - 86400000), // 1 day ago
+ }
+
+ mockDbResults.push([expiredInvitation])
+ mockDbResults.push([mockWorkspace])
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
+ )
+ const params = Promise.resolve({ invitationId: 'token-abc123' })
+
+ const response = await GET(request, { params })
+
+ expect(response.status).toBe(307)
+ expect(response.headers.get('location')).toBe(
+ 'https://test.sim.ai/invite/invitation-789?error=expired'
+ )
+ })
+
+ it('should redirect to error page when email mismatch', async () => {
+ const { GET } = await import('./route')
+
+ mockGetSession.mockResolvedValue({
+ user: { ...mockUser, email: 'wrong@example.com' },
+ })
+
+ mockDbResults.push([mockInvitation])
+ mockDbResults.push([mockWorkspace])
+ mockDbResults.push([{ ...mockUser, email: 'wrong@example.com' }])
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
+ )
+ const params = Promise.resolve({ invitationId: 'token-abc123' })
+
+ const response = await GET(request, { params })
+
+ expect(response.status).toBe(307)
+ expect(response.headers.get('location')).toBe(
+ 'https://test.sim.ai/invite/invitation-789?error=email-mismatch'
+ )
+ })
+ })
+
+ describe('DELETE /api/workspaces/invitations/[invitationId]', () => {
+ it('should return 401 when user is not authenticated', async () => {
+ const { DELETE } = await import('./route')
+
+ mockGetSession.mockResolvedValue(null)
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/invitation-789',
+ {
+ method: 'DELETE',
+ }
+ )
+ const params = Promise.resolve({ invitationId: 'invitation-789' })
+
+ const response = await DELETE(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(401)
+ expect(data).toEqual({ error: 'Unauthorized' })
+ })
+
+ it('should return 404 when invitation does not exist', async () => {
+ const { DELETE } = await import('./route')
+
+ mockGetSession.mockResolvedValue({ user: mockUser })
+
+ mockDbResults.push([])
+
+ const request = new NextRequest('http://localhost/api/workspaces/invitations/non-existent', {
+ method: 'DELETE',
+ })
+ const params = Promise.resolve({ invitationId: 'non-existent' })
+
+ const response = await DELETE(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(404)
+ expect(data).toEqual({ error: 'Invitation not found' })
+ })
+
+ it('should return 403 when user lacks admin access', async () => {
+ const { DELETE } = await import('./route')
+
+ mockGetSession.mockResolvedValue({ user: mockUser })
+ mockHasWorkspaceAdminAccess.mockResolvedValue(false)
+
+ mockDbResults.push([mockInvitation])
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/invitation-789',
+ {
+ method: 'DELETE',
+ }
+ )
+ const params = Promise.resolve({ invitationId: 'invitation-789' })
+
+ const response = await DELETE(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(403)
+ expect(data).toEqual({ error: 'Insufficient permissions' })
+ expect(mockHasWorkspaceAdminAccess).toHaveBeenCalledWith('user-123', 'workspace-456')
+ })
+
+ it('should return 400 when trying to delete non-pending invitation', async () => {
+ const { DELETE } = await import('./route')
+
+ mockGetSession.mockResolvedValue({ user: mockUser })
+ mockHasWorkspaceAdminAccess.mockResolvedValue(true)
+
+ const acceptedInvitation = { ...mockInvitation, status: 'accepted' }
+ mockDbResults.push([acceptedInvitation])
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/invitation-789',
+ {
+ method: 'DELETE',
+ }
+ )
+ const params = Promise.resolve({ invitationId: 'invitation-789' })
+
+ const response = await DELETE(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(400)
+ expect(data).toEqual({ error: 'Can only delete pending invitations' })
+ })
+
+ it('should successfully delete pending invitation when user has admin access', async () => {
+ const { DELETE } = await import('./route')
+
+ mockGetSession.mockResolvedValue({ user: mockUser })
+ mockHasWorkspaceAdminAccess.mockResolvedValue(true)
+
+ mockDbResults.push([mockInvitation])
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/invitation-789',
+ {
+ method: 'DELETE',
+ }
+ )
+ const params = Promise.resolve({ invitationId: 'invitation-789' })
+
+ const response = await DELETE(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(200)
+ expect(data).toEqual({ success: true })
+ })
+
+ it('should return 500 when database error occurs', async () => {
+ vi.resetModules()
+
+ const mockErrorDb = {
+ select: vi.fn().mockReturnThis(),
+ from: vi.fn().mockReturnThis(),
+ where: vi.fn().mockReturnThis(),
+ then: vi.fn().mockRejectedValue(new Error('Database connection failed')),
+ }
+
+ vi.doMock('@/db', () => ({ db: mockErrorDb }))
+ vi.doMock('@/lib/auth', () => ({
+ getSession: vi.fn().mockResolvedValue({ user: mockUser }),
+ }))
+ vi.doMock('@/lib/permissions/utils', () => ({
+ hasWorkspaceAdminAccess: vi.fn(),
+ }))
+ vi.doMock('@/db/schema', () => ({
+ workspaceInvitation: { id: 'id' },
+ }))
+ vi.doMock('drizzle-orm', () => ({
+ eq: vi.fn(),
+ }))
+
+ const { DELETE } = await import('./route')
+
+ const request = new NextRequest(
+ 'http://localhost/api/workspaces/invitations/invitation-789',
+ {
+ method: 'DELETE',
+ }
+ )
+ const params = Promise.resolve({ invitationId: 'invitation-789' })
+
+ const response = await DELETE(request, { params })
+ const data = await response.json()
+
+ expect(response.status).toBe(500)
+ expect(data).toEqual({ error: 'Failed to delete invitation' })
+ })
+ })
+})
diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
new file mode 100644
index 0000000000..8e0878809e
--- /dev/null
+++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
@@ -0,0 +1,236 @@
+import { randomUUID } from 'crypto'
+import { and, eq } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import { getSession } from '@/lib/auth'
+import { env } from '@/lib/env'
+import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
+import { db } from '@/db'
+import {
+ permissions,
+ user,
+ type WorkspaceInvitationStatus,
+ workspace,
+ workspaceInvitation,
+} from '@/db/schema'
+
+// GET /api/workspaces/invitations/[invitationId] - Get invitation details OR accept via token
+export async function GET(
+ req: NextRequest,
+ { params }: { params: Promise<{ invitationId: string }> }
+) {
+ const { invitationId } = await params
+ const session = await getSession()
+ const token = req.nextUrl.searchParams.get('token')
+ const isAcceptFlow = !!token // If token is provided, this is an acceptance flow
+
+ if (!session?.user?.id) {
+ // For token-based acceptance flows, redirect to login
+ if (isAcceptFlow) {
+ return NextResponse.redirect(
+ new URL(
+ `/invite/${invitationId}?token=${token}`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ try {
+ const whereClause = token
+ ? eq(workspaceInvitation.token, token)
+ : eq(workspaceInvitation.id, invitationId)
+
+ const invitation = await db
+ .select()
+ .from(workspaceInvitation)
+ .where(whereClause)
+ .then((rows) => rows[0])
+
+ if (!invitation) {
+ return NextResponse.json({ error: 'Invitation not found or has expired' }, { status: 404 })
+ }
+
+ if (new Date() > new Date(invitation.expiresAt)) {
+ if (isAcceptFlow) {
+ return NextResponse.redirect(
+ new URL(
+ `/invite/${invitation.id}?error=expired`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+ return NextResponse.json({ error: 'Invitation has expired' }, { status: 400 })
+ }
+
+ const workspaceDetails = await db
+ .select()
+ .from(workspace)
+ .where(eq(workspace.id, invitation.workspaceId))
+ .then((rows) => rows[0])
+
+ if (!workspaceDetails) {
+ if (isAcceptFlow) {
+ return NextResponse.redirect(
+ new URL(
+ `/invite/${invitation.id}?error=workspace-not-found`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
+ }
+
+ if (isAcceptFlow) {
+ if (invitation.status !== ('pending' as WorkspaceInvitationStatus)) {
+ return NextResponse.redirect(
+ new URL(
+ `/invite/${invitation.id}?error=already-processed`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+
+ const userEmail = session.user.email.toLowerCase()
+ const invitationEmail = invitation.email.toLowerCase()
+
+ const userData = await db
+ .select()
+ .from(user)
+ .where(eq(user.id, session.user.id))
+ .then((rows) => rows[0])
+
+ if (!userData) {
+ return NextResponse.redirect(
+ new URL(
+ `/invite/${invitation.id}?error=user-not-found`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+
+ const isValidMatch = userEmail === invitationEmail
+
+ if (!isValidMatch) {
+ return NextResponse.redirect(
+ new URL(
+ `/invite/${invitation.id}?error=email-mismatch`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+
+ const existingPermission = await db
+ .select()
+ .from(permissions)
+ .where(
+ and(
+ eq(permissions.entityId, invitation.workspaceId),
+ eq(permissions.entityType, 'workspace'),
+ eq(permissions.userId, session.user.id)
+ )
+ )
+ .then((rows) => rows[0])
+
+ if (existingPermission) {
+ await db
+ .update(workspaceInvitation)
+ .set({
+ status: 'accepted' as WorkspaceInvitationStatus,
+ updatedAt: new Date(),
+ })
+ .where(eq(workspaceInvitation.id, invitation.id))
+
+ return NextResponse.redirect(
+ new URL(
+ `/workspace/${invitation.workspaceId}/w`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+
+ await db.transaction(async (tx) => {
+ await tx.insert(permissions).values({
+ id: randomUUID(),
+ entityType: 'workspace' as const,
+ entityId: invitation.workspaceId,
+ userId: session.user.id,
+ permissionType: invitation.permissions || 'read',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+
+ await tx
+ .update(workspaceInvitation)
+ .set({
+ status: 'accepted' as WorkspaceInvitationStatus,
+ updatedAt: new Date(),
+ })
+ .where(eq(workspaceInvitation.id, invitation.id))
+ })
+
+ return NextResponse.redirect(
+ new URL(
+ `/workspace/${invitation.workspaceId}/w`,
+ env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
+ )
+ )
+ }
+
+ return NextResponse.json({
+ ...invitation,
+ workspaceName: workspaceDetails.name,
+ })
+ } catch (error) {
+ console.error('Error fetching workspace invitation:', error)
+ return NextResponse.json({ error: 'Failed to fetch invitation details' }, { status: 500 })
+ }
+}
+
+// DELETE /api/workspaces/invitations/[invitationId] - Delete a workspace invitation
+export async function DELETE(
+ _req: NextRequest,
+ { params }: { params: Promise<{ invitationId: string }> }
+) {
+ const { invitationId } = await params
+ const session = await getSession()
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ try {
+ const invitation = await db
+ .select({
+ id: workspaceInvitation.id,
+ workspaceId: workspaceInvitation.workspaceId,
+ email: workspaceInvitation.email,
+ inviterId: workspaceInvitation.inviterId,
+ status: workspaceInvitation.status,
+ })
+ .from(workspaceInvitation)
+ .where(eq(workspaceInvitation.id, invitationId))
+ .then((rows) => rows[0])
+
+ if (!invitation) {
+ return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
+ }
+
+ const hasAdminAccess = await hasWorkspaceAdminAccess(session.user.id, invitation.workspaceId)
+
+ if (!hasAdminAccess) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
+ }
+
+ if (invitation.status !== ('pending' as WorkspaceInvitationStatus)) {
+ return NextResponse.json({ error: 'Can only delete pending invitations' }, { status: 400 })
+ }
+
+ await db.delete(workspaceInvitation).where(eq(workspaceInvitation.id, invitationId))
+
+ return NextResponse.json({ success: true })
+ } catch (error) {
+ console.error('Error deleting workspace invitation:', error)
+ return NextResponse.json({ error: 'Failed to delete invitation' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/api/workspaces/invitations/accept/route.ts b/apps/sim/app/api/workspaces/invitations/accept/route.ts
deleted file mode 100644
index 0903aca8c1..0000000000
--- a/apps/sim/app/api/workspaces/invitations/accept/route.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import { randomUUID } from 'crypto'
-import { and, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { getSession } from '@/lib/auth'
-import { env } from '@/lib/env'
-import { db } from '@/db'
-import { permissions, user, workspace, workspaceInvitation } from '@/db/schema'
-
-// Accept an invitation via token
-export async function GET(req: NextRequest) {
- const token = req.nextUrl.searchParams.get('token')
-
- if (!token) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=missing-token',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- const session = await getSession()
-
- if (!session?.user?.id) {
- // No need to encode API URL as callback, just redirect to invite page
- // The middleware will handle proper login flow and return to invite page
- return NextResponse.redirect(
- new URL(`/invite/${token}?token=${token}`, env.NEXT_PUBLIC_APP_URL || 'https://sim.ai')
- )
- }
-
- try {
- // Find the invitation by token
- const invitation = await db
- .select()
- .from(workspaceInvitation)
- .where(eq(workspaceInvitation.token, token))
- .then((rows) => rows[0])
-
- if (!invitation) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=invalid-token',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Check if invitation has expired
- if (new Date() > new Date(invitation.expiresAt)) {
- return NextResponse.redirect(
- new URL('/invite/invite-error?reason=expired', env.NEXT_PUBLIC_APP_URL || 'https://sim.ai')
- )
- }
-
- // Check if invitation is already accepted
- if (invitation.status !== 'pending') {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=already-processed',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Get the user's email from the session
- const userEmail = session.user.email.toLowerCase()
- const invitationEmail = invitation.email.toLowerCase()
-
- // Get user data to check email verification status and for error messages
- const userData = await db
- .select()
- .from(user)
- .where(eq(user.id, session.user.id))
- .then((rows) => rows[0])
-
- if (!userData) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=user-not-found',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Check if the logged-in user's email matches the invitation
- const isValidMatch = userEmail === invitationEmail
-
- if (!isValidMatch) {
- return NextResponse.redirect(
- new URL(
- `/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${invitation.email}, but you're logged in as ${userData.email}`)}`,
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Get the workspace details
- const workspaceDetails = await db
- .select()
- .from(workspace)
- .where(eq(workspace.id, invitation.workspaceId))
- .then((rows) => rows[0])
-
- if (!workspaceDetails) {
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=workspace-not-found',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Check if user already has permissions for this workspace
- const existingPermission = await db
- .select()
- .from(permissions)
- .where(
- and(
- eq(permissions.entityId, invitation.workspaceId),
- eq(permissions.entityType, 'workspace'),
- eq(permissions.userId, session.user.id)
- )
- )
- .then((rows) => rows[0])
-
- if (existingPermission) {
- // User already has permissions, just mark the invitation as accepted and redirect
- await db
- .update(workspaceInvitation)
- .set({
- status: 'accepted',
- updatedAt: new Date(),
- })
- .where(eq(workspaceInvitation.id, invitation.id))
-
- return NextResponse.redirect(
- new URL(
- `/workspace/${invitation.workspaceId}/w`,
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-
- // Add user permissions and mark invitation as accepted in a transaction
- await db.transaction(async (tx) => {
- // Create permissions for the user
- await tx.insert(permissions).values({
- id: randomUUID(),
- entityType: 'workspace' as const,
- entityId: invitation.workspaceId,
- userId: session.user.id,
- permissionType: invitation.permissions || 'read',
- createdAt: new Date(),
- updatedAt: new Date(),
- })
-
- // Mark invitation as accepted
- await tx
- .update(workspaceInvitation)
- .set({
- status: 'accepted',
- updatedAt: new Date(),
- })
- .where(eq(workspaceInvitation.id, invitation.id))
- })
-
- // Redirect to the workspace
- return NextResponse.redirect(
- new URL(`/workspace/${invitation.workspaceId}/w`, env.NEXT_PUBLIC_APP_URL || 'https://sim.ai')
- )
- } catch (error) {
- console.error('Error accepting invitation:', error)
- return NextResponse.redirect(
- new URL(
- '/invite/invite-error?reason=server-error',
- env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- )
- )
- }
-}
diff --git a/apps/sim/app/api/workspaces/invitations/details/route.ts b/apps/sim/app/api/workspaces/invitations/details/route.ts
deleted file mode 100644
index 971c732c4a..0000000000
--- a/apps/sim/app/api/workspaces/invitations/details/route.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { getSession } from '@/lib/auth'
-import { db } from '@/db'
-import { workspace, workspaceInvitation } from '@/db/schema'
-
-// Get invitation details by token
-export async function GET(req: NextRequest) {
- const token = req.nextUrl.searchParams.get('token')
-
- if (!token) {
- return NextResponse.json({ error: 'Token is required' }, { status: 400 })
- }
-
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- try {
- // Find the invitation by token
- const invitation = await db
- .select()
- .from(workspaceInvitation)
- .where(eq(workspaceInvitation.token, token))
- .then((rows) => rows[0])
-
- if (!invitation) {
- return NextResponse.json({ error: 'Invitation not found or has expired' }, { status: 404 })
- }
-
- // Check if invitation has expired
- if (new Date() > new Date(invitation.expiresAt)) {
- return NextResponse.json({ error: 'Invitation has expired' }, { status: 400 })
- }
-
- // Get workspace details
- const workspaceDetails = await db
- .select()
- .from(workspace)
- .where(eq(workspace.id, invitation.workspaceId))
- .then((rows) => rows[0])
-
- if (!workspaceDetails) {
- return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
- }
-
- // Return the invitation with workspace name
- return NextResponse.json({
- ...invitation,
- workspaceName: workspaceDetails.name,
- })
- } catch (error) {
- console.error('Error fetching workspace invitation:', error)
- return NextResponse.json({ error: 'Failed to fetch invitation details' }, { status: 500 })
- }
-}
diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts
index a5ecbf338a..5889a431e6 100644
--- a/apps/sim/app/api/workspaces/invitations/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/route.ts
@@ -13,6 +13,7 @@ import {
permissions,
type permissionTypeEnum,
user,
+ type WorkspaceInvitationStatus,
workspace,
workspaceInvitation,
} from '@/db/schema'
@@ -162,7 +163,7 @@ export async function POST(req: NextRequest) {
and(
eq(workspaceInvitation.workspaceId, workspaceId),
eq(workspaceInvitation.email, email),
- eq(workspaceInvitation.status, 'pending')
+ eq(workspaceInvitation.status, 'pending' as WorkspaceInvitationStatus)
)
)
.then((rows) => rows[0])
@@ -189,7 +190,7 @@ export async function POST(req: NextRequest) {
email,
inviterId: session.user.id,
role,
- status: 'pending',
+ status: 'pending' as WorkspaceInvitationStatus,
token,
permissions: permission,
expiresAt,
@@ -205,6 +206,7 @@ export async function POST(req: NextRequest) {
to: email,
inviterName: session.user.name || session.user.email || 'A user',
workspaceName: workspaceDetails.name,
+ invitationId: invitationData.id,
token: token,
})
@@ -220,17 +222,19 @@ async function sendInvitationEmail({
to,
inviterName,
workspaceName,
+ invitationId,
token,
}: {
to: string
inviterName: string
workspaceName: string
+ invitationId: string
token: string
}) {
try {
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
- // Always use the client-side invite route with token parameter
- const invitationLink = `${baseUrl}/invite/${token}?token=${token}`
+ // Use invitation ID in path, token in query parameter for security
+ const invitationLink = `${baseUrl}/invite/${invitationId}?token=${token}`
const emailHtml = await render(
WorkspaceInvitationEmail({
diff --git a/apps/sim/app/chat/[subdomain]/chat-client.css b/apps/sim/app/chat/[subdomain]/chat.css
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/chat-client.css
rename to apps/sim/app/chat/[subdomain]/chat.css
diff --git a/apps/sim/app/chat/[subdomain]/chat-client.tsx b/apps/sim/app/chat/[subdomain]/chat.tsx
similarity index 99%
rename from apps/sim/app/chat/[subdomain]/chat-client.tsx
rename to apps/sim/app/chat/[subdomain]/chat.tsx
index 482f9e3098..941b65ad29 100644
--- a/apps/sim/app/chat/[subdomain]/chat-client.tsx
+++ b/apps/sim/app/chat/[subdomain]/chat.tsx
@@ -15,8 +15,8 @@ import {
EmailAuth,
PasswordAuth,
VoiceInterface,
-} from '@/app/chat/[subdomain]/components'
-import { useAudioStreaming, useChatStreaming } from '@/app/chat/[subdomain]/hooks'
+} from '@/app/chat/components'
+import { useAudioStreaming, useChatStreaming } from '@/app/chat/hooks'
const logger = createLogger('ChatClient')
diff --git a/apps/sim/app/chat/[subdomain]/layout.tsx b/apps/sim/app/chat/[subdomain]/layout.tsx
index d16a72e852..de843b8d4a 100644
--- a/apps/sim/app/chat/[subdomain]/layout.tsx
+++ b/apps/sim/app/chat/[subdomain]/layout.tsx
@@ -1,7 +1,7 @@
'use client'
import { ThemeProvider } from 'next-themes'
-import './chat-client.css'
+import './chat.css'
export default function ChatLayout({ children }: { children: React.ReactNode }) {
return (
diff --git a/apps/sim/app/chat/[subdomain]/page.tsx b/apps/sim/app/chat/[subdomain]/page.tsx
index 52162b2c9e..7a005a4dd5 100644
--- a/apps/sim/app/chat/[subdomain]/page.tsx
+++ b/apps/sim/app/chat/[subdomain]/page.tsx
@@ -1,4 +1,4 @@
-import ChatClient from '@/app/chat/[subdomain]/chat-client'
+import ChatClient from '@/app/chat/[subdomain]/chat'
export default async function ChatPage({ params }: { params: Promise<{ subdomain: string }> }) {
const { subdomain } = await params
diff --git a/apps/sim/app/chat/[subdomain]/components/auth/email/email-auth.tsx b/apps/sim/app/chat/components/auth/email/email-auth.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/auth/email/email-auth.tsx
rename to apps/sim/app/chat/components/auth/email/email-auth.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/auth/password/password-auth.tsx b/apps/sim/app/chat/components/auth/password/password-auth.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/auth/password/password-auth.tsx
rename to apps/sim/app/chat/components/auth/password/password-auth.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/components/header-links/header-links.tsx b/apps/sim/app/chat/components/components/header-links/header-links.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/components/header-links/header-links.tsx
rename to apps/sim/app/chat/components/components/header-links/header-links.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/components/markdown-renderer/markdown-renderer.tsx b/apps/sim/app/chat/components/components/markdown-renderer/markdown-renderer.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/components/markdown-renderer/markdown-renderer.tsx
rename to apps/sim/app/chat/components/components/markdown-renderer/markdown-renderer.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/error-state/error-state.tsx b/apps/sim/app/chat/components/error-state/error-state.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/error-state/error-state.tsx
rename to apps/sim/app/chat/components/error-state/error-state.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/header/header.tsx b/apps/sim/app/chat/components/header/header.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/header/header.tsx
rename to apps/sim/app/chat/components/header/header.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/index.ts b/apps/sim/app/chat/components/index.ts
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/index.ts
rename to apps/sim/app/chat/components/index.ts
diff --git a/apps/sim/app/chat/[subdomain]/components/input/input.tsx b/apps/sim/app/chat/components/input/input.tsx
similarity index 99%
rename from apps/sim/app/chat/[subdomain]/components/input/input.tsx
rename to apps/sim/app/chat/components/input/input.tsx
index ef0fab9742..7aff2baf47 100644
--- a/apps/sim/app/chat/[subdomain]/components/input/input.tsx
+++ b/apps/sim/app/chat/components/input/input.tsx
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'
import { motion } from 'framer-motion'
import { Send, Square } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
-import { VoiceInput } from '@/app/chat/[subdomain]/components/input/voice-input'
+import { VoiceInput } from '@/app/chat/components/input/voice-input'
const PLACEHOLDER_MOBILE = 'Enter a message'
const PLACEHOLDER_DESKTOP = 'Enter a message or click the mic to speak'
diff --git a/apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx b/apps/sim/app/chat/components/input/voice-input.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx
rename to apps/sim/app/chat/components/input/voice-input.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/loading-state/loading-state.tsx b/apps/sim/app/chat/components/loading-state/loading-state.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/loading-state/loading-state.tsx
rename to apps/sim/app/chat/components/loading-state/loading-state.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/message-container/message-container.tsx b/apps/sim/app/chat/components/message-container/message-container.tsx
similarity index 96%
rename from apps/sim/app/chat/[subdomain]/components/message-container/message-container.tsx
rename to apps/sim/app/chat/components/message-container/message-container.tsx
index 286d98cc90..8695878e98 100644
--- a/apps/sim/app/chat/[subdomain]/components/message-container/message-container.tsx
+++ b/apps/sim/app/chat/components/message-container/message-container.tsx
@@ -3,10 +3,7 @@
import { memo, type RefObject } from 'react'
import { ArrowDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
-import {
- type ChatMessage,
- ClientChatMessage,
-} from '@/app/chat/[subdomain]/components/message/message'
+import { type ChatMessage, ClientChatMessage } from '@/app/chat/components/message/message'
interface ChatMessageContainerProps {
messages: ChatMessage[]
diff --git a/apps/sim/app/chat/[subdomain]/components/message/components/markdown-renderer.tsx b/apps/sim/app/chat/components/message/components/markdown-renderer.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/message/components/markdown-renderer.tsx
rename to apps/sim/app/chat/components/message/components/markdown-renderer.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/message/message.tsx b/apps/sim/app/chat/components/message/message.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/message/message.tsx
rename to apps/sim/app/chat/components/message/message.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/voice-interface/components/particles.tsx b/apps/sim/app/chat/components/voice-interface/components/particles.tsx
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/components/voice-interface/components/particles.tsx
rename to apps/sim/app/chat/components/voice-interface/components/particles.tsx
diff --git a/apps/sim/app/chat/[subdomain]/components/voice-interface/voice-interface.tsx b/apps/sim/app/chat/components/voice-interface/voice-interface.tsx
similarity index 99%
rename from apps/sim/app/chat/[subdomain]/components/voice-interface/voice-interface.tsx
rename to apps/sim/app/chat/components/voice-interface/voice-interface.tsx
index 15efe748fb..96c2300a10 100644
--- a/apps/sim/app/chat/[subdomain]/components/voice-interface/voice-interface.tsx
+++ b/apps/sim/app/chat/components/voice-interface/voice-interface.tsx
@@ -5,7 +5,7 @@ import { Mic, MicOff, Phone } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
-import { ParticlesVisualization } from '@/app/chat/[subdomain]/components/voice-interface/components/particles'
+import { ParticlesVisualization } from '@/app/chat/components/voice-interface/components/particles'
const logger = createLogger('VoiceInterface')
diff --git a/apps/sim/app/chat/[subdomain]/hooks/index.ts b/apps/sim/app/chat/hooks/index.ts
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/hooks/index.ts
rename to apps/sim/app/chat/hooks/index.ts
diff --git a/apps/sim/app/chat/[subdomain]/hooks/use-audio-streaming.ts b/apps/sim/app/chat/hooks/use-audio-streaming.ts
similarity index 100%
rename from apps/sim/app/chat/[subdomain]/hooks/use-audio-streaming.ts
rename to apps/sim/app/chat/hooks/use-audio-streaming.ts
diff --git a/apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts b/apps/sim/app/chat/hooks/use-chat-streaming.ts
similarity index 99%
rename from apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts
rename to apps/sim/app/chat/hooks/use-chat-streaming.ts
index 9bad3adf1e..b8ad400a0d 100644
--- a/apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts
+++ b/apps/sim/app/chat/hooks/use-chat-streaming.ts
@@ -2,7 +2,7 @@
import { useRef, useState } from 'react'
import { createLogger } from '@/lib/logs/console/logger'
-import type { ChatMessage } from '@/app/chat/[subdomain]/components/message/message'
+import type { ChatMessage } from '@/app/chat/components/message/message'
// No longer need complex output extraction - backend handles this
import type { ExecutionResult } from '@/executor/types'
diff --git a/apps/sim/app/invite/[id]/invite.tsx b/apps/sim/app/invite/[id]/invite.tsx
index 54ac6d9a64..5afe3d625f 100644
--- a/apps/sim/app/invite/[id]/invite.tsx
+++ b/apps/sim/app/invite/[id]/invite.tsx
@@ -1,16 +1,13 @@
'use client'
import { useEffect, useState } from 'react'
-import { AlertCircle, CheckCircle2, Mail, UserPlus, Users2 } from 'lucide-react'
-import Image from 'next/image'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
-import { Button } from '@/components/ui/button'
-import { LoadingAgent } from '@/components/ui/loading-agent'
import { client, useSession } from '@/lib/auth-client'
-import { useBrandConfig } from '@/lib/branding/branding'
import { createLogger } from '@/lib/logs/console/logger'
+import { getErrorMessage } from '@/app/invite/[id]/utils'
+import { InviteLayout, InviteStatusCard } from '@/app/invite/components'
-const logger = createLogger('InviteByIDAPI')
+const logger = createLogger('InviteById')
export default function Invite() {
const router = useRouter()
@@ -18,7 +15,6 @@ export default function Invite() {
const inviteId = params.id as string
const searchParams = useSearchParams()
const { data: session, isPending } = useSession()
- const brandConfig = useBrandConfig()
const [invitationDetails, setInvitationDetails] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
@@ -28,12 +24,18 @@ export default function Invite() {
const [token, setToken] = useState(null)
const [invitationType, setInvitationType] = useState<'organization' | 'workspace'>('workspace')
- // Check if this is a new user vs. existing user and get token from query
useEffect(() => {
+ const errorReason = searchParams.get('error')
+
+ if (errorReason) {
+ setError(getErrorMessage(errorReason))
+ setIsLoading(false)
+ return
+ }
+
const isNew = searchParams.get('new') === 'true'
setIsNewUser(isNew)
- // Get token from URL or use inviteId as token
const tokenFromQuery = searchParams.get('token')
const effectiveToken = tokenFromQuery || inviteId
@@ -43,20 +45,16 @@ export default function Invite() {
}
}, [searchParams, inviteId])
- // Auto-fetch invitation details when logged in
useEffect(() => {
if (!session?.user || !token) return
async function fetchInvitationDetails() {
setIsLoading(true)
try {
- // First try to fetch workspace invitation details
- const workspaceInviteResponse = await fetch(
- `/api/workspaces/invitations/details?token=${token}`,
- {
- method: 'GET',
- }
- )
+ // Fetch invitation details using the invitation ID from the URL path
+ const workspaceInviteResponse = await fetch(`/api/workspaces/invitations/${inviteId}`, {
+ method: 'GET',
+ })
if (workspaceInviteResponse.ok) {
const data = await workspaceInviteResponse.json()
@@ -70,7 +68,6 @@ export default function Invite() {
return
}
- // If workspace invitation not found, try organization invitation
try {
const { data } = await client.organization.getInvitation({
query: { id: inviteId },
@@ -84,7 +81,6 @@ export default function Invite() {
name: data.organizationName || 'an organization',
})
- // Get organization details
if (data.organizationId) {
const orgResponse = await client.organization.getFullOrganization({
query: { organizationId: data.organizationId },
@@ -101,7 +97,6 @@ export default function Invite() {
throw new Error('Invitation not found or has expired')
}
} catch (_err) {
- // If neither workspace nor organization invitation is found
throw new Error('Invitation not found or has expired')
}
} catch (err: any) {
@@ -115,22 +110,19 @@ export default function Invite() {
fetchInvitationDetails()
}, [session?.user, inviteId, token])
- // Handle invitation acceptance
const handleAcceptInvitation = async () => {
if (!session?.user) return
setIsAccepting(true)
if (invitationType === 'workspace') {
- window.location.href = `/api/workspaces/invitations/accept?token=${encodeURIComponent(token || '')}`
+ window.location.href = `/api/workspaces/invitations/${encodeURIComponent(inviteId)}?token=${encodeURIComponent(token || '')}`
} else {
try {
- // For organization invites, use the client API
const response = await client.organization.acceptInvitation({
invitationId: inviteId,
})
- // Set the active organization to the one just joined
const orgId =
response.data?.invitation.organizationId || invitationDetails?.data?.organizationId
@@ -142,7 +134,6 @@ export default function Invite() {
setAccepted(true)
- // Redirect to workspace after a brief delay
setTimeout(() => {
router.push('/workspace')
}, 2000)
@@ -155,281 +146,135 @@ export default function Invite() {
}
}
- // Prepare the callback URL - this ensures after login, user returns to invite page
const getCallbackUrl = () => {
return `/invite/${inviteId}${token && token !== inviteId ? `?token=${token}` : ''}`
}
- // Show login/signup prompt if not logged in
if (!session?.user && !isPending) {
const callbackUrl = encodeURIComponent(getCallbackUrl())
return (
-
-
-
-
-
-
-
-
-
-
-
- You've been invited!
-
-
-
- {isNewUser
+
+
-
-
- {isNewUser ? (
- <>
- router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true`)}
- >
- Create an account
-
- router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`)}
- >
- I already have an account
-
- >
- ) : (
- <>
- router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`)}
- >
- Sign in
-
-
- router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true&new=true`)
- }
- >
- Create an account
-
- >
- )}
-
- router.push('/')}
- >
- Return to Home
-
-
-
-
-
-
+ : 'Sign in to your account to accept this invitation'
+ }
+ icon='userPlus'
+ actions={[
+ ...(isNewUser
+ ? [
+ {
+ label: 'Create an account',
+ onClick: () =>
+ router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true`),
+ },
+ {
+ label: 'I already have an account',
+ onClick: () =>
+ router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`),
+ variant: 'outline' as const,
+ },
+ ]
+ : [
+ {
+ label: 'Sign in',
+ onClick: () =>
+ router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`),
+ },
+ {
+ label: 'Create an account',
+ onClick: () =>
+ router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true&new=true`),
+ variant: 'outline' as const,
+ },
+ ]),
+ {
+ label: 'Return to Home',
+ onClick: () => router.push('/'),
+ },
+ ]}
+ />
+
)
}
- // Show loading state
if (isLoading || isPending) {
return (
-
-
-
-
-
-
Loading invitation...
-
-
-
+
+
+
)
}
- // Show error state
if (error) {
+ const errorReason = searchParams.get('error')
+ const isExpiredError = errorReason === 'expired'
+
return (
-
-
-
-
-
-
-
- Invitation Error
-
-
{error}
-
-
router.push('/')}
- >
- Return to Home
-
-
-
-
-
+
+ router.push('/'),
+ },
+ ]}
+ />
+
)
}
- // Show success state
if (accepted) {
return (
-
-
-
-
-
-
-
-
-
Welcome!
-
- You have successfully joined {invitationDetails?.name || 'the workspace'}. Redirecting
- to your workspace...
-
-
-
router.push('/')}
- >
- Return to Home
-
-
-
-
-
+
+ router.push('/'),
+ },
+ ]}
+ />
+
)
}
- // Show invitation details
return (
-
-
-
-
-
-
-
- {invitationType === 'organization' ? (
-
- ) : (
-
- )}
-
-
-
- {invitationType === 'organization' ? 'Organization Invitation' : 'Workspace Invitation'}
-
-
-
- You've been invited to join{' '}
-
- {invitationDetails?.name || `a ${invitationType}`}
-
- . Click accept below to join.
-
-
-
-
- {isAccepting ? (
- <>
-
- Accepting...
- >
- ) : (
- 'Accept Invitation'
- )}
-
- router.push('/')}
- >
- Return to Home
-
-
-
-
-
-
+
+ router.push('/'),
+ variant: 'ghost',
+ },
+ ]}
+ />
+
)
}
diff --git a/apps/sim/app/invite/[id]/utils.ts b/apps/sim/app/invite/[id]/utils.ts
new file mode 100644
index 0000000000..61c90f5867
--- /dev/null
+++ b/apps/sim/app/invite/[id]/utils.ts
@@ -0,0 +1,28 @@
+export function getErrorMessage(reason: string): string {
+ switch (reason) {
+ case 'missing-token':
+ return 'The invitation link is invalid or missing a required parameter.'
+ case 'invalid-token':
+ return 'The invitation link is invalid or has already been used.'
+ case 'expired':
+ return 'This invitation has expired. Please ask for a new invitation.'
+ case 'already-processed':
+ return 'This invitation has already been accepted or declined.'
+ case 'email-mismatch':
+ return 'This invitation was sent to a different email address. Please log in with the correct account or contact the person who invited you.'
+ case 'workspace-not-found':
+ return 'The workspace associated with this invitation could not be found.'
+ case 'user-not-found':
+ return 'Your user account could not be found. Please try logging out and logging back in.'
+ case 'already-member':
+ return 'You are already a member of this organization or workspace.'
+ case 'invalid-invitation':
+ return 'This invitation is invalid or no longer exists.'
+ case 'missing-invitation-id':
+ return 'The invitation link is missing required information. Please use the original invitation link.'
+ case 'server-error':
+ return 'An unexpected error occurred while processing your invitation. Please try again later.'
+ default:
+ return 'An unknown error occurred while processing your invitation.'
+ }
+}
diff --git a/apps/sim/app/invite/components/index.ts b/apps/sim/app/invite/components/index.ts
new file mode 100644
index 0000000000..e95425f803
--- /dev/null
+++ b/apps/sim/app/invite/components/index.ts
@@ -0,0 +1,2 @@
+export { InviteLayout } from './layout'
+export { InviteStatusCard } from './status-card'
diff --git a/apps/sim/app/invite/components/layout.tsx b/apps/sim/app/invite/components/layout.tsx
new file mode 100644
index 0000000000..a3d01e34d0
--- /dev/null
+++ b/apps/sim/app/invite/components/layout.tsx
@@ -0,0 +1,56 @@
+'use client'
+
+import Image from 'next/image'
+import { useBrandConfig } from '@/lib/branding/branding'
+import { GridPattern } from '@/app/(landing)/components/grid-pattern'
+
+interface InviteLayoutProps {
+ children: React.ReactNode
+}
+
+export function InviteLayout({ children }: InviteLayoutProps) {
+ const brandConfig = useBrandConfig()
+
+ return (
+
+ {/* Background pattern */}
+
+
+ {/* Content */}
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/sim/app/invite/components/status-card.tsx b/apps/sim/app/invite/components/status-card.tsx
new file mode 100644
index 0000000000..51f0ca691b
--- /dev/null
+++ b/apps/sim/app/invite/components/status-card.tsx
@@ -0,0 +1,121 @@
+'use client'
+
+import { CheckCircle2, Mail, RotateCcw, ShieldX, UserPlus, Users2 } from 'lucide-react'
+import { useRouter } from 'next/navigation'
+import { Button } from '@/components/ui/button'
+import { LoadingAgent } from '@/components/ui/loading-agent'
+
+interface InviteStatusCardProps {
+ type: 'login' | 'loading' | 'error' | 'success' | 'invitation'
+ title: string
+ description: string | React.ReactNode
+ icon?: 'userPlus' | 'mail' | 'users' | 'error' | 'success'
+ actions?: Array<{
+ label: string
+ onClick: () => void
+ variant?: 'default' | 'outline' | 'ghost'
+ disabled?: boolean
+ loading?: boolean
+ }>
+ isExpiredError?: boolean
+}
+
+const iconMap = {
+ userPlus: UserPlus,
+ mail: Mail,
+ users: Users2,
+ error: ShieldX,
+ success: CheckCircle2,
+}
+
+const iconColorMap = {
+ userPlus: 'text-[#701ffc]',
+ mail: 'text-[#701ffc]',
+ users: 'text-[#701ffc]',
+ error: 'text-red-500 dark:text-red-400',
+ success: 'text-green-500 dark:text-green-400',
+}
+
+const iconBgMap = {
+ userPlus: 'bg-[#701ffc]/10',
+ mail: 'bg-[#701ffc]/10',
+ users: 'bg-[#701ffc]/10',
+ error: 'bg-red-50 dark:bg-red-950/20',
+ success: 'bg-green-50 dark:bg-green-950/20',
+}
+
+export function InviteStatusCard({
+ type,
+ title,
+ description,
+ icon,
+ actions = [],
+ isExpiredError = false,
+}: InviteStatusCardProps) {
+ const router = useRouter()
+
+ if (type === 'loading') {
+ return (
+
+ )
+ }
+
+ const IconComponent = icon ? iconMap[icon] : null
+ const iconColor = icon ? iconColorMap[icon] : ''
+ const iconBg = icon ? iconBgMap[icon] : ''
+
+ return (
+
+ {IconComponent && (
+
+
+
+ )}
+
+
{title}
+
+
{description}
+
+
+ {isExpiredError && (
+ router.push('/')}
+ >
+
+ Request New Invitation
+
+ )}
+
+ {actions.map((action, index) => (
+
+ {action.loading ? (
+ <>
+
+ {action.label}...
+ >
+ ) : (
+ action.label
+ )}
+
+ ))}
+
+
+ )
+}
diff --git a/apps/sim/app/invite/invite-error/invite-error.tsx b/apps/sim/app/invite/invite-error/invite-error.tsx
deleted file mode 100644
index 8169091f21..0000000000
--- a/apps/sim/app/invite/invite-error/invite-error.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client'
-
-import { useEffect, useState } from 'react'
-import { RotateCcw, ShieldX } from 'lucide-react'
-import Image from 'next/image'
-import Link from 'next/link'
-import { useSearchParams } from 'next/navigation'
-import { Button } from '@/components/ui/button'
-import { useBrandConfig } from '@/lib/branding/branding'
-
-function getErrorMessage(reason: string, details?: string): string {
- switch (reason) {
- case 'missing-token':
- return 'The invitation link is invalid or missing a required parameter.'
- case 'invalid-token':
- return 'The invitation link is invalid or has already been used.'
- case 'expired':
- return 'This invitation has expired. Please ask for a new invitation.'
- case 'already-processed':
- return 'This invitation has already been accepted or declined.'
- case 'email-mismatch':
- return details
- ? details
- : 'This invitation was sent to a different email address than the one you are logged in with.'
- case 'workspace-not-found':
- return 'The workspace associated with this invitation could not be found.'
- case 'user-not-found':
- return 'Your user account could not be found. Please try logging out and logging back in.'
- case 'already-member':
- return 'You are already a member of this organization or workspace.'
- case 'invalid-invitation':
- return 'This invitation is invalid or no longer exists.'
- case 'missing-invitation-id':
- return 'The invitation link is missing required information. Please use the original invitation link.'
- case 'server-error':
- return 'An unexpected error occurred while processing your invitation. Please try again later.'
- default:
- return 'An unknown error occurred while processing your invitation.'
- }
-}
-
-export default function InviteError() {
- const searchParams = useSearchParams()
- const reason = searchParams?.get('reason') || 'unknown'
- const details = searchParams?.get('details')
- const [errorMessage, setErrorMessage] = useState('')
- const brandConfig = useBrandConfig()
-
- useEffect(() => {
- // Only set the error message on the client side
- setErrorMessage(getErrorMessage(reason, details || undefined))
- }, [reason, details])
-
- // Provide a fallback message for SSR
- const displayMessage = errorMessage || 'Loading error details...'
-
- const isExpiredError = reason === 'expired'
-
- return (
-
- {/* Logo */}
-
-
-
-
-
-
-
-
-
-
Invitation Error
-
-
- {displayMessage}
-
-
-
- {isExpiredError && (
-
-
-
- Request New Invitation
-
-
- )}
-
-
- Return to Home
-
-
-
-
-
-
- )
-}
diff --git a/apps/sim/app/invite/invite-error/page.tsx b/apps/sim/app/invite/invite-error/page.tsx
deleted file mode 100644
index 646f4a3d8e..0000000000
--- a/apps/sim/app/invite/invite-error/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import InviteError from '@/app/invite/invite-error/invite-error'
-
-export const dynamic = 'force-dynamic'
-
-export default function InviteErrorPage() {
- return
-}
diff --git a/apps/sim/app/unsubscribe/page.tsx b/apps/sim/app/unsubscribe/page.tsx
index 658de1ee5d..c9ca1f2693 100644
--- a/apps/sim/app/unsubscribe/page.tsx
+++ b/apps/sim/app/unsubscribe/page.tsx
@@ -1,401 +1,3 @@
-'use client'
+import Unsubscribe from './unsubscribe'
-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
- email: string
- token: string
- emailType: string
- isTransactional: boolean
- currentPreferences: {
- unsubscribeAll?: boolean
- unsubscribeMarketing?: boolean
- unsubscribeUpdates?: boolean
- unsubscribeNotifications?: boolean
- }
-}
-
-function UnsubscribeContent() {
- const searchParams = useSearchParams()
- const [loading, setLoading] = useState(true)
- const [data, setData] = useState(null)
- const [error, setError] = useState(null)
- const [processing, setProcessing] = useState(false)
- const [unsubscribed, setUnsubscribed] = useState(false)
- const brand = useBrandConfig()
-
- const email = searchParams.get('email')
- const token = searchParams.get('token')
-
- useEffect(() => {
- if (!email || !token) {
- setError('Missing email or token in URL')
- setLoading(false)
- return
- }
-
- // Validate the unsubscribe link
- fetch(
- `/api/users/me/settings/unsubscribe?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
- )
- .then((res) => res.json())
- .then((data) => {
- if (data.success) {
- setData(data)
- } else {
- setError(data.error || 'Invalid unsubscribe link')
- }
- })
- .catch(() => {
- setError('Failed to validate unsubscribe link')
- })
- .finally(() => {
- setLoading(false)
- })
- }, [email, token])
-
- const handleUnsubscribe = async (type: 'all' | 'marketing' | 'updates' | 'notifications') => {
- if (!email || !token) return
-
- setProcessing(true)
-
- try {
- const response = await fetch('/api/users/me/settings/unsubscribe', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- email,
- token,
- type,
- }),
- })
-
- const result = await response.json()
-
- if (result.success) {
- setUnsubscribed(true)
- // Update the data to reflect the change
- if (data) {
- // Type-safe property construction with validation
- const validTypes = ['all', 'marketing', 'updates', 'notifications'] as const
- if (validTypes.includes(type)) {
- if (type === 'all') {
- setData({
- ...data,
- currentPreferences: {
- ...data.currentPreferences,
- unsubscribeAll: true,
- },
- })
- } else {
- const propertyKey = `unsubscribe${type.charAt(0).toUpperCase()}${type.slice(1)}` as
- | 'unsubscribeMarketing'
- | 'unsubscribeUpdates'
- | 'unsubscribeNotifications'
- setData({
- ...data,
- currentPreferences: {
- ...data.currentPreferences,
- [propertyKey]: true,
- },
- })
- }
- }
- }
- } else {
- setError(result.error || 'Failed to unsubscribe')
- }
- } catch (error) {
- setError('Failed to process unsubscribe request')
- } finally {
- setProcessing(false)
- }
- }
-
- if (loading) {
- return (
-
-
-
-
-
-
-
- )
- }
-
- if (error) {
- return (
-
-
-
-
- Invalid Unsubscribe Link
-
- This unsubscribe link is invalid or has expired
-
-
-
-
-
-
-
This could happen if:
-
- The link is missing required parameters
- The link has expired or been used already
- The link was copied incorrectly
-
-
-
-
-
- window.open(
- `mailto:${brand.supportEmail}?subject=Unsubscribe%20Help&body=Hi%2C%20I%20need%20help%20unsubscribing%20from%20emails.%20My%20unsubscribe%20link%20is%20not%20working.`,
- '_blank'
- )
- }
- className='w-full bg-[var(--brand-primary-hex)] font-medium text-white shadow-sm transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'
- >
- Contact Support
-
- window.history.back()} variant='outline' className='w-full'>
- Go Back
-
-
-
-
-
-
-
- )
- }
-
- // Handle transactional emails
- if (data?.isTransactional) {
- return (
-
-
-
-
- Important Account Emails
-
- This email contains important information about your account
-
-
-
-
-
- Transactional emails like password resets, account confirmations,
- and security alerts cannot be unsubscribed from as they contain essential
- information for your account security and functionality.
-
-
-
-
-
- If you no longer wish to receive these emails, you can:
-
-
- Close your account entirely
- Contact our support team for assistance
-
-
-
-
-
- window.open(
- `mailto:${brand.supportEmail}?subject=Account%20Help&body=Hi%2C%20I%20need%20help%20with%20my%20account%20emails.`,
- '_blank'
- )
- }
- className='w-full bg-blue-600 text-white hover:bg-blue-700'
- >
- Contact Support
-
- window.close()} variant='outline' className='w-full'>
- Close
-
-
-
-
-
- )
- }
-
- if (unsubscribed) {
- return (
-
-
-
-
- Successfully Unsubscribed
-
- You have been unsubscribed from our emails. You will stop receiving emails within 48
- hours.
-
-
-
-
- If you change your mind, you can always update your email preferences in your account
- settings or contact us at{' '}
-
- {brand.supportEmail}
-
-
-
-
-
- )
- }
-
- return (
-
-
-
-
- We're sorry to see you go!
-
- We understand email preferences are personal. Choose which emails you'd like to
- stop receiving from Sim.
-
-
-
- Email: {data?.email}
-
-
-
-
-
-
handleUnsubscribe('all')}
- disabled={processing || data?.currentPreferences.unsubscribeAll}
- variant='destructive'
- className='w-full'
- >
- {processing ? (
-
- ) : data?.currentPreferences.unsubscribeAll ? (
-
- ) : null}
- {data?.currentPreferences.unsubscribeAll
- ? 'Unsubscribed from All Emails'
- : 'Unsubscribe from All Marketing Emails'}
-
-
-
- or choose specific types:
-
-
-
handleUnsubscribe('marketing')}
- disabled={
- processing ||
- data?.currentPreferences.unsubscribeAll ||
- data?.currentPreferences.unsubscribeMarketing
- }
- variant='outline'
- className='w-full'
- >
- {data?.currentPreferences.unsubscribeMarketing ? (
-
- ) : null}
- {data?.currentPreferences.unsubscribeMarketing
- ? 'Unsubscribed from Marketing'
- : 'Unsubscribe from Marketing Emails'}
-
-
-
handleUnsubscribe('updates')}
- disabled={
- processing ||
- data?.currentPreferences.unsubscribeAll ||
- data?.currentPreferences.unsubscribeUpdates
- }
- variant='outline'
- className='w-full'
- >
- {data?.currentPreferences.unsubscribeUpdates ? (
-
- ) : null}
- {data?.currentPreferences.unsubscribeUpdates
- ? 'Unsubscribed from Updates'
- : 'Unsubscribe from Product Updates'}
-
-
-
handleUnsubscribe('notifications')}
- disabled={
- processing ||
- data?.currentPreferences.unsubscribeAll ||
- data?.currentPreferences.unsubscribeNotifications
- }
- variant='outline'
- className='w-full'
- >
- {data?.currentPreferences.unsubscribeNotifications ? (
-
- ) : null}
- {data?.currentPreferences.unsubscribeNotifications
- ? 'Unsubscribed from Notifications'
- : 'Unsubscribe from Notifications'}
-
-
-
-
-
-
- Note: You'll continue receiving important account emails like
- password resets and security alerts.
-
-
-
-
- Questions? Contact us at{' '}
-
- {brand.supportEmail}
-
-
-
-
-
-
- )
-}
-
-export default function UnsubscribePage() {
- return (
-
-
-
-
-
-
-
- }
- >
-
-
- )
-}
+export default Unsubscribe
diff --git a/apps/sim/app/unsubscribe/unsubscribe.tsx b/apps/sim/app/unsubscribe/unsubscribe.tsx
new file mode 100644
index 0000000000..58de6e18b0
--- /dev/null
+++ b/apps/sim/app/unsubscribe/unsubscribe.tsx
@@ -0,0 +1,401 @@
+'use client'
+
+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
+ email: string
+ token: string
+ emailType: string
+ isTransactional: boolean
+ currentPreferences: {
+ unsubscribeAll?: boolean
+ unsubscribeMarketing?: boolean
+ unsubscribeUpdates?: boolean
+ unsubscribeNotifications?: boolean
+ }
+}
+
+function UnsubscribeContent() {
+ const searchParams = useSearchParams()
+ const [loading, setLoading] = useState(true)
+ const [data, setData] = useState(null)
+ const [error, setError] = useState(null)
+ const [processing, setProcessing] = useState(false)
+ const [unsubscribed, setUnsubscribed] = useState(false)
+ const brand = useBrandConfig()
+
+ const email = searchParams.get('email')
+ const token = searchParams.get('token')
+
+ useEffect(() => {
+ if (!email || !token) {
+ setError('Missing email or token in URL')
+ setLoading(false)
+ return
+ }
+
+ // Validate the unsubscribe link
+ fetch(
+ `/api/users/me/settings/unsubscribe?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
+ )
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.success) {
+ setData(data)
+ } else {
+ setError(data.error || 'Invalid unsubscribe link')
+ }
+ })
+ .catch(() => {
+ setError('Failed to validate unsubscribe link')
+ })
+ .finally(() => {
+ setLoading(false)
+ })
+ }, [email, token])
+
+ const handleUnsubscribe = async (type: 'all' | 'marketing' | 'updates' | 'notifications') => {
+ if (!email || !token) return
+
+ setProcessing(true)
+
+ try {
+ const response = await fetch('/api/users/me/settings/unsubscribe', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ email,
+ token,
+ type,
+ }),
+ })
+
+ const result = await response.json()
+
+ if (result.success) {
+ setUnsubscribed(true)
+ // Update the data to reflect the change
+ if (data) {
+ // Type-safe property construction with validation
+ const validTypes = ['all', 'marketing', 'updates', 'notifications'] as const
+ if (validTypes.includes(type)) {
+ if (type === 'all') {
+ setData({
+ ...data,
+ currentPreferences: {
+ ...data.currentPreferences,
+ unsubscribeAll: true,
+ },
+ })
+ } else {
+ const propertyKey = `unsubscribe${type.charAt(0).toUpperCase()}${type.slice(1)}` as
+ | 'unsubscribeMarketing'
+ | 'unsubscribeUpdates'
+ | 'unsubscribeNotifications'
+ setData({
+ ...data,
+ currentPreferences: {
+ ...data.currentPreferences,
+ [propertyKey]: true,
+ },
+ })
+ }
+ }
+ }
+ } else {
+ setError(result.error || 'Failed to unsubscribe')
+ }
+ } catch (error) {
+ setError('Failed to process unsubscribe request')
+ } finally {
+ setProcessing(false)
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+
+ if (error) {
+ return (
+
+
+
+
+ Invalid Unsubscribe Link
+
+ This unsubscribe link is invalid or has expired
+
+
+
+
+
+
+
This could happen if:
+
+ The link is missing required parameters
+ The link has expired or been used already
+ The link was copied incorrectly
+
+
+
+
+
+ window.open(
+ `mailto:${brand.supportEmail}?subject=Unsubscribe%20Help&body=Hi%2C%20I%20need%20help%20unsubscribing%20from%20emails.%20My%20unsubscribe%20link%20is%20not%20working.`,
+ '_blank'
+ )
+ }
+ className='w-full bg-[var(--brand-primary-hex)] font-medium text-white shadow-sm transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'
+ >
+ Contact Support
+
+ window.history.back()} variant='outline' className='w-full'>
+ Go Back
+
+
+
+
+
+
+
+ )
+ }
+
+ // Handle transactional emails
+ if (data?.isTransactional) {
+ return (
+
+
+
+
+ Important Account Emails
+
+ This email contains important information about your account
+
+
+
+
+
+ Transactional emails like password resets, account confirmations,
+ and security alerts cannot be unsubscribed from as they contain essential
+ information for your account security and functionality.
+
+
+
+
+
+ If you no longer wish to receive these emails, you can:
+
+
+ Close your account entirely
+ Contact our support team for assistance
+
+
+
+
+
+ window.open(
+ `mailto:${brand.supportEmail}?subject=Account%20Help&body=Hi%2C%20I%20need%20help%20with%20my%20account%20emails.`,
+ '_blank'
+ )
+ }
+ className='w-full bg-blue-600 text-white hover:bg-blue-700'
+ >
+ Contact Support
+
+ window.close()} variant='outline' className='w-full'>
+ Close
+
+
+
+
+
+ )
+ }
+
+ if (unsubscribed) {
+ return (
+
+
+
+
+ Successfully Unsubscribed
+
+ You have been unsubscribed from our emails. You will stop receiving emails within 48
+ hours.
+
+
+
+
+ If you change your mind, you can always update your email preferences in your account
+ settings or contact us at{' '}
+
+ {brand.supportEmail}
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ We're sorry to see you go!
+
+ We understand email preferences are personal. Choose which emails you'd like to
+ stop receiving from Sim.
+
+
+
+ Email: {data?.email}
+
+
+
+
+
+
handleUnsubscribe('all')}
+ disabled={processing || data?.currentPreferences.unsubscribeAll}
+ variant='destructive'
+ className='w-full'
+ >
+ {processing ? (
+
+ ) : data?.currentPreferences.unsubscribeAll ? (
+
+ ) : null}
+ {data?.currentPreferences.unsubscribeAll
+ ? 'Unsubscribed from All Emails'
+ : 'Unsubscribe from All Marketing Emails'}
+
+
+
+ or choose specific types:
+
+
+
handleUnsubscribe('marketing')}
+ disabled={
+ processing ||
+ data?.currentPreferences.unsubscribeAll ||
+ data?.currentPreferences.unsubscribeMarketing
+ }
+ variant='outline'
+ className='w-full'
+ >
+ {data?.currentPreferences.unsubscribeMarketing ? (
+
+ ) : null}
+ {data?.currentPreferences.unsubscribeMarketing
+ ? 'Unsubscribed from Marketing'
+ : 'Unsubscribe from Marketing Emails'}
+
+
+
handleUnsubscribe('updates')}
+ disabled={
+ processing ||
+ data?.currentPreferences.unsubscribeAll ||
+ data?.currentPreferences.unsubscribeUpdates
+ }
+ variant='outline'
+ className='w-full'
+ >
+ {data?.currentPreferences.unsubscribeUpdates ? (
+
+ ) : null}
+ {data?.currentPreferences.unsubscribeUpdates
+ ? 'Unsubscribed from Updates'
+ : 'Unsubscribe from Product Updates'}
+
+
+
handleUnsubscribe('notifications')}
+ disabled={
+ processing ||
+ data?.currentPreferences.unsubscribeAll ||
+ data?.currentPreferences.unsubscribeNotifications
+ }
+ variant='outline'
+ className='w-full'
+ >
+ {data?.currentPreferences.unsubscribeNotifications ? (
+
+ ) : null}
+ {data?.currentPreferences.unsubscribeNotifications
+ ? 'Unsubscribed from Notifications'
+ : 'Unsubscribe from Notifications'}
+
+
+
+
+
+
+ Note: You'll continue receiving important account emails like
+ password resets and security alerts.
+
+
+
+
+ Questions? Contact us at{' '}
+
+ {brand.supportEmail}
+
+
+
+
+
+
+ )
+}
+
+export default function Unsubscribe() {
+ return (
+
+
+
+
+
+
+
+ }
+ >
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx
index fbf4e5357e..90fa03a6df 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx
@@ -26,7 +26,7 @@ export default function Workflow() {
const fetchWorkflows = async () => {
try {
setLoading(true)
- const response = await fetch('/api/workflows/sync')
+ const response = await fetch('/api/workflows')
if (response.ok) {
const { data } = await response.json()
const workflowOptions: WorkflowOption[] = data.map((workflow: any) => ({
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx
index b506bfefac..9adb54cdf6 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx
@@ -502,7 +502,7 @@ export function FrozenCanvas({
setLoading(true)
setError(null)
- const response = await fetch(`/api/logs/${executionId}/frozen-canvas`)
+ const response = await fetch(`/api/logs/execution/${executionId}`)
if (!response.ok) {
throw new Error(`Failed to fetch frozen canvas data: ${response.statusText}`)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
index 68a46a7d9e..aa51ac7ef0 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
@@ -161,7 +161,7 @@ export default function Logs() {
Promise.all(
idsToFetch.map(async ({ id, merge }) => {
try {
- const res = await fetch(`/api/logs/by-id/${id}`, { signal: controller.signal })
+ const res = await fetch(`/api/logs/${id}`, { signal: controller.signal })
if (!res.ok) return
const body = await res.json()
const detailed = body?.data
@@ -216,7 +216,7 @@ export default function Logs() {
Promise.all(
idsToFetch.map(async ({ id, merge }) => {
try {
- const res = await fetch(`/api/logs/by-id/${id}`, { signal: controller.signal })
+ const res = await fetch(`/api/logs/${id}`, { signal: controller.signal })
if (!res.ok) return
const body = await res.json()
const detailed = body?.data
@@ -274,7 +274,7 @@ export default function Logs() {
Promise.all(
idsToFetch.map(async ({ id, merge }) => {
try {
- const res = await fetch(`/api/logs/by-id/${id}`, { signal: controller.signal })
+ const res = await fetch(`/api/logs/${id}`, { signal: controller.signal })
if (!res.ok) return
const body = await res.json()
const detailed = body?.data
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx
index 62d220c9c8..fd2b7d677b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx
@@ -79,7 +79,7 @@ export function ExampleCommand({
case 'rate-limits': {
const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0]
return `curl -H "X-API-Key: ${apiKey}" \\
- ${baseUrlForRateLimit}/api/users/rate-limit`
+ ${baseUrlForRateLimit}/api/users/me/rate-limit`
}
default:
@@ -119,7 +119,7 @@ export function ExampleCommand({
case 'rate-limits': {
const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0]
return `curl -H "X-API-Key: SIM_API_KEY" \\
- ${baseUrlForRateLimit}/api/users/rate-limit`
+ ${baseUrlForRateLimit}/api/users/me/rate-limit`
}
default:
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx
index b453c54139..4eb3d8e46d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx
@@ -321,7 +321,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
try {
// Primary: call server-side usage check to mirror backend enforcement
- const res = await fetch('/api/usage/check', { cache: 'no-store' })
+ const res = await fetch('/api/usage?context=user', { cache: 'no-store' })
if (res.ok) {
const payload = await res.json()
const usage = payload?.data
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx
index 756b7c8965..4e763ea3dd 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx
@@ -385,7 +385,7 @@ const UserInput = forwardRef(
if (isLoadingWorkflows || workflows.length > 0) return
try {
setIsLoadingWorkflows(true)
- const resp = await fetch('/api/workflows/sync')
+ const resp = await fetch('/api/workflows')
if (!resp.ok) throw new Error(`Failed to load workflows: ${resp.status}`)
const data = await resp.json()
const items = Array.isArray(data?.data) ? data.data : []
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx
index 6d5f05a8a5..ca5e3df6b4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx
@@ -279,7 +279,10 @@ export function CredentialSelector({
-
+
{isLoading ? (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx
index 333fc1ab13..448a1ca4fb 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx
@@ -149,9 +149,15 @@ export function TriggerConfigSection({
-
-
-
+
+
+ e.stopPropagation()}
+ >
{availableOptions.length === 0
? 'No options available. Please select credentials first.'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
index 51e6a3cb9d..c991c07be0 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
@@ -23,7 +23,6 @@ import type { EnvironmentVariable as StoreEnvironmentVariable } from '@/stores/s
const logger = createLogger('EnvironmentVariables')
-// Constants
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr),minmax(0,1fr),88px] gap-4'
const INITIAL_ENV_VAR: UIEnvironmentVariable = { key: '', value: '' }
@@ -66,7 +65,6 @@ export function EnvironmentVariables({
const pendingClose = useRef(false)
const initialVarsRef = useRef([])
- // Filter environment variables based on search term
const filteredEnvVars = useMemo(() => {
if (!searchTerm.trim()) {
return envVars.map((envVar, index) => ({ envVar, originalIndex: index }))
@@ -77,7 +75,6 @@ export function EnvironmentVariables({
.filter(({ envVar }) => envVar.key.toLowerCase().includes(searchTerm.toLowerCase()))
}, [envVars, searchTerm])
- // Derived state
const hasChanges = useMemo(() => {
const initialVars = initialVarsRef.current.filter((v) => v.key || v.value)
const currentVars = envVars.filter((v) => v.key || v.value)
@@ -96,7 +93,6 @@ export function EnvironmentVariables({
if (!currentMap.has(key)) return true
}
- // Workspace diffs
const before = initialWorkspaceVarsRef.current
const after = workspaceVars
const beforeKeys = Object.keys(before)
@@ -109,12 +105,10 @@ export function EnvironmentVariables({
return false
}, [envVars, workspaceVars])
- // Check if there are any active conflicts
const hasConflicts = useMemo(() => {
return envVars.some((envVar) => !!envVar.key && Object.hasOwn(workspaceVars, envVar.key))
}, [envVars, workspaceVars])
- // Intercept close attempts to check for unsaved changes
const handleModalClose = (open: boolean) => {
if (!open && hasChanges) {
setShowUnsavedChanges(true)
@@ -124,7 +118,6 @@ export function EnvironmentVariables({
}
}
- // Initialization effect
useEffect(() => {
const existingVars = Object.values(variables)
const initialVars = existingVars.length ? existingVars : [INITIAL_ENV_VAR]
@@ -158,14 +151,12 @@ export function EnvironmentVariables({
}
}, [workspaceId, loadWorkspaceEnvironment])
- // Register close handler with parent
useEffect(() => {
if (registerCloseHandler) {
registerCloseHandler(handleModalClose)
}
}, [registerCloseHandler, hasChanges])
- // Scroll effect - only when explicitly adding a new variable
useEffect(() => {
if (shouldScrollToBottom && scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
@@ -322,8 +313,7 @@ export function EnvironmentVariables({
}),
{}
)
-
- useEnvironmentStore.getState().setVariables(validVariables)
+ await useEnvironmentStore.getState().saveEnvironmentVariables(validVariables)
const before = initialWorkspaceVarsRef.current
const after = workspaceVars
@@ -354,7 +344,6 @@ export function EnvironmentVariables({
}
}
- // UI rendering
const renderEnvVarRow = (envVar: UIEnvironmentVariable, originalIndex: number) => {
const isConflict = !!envVar.key && Object.hasOwn(workspaceVars, envVar.key)
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx
index 4f64d889bf..186cc874d6 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx
@@ -113,7 +113,7 @@ export const UsageLimit = forwardRef(
throw new Error('Organization ID is required')
}
- const response = await fetch('/api/usage-limits', {
+ const response = await fetch('/api/usage', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts
index 8dfc9bccce..3d52aa3d18 100644
--- a/apps/sim/blocks/blocks/agent.ts
+++ b/apps/sim/blocks/blocks/agent.ts
@@ -16,11 +16,10 @@ import {
// Get current Ollama models dynamically
const getCurrentOllamaModels = () => {
- return useOllamaStore.getState().models
+ return useProvidersStore.getState().providers.ollama.models
}
-import { useOllamaStore } from '@/stores/ollama/store'
-import { useOpenRouterStore } from '@/stores/openrouter/store'
+import { useProvidersStore } from '@/stores/providers/store'
import type { ToolResponse } from '@/tools/types'
const logger = createLogger('AgentBlock')
@@ -158,8 +157,9 @@ Create a system prompt appropriately detailed for the request, using clear langu
placeholder: 'Type or select a model...',
required: true,
options: () => {
- const ollamaModels = useOllamaStore.getState().models
- const openrouterModels = useOpenRouterStore.getState().models
+ const providersState = useProvidersStore.getState()
+ const ollamaModels = providersState.providers.ollama.models
+ const openrouterModels = providersState.providers.openrouter.models
const baseModels = Object.keys(getBaseModelProviders())
const allModels = Array.from(new Set([...baseModels, ...ollamaModels, ...openrouterModels]))
diff --git a/apps/sim/blocks/blocks/evaluator.ts b/apps/sim/blocks/blocks/evaluator.ts
index 0f3e05ea81..52e0899ed9 100644
--- a/apps/sim/blocks/blocks/evaluator.ts
+++ b/apps/sim/blocks/blocks/evaluator.ts
@@ -4,7 +4,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import type { BlockConfig, ParamType } from '@/blocks/types'
import type { ProviderId } from '@/providers/types'
import { getAllModelProviders, getBaseModelProviders, getHostedModels } from '@/providers/utils'
-import { useOllamaStore } from '@/stores/ollama/store'
+import { useProvidersStore } from '@/stores/providers/store'
import type { ToolResponse } from '@/tools/types'
const logger = createLogger('EvaluatorBlock')
@@ -177,7 +177,7 @@ export const EvaluatorBlock: BlockConfig = {
layout: 'half',
required: true,
options: () => {
- const ollamaModels = useOllamaStore.getState().models
+ const ollamaModels = useProvidersStore.getState().providers.ollama.models
const baseModels = Object.keys(getBaseModelProviders())
return [...baseModels, ...ollamaModels].map((model) => ({
label: model,
diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts
index 825dec5a03..7fe72de2f2 100644
--- a/apps/sim/blocks/blocks/router.ts
+++ b/apps/sim/blocks/blocks/router.ts
@@ -3,7 +3,7 @@ import { isHosted } from '@/lib/environment'
import type { BlockConfig } from '@/blocks/types'
import type { ProviderId } from '@/providers/types'
import { getAllModelProviders, getBaseModelProviders, getHostedModels } from '@/providers/utils'
-import { useOllamaStore } from '@/stores/ollama/store'
+import { useProvidersStore } from '@/stores/providers/store'
import type { ToolResponse } from '@/tools/types'
interface RouterResponse extends ToolResponse {
@@ -119,7 +119,7 @@ export const RouterBlock: BlockConfig = {
type: 'dropdown',
layout: 'half',
options: () => {
- const ollamaModels = useOllamaStore.getState().models
+ const ollamaModels = useProvidersStore.getState().providers.ollama.models
const baseModels = Object.keys(getBaseModelProviders())
return [...baseModels, ...ollamaModels].map((model) => ({
label: model,
diff --git a/apps/sim/components/emails/workspace-invitation.tsx b/apps/sim/components/emails/workspace-invitation.tsx
index ce03cb8576..81834cecbd 100644
--- a/apps/sim/components/emails/workspace-invitation.tsx
+++ b/apps/sim/components/emails/workspace-invitation.tsx
@@ -39,8 +39,11 @@ export const WorkspaceInvitationEmail = ({
let enhancedLink = invitationLink
try {
- // If the link is pointing to the API endpoint directly, update it to use the client route
- if (invitationLink.includes('/api/workspaces/invitations/accept')) {
+ // If the link is pointing to any API endpoint directly, update it to use the client route
+ if (
+ invitationLink.includes('/api/workspaces/invitations/accept') ||
+ invitationLink.match(/\/api\/workspaces\/invitations\/[^?]+\?token=/)
+ ) {
const url = new URL(invitationLink)
const token = url.searchParams.get('token')
if (token) {
diff --git a/apps/sim/components/ui/switch.tsx b/apps/sim/components/ui/switch.tsx
index 7ea9f2a74d..f07fc75f05 100644
--- a/apps/sim/components/ui/switch.tsx
+++ b/apps/sim/components/ui/switch.tsx
@@ -10,7 +10,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
statement-breakpoint
+ALTER TABLE "workspace_invitation" ALTER COLUMN "status" SET DEFAULT 'pending'::"public"."workspace_invitation_status";--> statement-breakpoint
+ALTER TABLE "workspace_invitation" ALTER COLUMN "status" SET DATA TYPE "public"."workspace_invitation_status" USING "status"::"public"."workspace_invitation_status";
\ No newline at end of file
diff --git a/apps/sim/db/migrations/meta/0083_snapshot.json b/apps/sim/db/migrations/meta/0083_snapshot.json
new file mode 100644
index 0000000000..e4f270a28a
--- /dev/null
+++ b/apps/sim/db/migrations/meta/0083_snapshot.json
@@ -0,0 +1,6049 @@
+{
+ "id": "f4d42c71-1238-4445-b29d-7cc74d1f748c",
+ "prevId": "5466d2f8-8b28-47d3-816f-07bbb3721d3d",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_key": {
+ "name": "api_key",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "api_key_user_id_user_id_fk": {
+ "name": "api_key_user_id_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_key_key_unique": {
+ "name": "api_key_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat": {
+ "name": "chat",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subdomain": {
+ "name": "subdomain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "customizations": {
+ "name": "customizations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "output_configs": {
+ "name": "output_configs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "subdomain_idx": {
+ "name": "subdomain_idx",
+ "columns": [
+ {
+ "expression": "subdomain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_workflow_id_workflow_id_fk": {
+ "name": "chat_workflow_id_workflow_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_user_id_user_id_fk": {
+ "name": "chat_user_id_user_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_chats": {
+ "name": "copilot_chats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "messages": {
+ "name": "messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'claude-3-7-sonnet-latest'"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preview_yaml": {
+ "name": "preview_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_chats_user_id_idx": {
+ "name": "copilot_chats_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_workflow_id_idx": {
+ "name": "copilot_chats_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workflow_idx": {
+ "name": "copilot_chats_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_created_at_idx": {
+ "name": "copilot_chats_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_updated_at_idx": {
+ "name": "copilot_chats_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_chats_user_id_user_id_fk": {
+ "name": "copilot_chats_user_id_user_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_workflow_id_workflow_id_fk": {
+ "name": "copilot_chats_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_feedback": {
+ "name": "copilot_feedback",
+ "schema": "",
+ "columns": {
+ "feedback_id": {
+ "name": "feedback_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_query": {
+ "name": "user_query",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_response": {
+ "name": "agent_response",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_positive": {
+ "name": "is_positive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feedback": {
+ "name": "feedback",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_yaml": {
+ "name": "workflow_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_feedback_user_id_idx": {
+ "name": "copilot_feedback_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_chat_id_idx": {
+ "name": "copilot_feedback_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_user_chat_idx": {
+ "name": "copilot_feedback_user_chat_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_is_positive_idx": {
+ "name": "copilot_feedback_is_positive_idx",
+ "columns": [
+ {
+ "expression": "is_positive",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_created_at_idx": {
+ "name": "copilot_feedback_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_feedback_user_id_user_id_fk": {
+ "name": "copilot_feedback_user_id_user_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_feedback_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_feedback_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_tools": {
+ "name": "custom_tools",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "custom_tools_user_id_user_id_fk": {
+ "name": "custom_tools_user_id_user_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.docs_embeddings": {
+ "name": "docs_embeddings",
+ "schema": "",
+ "columns": {
+ "chunk_id": {
+ "name": "chunk_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chunk_text": {
+ "name": "chunk_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_document": {
+ "name": "source_document",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_link": {
+ "name": "source_link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_text": {
+ "name": "header_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_level": {
+ "name": "header_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "chunk_text_tsv": {
+ "name": "chunk_text_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "docs_emb_source_document_idx": {
+ "name": "docs_emb_source_document_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_header_level_idx": {
+ "name": "docs_emb_header_level_idx",
+ "columns": [
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_source_header_idx": {
+ "name": "docs_emb_source_header_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_model_idx": {
+ "name": "docs_emb_model_idx",
+ "columns": [
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_created_at_idx": {
+ "name": "docs_emb_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_embedding_vector_hnsw_idx": {
+ "name": "docs_embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "docs_emb_metadata_gin_idx": {
+ "name": "docs_emb_metadata_gin_idx",
+ "columns": [
+ {
+ "expression": "metadata",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "docs_emb_chunk_text_fts_idx": {
+ "name": "docs_emb_chunk_text_fts_idx",
+ "columns": [
+ {
+ "expression": "chunk_text_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "docs_embedding_not_null_check": {
+ "name": "docs_embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ },
+ "docs_header_level_check": {
+ "name": "docs_header_level_check",
+ "value": "\"header_level\" >= 1 AND \"header_level\" <= 6"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.document": {
+ "name": "document",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_count": {
+ "name": "chunk_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "character_count": {
+ "name": "character_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_completed_at": {
+ "name": "processing_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_error": {
+ "name": "processing_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "doc_kb_id_idx": {
+ "name": "doc_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_filename_idx": {
+ "name": "doc_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_uploaded_at_idx": {
+ "name": "doc_kb_uploaded_at_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "uploaded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_processing_status_idx": {
+ "name": "doc_processing_status_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag1_idx": {
+ "name": "doc_tag1_idx",
+ "columns": [
+ {
+ "expression": "tag1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag2_idx": {
+ "name": "doc_tag2_idx",
+ "columns": [
+ {
+ "expression": "tag2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag3_idx": {
+ "name": "doc_tag3_idx",
+ "columns": [
+ {
+ "expression": "tag3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag4_idx": {
+ "name": "doc_tag4_idx",
+ "columns": [
+ {
+ "expression": "tag4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag5_idx": {
+ "name": "doc_tag5_idx",
+ "columns": [
+ {
+ "expression": "tag5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag6_idx": {
+ "name": "doc_tag6_idx",
+ "columns": [
+ {
+ "expression": "tag6",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag7_idx": {
+ "name": "doc_tag7_idx",
+ "columns": [
+ {
+ "expression": "tag7",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "document_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "document_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.embedding": {
+ "name": "embedding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_index": {
+ "name": "chunk_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_hash": {
+ "name": "chunk_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_length": {
+ "name": "content_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "content_tsv": {
+ "name": "content_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"embedding\".\"content\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "emb_kb_id_idx": {
+ "name": "emb_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_id_idx": {
+ "name": "emb_doc_id_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_chunk_idx": {
+ "name": "emb_doc_chunk_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chunk_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_model_idx": {
+ "name": "emb_kb_model_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_enabled_idx": {
+ "name": "emb_kb_enabled_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_enabled_idx": {
+ "name": "emb_doc_enabled_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "embedding_vector_hnsw_idx": {
+ "name": "embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "emb_tag1_idx": {
+ "name": "emb_tag1_idx",
+ "columns": [
+ {
+ "expression": "tag1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag2_idx": {
+ "name": "emb_tag2_idx",
+ "columns": [
+ {
+ "expression": "tag2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag3_idx": {
+ "name": "emb_tag3_idx",
+ "columns": [
+ {
+ "expression": "tag3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag4_idx": {
+ "name": "emb_tag4_idx",
+ "columns": [
+ {
+ "expression": "tag4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag5_idx": {
+ "name": "emb_tag5_idx",
+ "columns": [
+ {
+ "expression": "tag5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag6_idx": {
+ "name": "emb_tag6_idx",
+ "columns": [
+ {
+ "expression": "tag6",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag7_idx": {
+ "name": "emb_tag7_idx",
+ "columns": [
+ {
+ "expression": "tag7",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_content_fts_idx": {
+ "name": "emb_content_fts_idx",
+ "columns": [
+ {
+ "expression": "content_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "embedding_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "embedding_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "embedding_document_id_document_id_fk": {
+ "name": "embedding_document_id_document_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_not_null_check": {
+ "name": "embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.environment": {
+ "name": "environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "environment_user_id_user_id_fk": {
+ "name": "environment_user_id_user_id_fk",
+ "tableFrom": "environment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "environment_user_id_unique": {
+ "name": "environment_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "invitation_email_idx": {
+ "name": "invitation_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_organization_id_idx": {
+ "name": "invitation_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base": {
+ "name": "knowledge_base",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "embedding_dimension": {
+ "name": "embedding_dimension",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1536
+ },
+ "chunking_config": {
+ "name": "chunking_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_user_id_idx": {
+ "name": "kb_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_id_idx": {
+ "name": "kb_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_user_workspace_idx": {
+ "name": "kb_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_deleted_at_idx": {
+ "name": "kb_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_user_id_user_id_fk": {
+ "name": "knowledge_base_user_id_user_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_workspace_id_workspace_id_fk": {
+ "name": "knowledge_base_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base_tag_definitions": {
+ "name": "knowledge_base_tag_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_slot": {
+ "name": "tag_slot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_type": {
+ "name": "field_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_tag_definitions_kb_slot_idx": {
+ "name": "kb_tag_definitions_kb_slot_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_slot",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_display_name_idx": {
+ "name": "kb_tag_definitions_kb_display_name_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_id_idx": {
+ "name": "kb_tag_definitions_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "knowledge_base_tag_definitions",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.marketplace": {
+ "name": "marketplace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_name": {
+ "name": "author_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "marketplace_workflow_id_workflow_id_fk": {
+ "name": "marketplace_workflow_id_workflow_id_fk",
+ "tableFrom": "marketplace",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "marketplace_author_id_user_id_fk": {
+ "name": "marketplace_author_id_user_id_fk",
+ "tableFrom": "marketplace",
+ "tableTo": "user",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "member_user_id_idx": {
+ "name": "member_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "member_organization_id_idx": {
+ "name": "member_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memory": {
+ "name": "memory",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "memory_key_idx": {
+ "name": "memory_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workflow_idx": {
+ "name": "memory_workflow_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workflow_key_idx": {
+ "name": "memory_workflow_key_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "memory_workflow_id_workflow_id_fk": {
+ "name": "memory_workflow_id_workflow_id_fk",
+ "tableFrom": "memory",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "org_usage_limit": {
+ "name": "org_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permissions_user_id_idx": {
+ "name": "permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_entity_idx": {
+ "name": "permissions_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_type_idx": {
+ "name": "permissions_user_entity_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_permission_idx": {
+ "name": "permissions_user_entity_permission_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_idx": {
+ "name": "permissions_user_entity_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_unique_constraint": {
+ "name": "permissions_unique_constraint",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permissions_user_id_user_id_fk": {
+ "name": "permissions_user_id_user_id_fk",
+ "tableFrom": "permissions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_token_idx": {
+ "name": "session_token_idx",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_active_organization_id_organization_id_fk": {
+ "name": "session_active_organization_id_organization_id_fk",
+ "tableFrom": "session",
+ "tableTo": "organization",
+ "columnsFrom": ["active_organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "auto_connect": {
+ "name": "auto_connect",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_fill_env_vars": {
+ "name": "auto_fill_env_vars",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_pan": {
+ "name": "auto_pan",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "console_expanded_by_default": {
+ "name": "console_expanded_by_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "telemetry_enabled": {
+ "name": "telemetry_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "email_preferences": {
+ "name": "email_preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "general": {
+ "name": "general",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "settings_user_id_user_id_fk": {
+ "name": "settings_user_id_user_id_fk",
+ "tableFrom": "settings",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "settings_user_id_unique": {
+ "name": "settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.subscription": {
+ "name": "subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_start": {
+ "name": "period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_end": {
+ "name": "period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_at_period_end": {
+ "name": "cancel_at_period_end",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seats": {
+ "name": "seats",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_start": {
+ "name": "trial_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_end": {
+ "name": "trial_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "subscription_reference_status_idx": {
+ "name": "subscription_reference_status_idx",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "check_enterprise_metadata": {
+ "name": "check_enterprise_metadata",
+ "value": "plan != 'enterprise' OR metadata IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.template_stars": {
+ "name": "template_stars",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "starred_at": {
+ "name": "starred_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "template_stars_user_id_idx": {
+ "name": "template_stars_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_id_idx": {
+ "name": "template_stars_template_id_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_user_template_idx": {
+ "name": "template_stars_user_template_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_user_idx": {
+ "name": "template_stars_template_user_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_starred_at_idx": {
+ "name": "template_stars_starred_at_idx",
+ "columns": [
+ {
+ "expression": "starred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_starred_at_idx": {
+ "name": "template_stars_template_starred_at_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "starred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_user_template_unique": {
+ "name": "template_stars_user_template_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "template_stars_user_id_user_id_fk": {
+ "name": "template_stars_user_id_user_id_fk",
+ "tableFrom": "template_stars",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "template_stars_template_id_templates_id_fk": {
+ "name": "template_stars_template_id_templates_id_fk",
+ "tableFrom": "template_stars",
+ "tableTo": "templates",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.templates": {
+ "name": "templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "stars": {
+ "name": "stars",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#3972F6'"
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'FileText'"
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "templates_workflow_id_idx": {
+ "name": "templates_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_user_id_idx": {
+ "name": "templates_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_category_idx": {
+ "name": "templates_category_idx",
+ "columns": [
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_views_idx": {
+ "name": "templates_views_idx",
+ "columns": [
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_stars_idx": {
+ "name": "templates_stars_idx",
+ "columns": [
+ {
+ "expression": "stars",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_category_views_idx": {
+ "name": "templates_category_views_idx",
+ "columns": [
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_category_stars_idx": {
+ "name": "templates_category_stars_idx",
+ "columns": [
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stars",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_user_category_idx": {
+ "name": "templates_user_category_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_created_at_idx": {
+ "name": "templates_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_updated_at_idx": {
+ "name": "templates_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "templates_workflow_id_workflow_id_fk": {
+ "name": "templates_workflow_id_workflow_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "templates_user_id_user_id_fk": {
+ "name": "templates_user_id_user_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_rate_limits": {
+ "name": "user_rate_limits",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "sync_api_requests": {
+ "name": "sync_api_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "async_api_requests": {
+ "name": "async_api_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_request_at": {
+ "name": "last_request_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "is_rate_limited": {
+ "name": "is_rate_limited",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "rate_limit_reset_at": {
+ "name": "rate_limit_reset_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_rate_limits_user_id_user_id_fk": {
+ "name": "user_rate_limits_user_id_user_id_fk",
+ "tableFrom": "user_rate_limits",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_stats": {
+ "name": "user_stats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_manual_executions": {
+ "name": "total_manual_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_api_calls": {
+ "name": "total_api_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_webhook_triggers": {
+ "name": "total_webhook_triggers",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_scheduled_executions": {
+ "name": "total_scheduled_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_chat_executions": {
+ "name": "total_chat_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tokens_used": {
+ "name": "total_tokens_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_usage_limit": {
+ "name": "current_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'10'"
+ },
+ "usage_limit_updated_at": {
+ "name": "usage_limit_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "current_period_cost": {
+ "name": "current_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_cost": {
+ "name": "last_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "total_copilot_cost": {
+ "name": "total_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "total_copilot_tokens": {
+ "name": "total_copilot_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_copilot_calls": {
+ "name": "total_copilot_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_active": {
+ "name": "last_active",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "billing_blocked": {
+ "name": "billing_blocked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_stats_user_id_user_id_fk": {
+ "name": "user_stats_user_id_user_id_fk",
+ "tableFrom": "user_stats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_stats_user_id_unique": {
+ "name": "user_stats_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.waitlist": {
+ "name": "waitlist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "waitlist_email_unique": {
+ "name": "waitlist_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook": {
+ "name": "webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_config": {
+ "name": "provider_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_workflow_id_workflow_id_fk": {
+ "name": "webhook_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "webhook_block_id_workflow_blocks_id_fk": {
+ "name": "webhook_block_id_workflow_blocks_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow": {
+ "name": "workflow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#3972F6'"
+ },
+ "last_synced": {
+ "name": "last_synced",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_deployed": {
+ "name": "is_deployed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deployed_state": {
+ "name": "deployed_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deployed_at": {
+ "name": "deployed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pinned_api_key": {
+ "name": "pinned_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "collaborators": {
+ "name": "collaborators",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "is_published": {
+ "name": "is_published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "marketplace_data": {
+ "name": "marketplace_data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workflow_user_id_idx": {
+ "name": "workflow_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_id_idx": {
+ "name": "workflow_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_user_workspace_idx": {
+ "name": "workflow_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_user_id_user_id_fk": {
+ "name": "workflow_user_id_user_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_workspace_id_workspace_id_fk": {
+ "name": "workflow_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_id_workflow_folder_id_fk": {
+ "name": "workflow_folder_id_workflow_folder_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workflow_folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_blocks": {
+ "name": "workflow_blocks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_x": {
+ "name": "position_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_y": {
+ "name": "position_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "horizontal_handles": {
+ "name": "horizontal_handles",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_wide": {
+ "name": "is_wide",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "advanced_mode": {
+ "name": "advanced_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "trigger_mode": {
+ "name": "trigger_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "sub_blocks": {
+ "name": "sub_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extent": {
+ "name": "extent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_blocks_workflow_id_idx": {
+ "name": "workflow_blocks_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_parent_id_idx": {
+ "name": "workflow_blocks_parent_id_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_workflow_parent_idx": {
+ "name": "workflow_blocks_workflow_parent_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_workflow_type_idx": {
+ "name": "workflow_blocks_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_blocks_workflow_id_workflow_id_fk": {
+ "name": "workflow_blocks_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_blocks",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_checkpoints": {
+ "name": "workflow_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_state": {
+ "name": "workflow_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_checkpoints_user_id_idx": {
+ "name": "workflow_checkpoints_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_id_idx": {
+ "name": "workflow_checkpoints_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_id_idx": {
+ "name": "workflow_checkpoints_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_message_id_idx": {
+ "name": "workflow_checkpoints_message_id_idx",
+ "columns": [
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_user_workflow_idx": {
+ "name": "workflow_checkpoints_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_chat_idx": {
+ "name": "workflow_checkpoints_workflow_chat_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_created_at_idx": {
+ "name": "workflow_checkpoints_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_created_at_idx": {
+ "name": "workflow_checkpoints_chat_created_at_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_checkpoints_user_id_user_id_fk": {
+ "name": "workflow_checkpoints_user_id_user_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_workflow_id_workflow_id_fk": {
+ "name": "workflow_checkpoints_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_chat_id_copilot_chats_id_fk": {
+ "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_edges": {
+ "name": "workflow_edges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_block_id": {
+ "name": "source_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_handle": {
+ "name": "source_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_handle": {
+ "name": "target_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_edges_workflow_id_idx": {
+ "name": "workflow_edges_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_source_block_idx": {
+ "name": "workflow_edges_source_block_idx",
+ "columns": [
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_target_block_idx": {
+ "name": "workflow_edges_target_block_idx",
+ "columns": [
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_source_idx": {
+ "name": "workflow_edges_workflow_source_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_target_idx": {
+ "name": "workflow_edges_workflow_target_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_edges_workflow_id_workflow_id_fk": {
+ "name": "workflow_edges_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_source_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["source_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_target_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["target_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_logs": {
+ "name": "workflow_execution_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_snapshot_id": {
+ "name": "state_snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_duration_ms": {
+ "name": "total_duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_data": {
+ "name": "execution_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "cost": {
+ "name": "cost",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "files": {
+ "name": "files",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_execution_logs_workflow_id_idx": {
+ "name": "workflow_execution_logs_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_execution_id_idx": {
+ "name": "workflow_execution_logs_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_trigger_idx": {
+ "name": "workflow_execution_logs_trigger_idx",
+ "columns": [
+ {
+ "expression": "trigger",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_level_idx": {
+ "name": "workflow_execution_logs_level_idx",
+ "columns": [
+ {
+ "expression": "level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_started_at_idx": {
+ "name": "workflow_execution_logs_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_execution_id_unique": {
+ "name": "workflow_execution_logs_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workflow_started_at_idx": {
+ "name": "workflow_execution_logs_workflow_started_at_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_logs_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_logs_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": {
+ "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_execution_snapshots",
+ "columnsFrom": ["state_snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_snapshots": {
+ "name": "workflow_execution_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_hash": {
+ "name": "state_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_data": {
+ "name": "state_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_snapshots_workflow_id_idx": {
+ "name": "workflow_snapshots_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_hash_idx": {
+ "name": "workflow_snapshots_hash_idx",
+ "columns": [
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_workflow_hash_idx": {
+ "name": "workflow_snapshots_workflow_hash_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_created_at_idx": {
+ "name": "workflow_snapshots_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_snapshots_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_snapshots",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_folder": {
+ "name": "workflow_folder",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'#6B7280'"
+ },
+ "is_expanded": {
+ "name": "is_expanded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_folder_user_idx": {
+ "name": "workflow_folder_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_workspace_parent_idx": {
+ "name": "workflow_folder_workspace_parent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_parent_sort_idx": {
+ "name": "workflow_folder_parent_sort_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_folder_user_id_user_id_fk": {
+ "name": "workflow_folder_user_id_user_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_workspace_id_workspace_id_fk": {
+ "name": "workflow_folder_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_schedule": {
+ "name": "workflow_schedule",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_ran_at": {
+ "name": "last_ran_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_type": {
+ "name": "trigger_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_schedule_workflow_block_unique": {
+ "name": "workflow_schedule_workflow_block_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_schedule_workflow_id_workflow_id_fk": {
+ "name": "workflow_schedule_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_schedule_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_subflows": {
+ "name": "workflow_subflows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_subflows_workflow_id_idx": {
+ "name": "workflow_subflows_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_subflows_workflow_type_idx": {
+ "name": "workflow_subflows_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_subflows_workflow_id_workflow_id_fk": {
+ "name": "workflow_subflows_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_subflows",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace": {
+ "name": "workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_owner_id_user_id_fk": {
+ "name": "workspace_owner_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_environment": {
+ "name": "workspace_environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_environment_workspace_unique": {
+ "name": "workspace_environment_workspace_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_environment_workspace_id_idx": {
+ "name": "workspace_environment_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_environment_workspace_id_workspace_id_fk": {
+ "name": "workspace_environment_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_environment",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_invitation": {
+ "name": "workspace_invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "status": {
+ "name": "status",
+ "type": "workspace_invitation_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'admin'"
+ },
+ "org_invitation_id": {
+ "name": "org_invitation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_invitation_workspace_id_workspace_id_fk": {
+ "name": "workspace_invitation_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_invitation_inviter_id_user_id_fk": {
+ "name": "workspace_invitation_inviter_id_user_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_invitation_token_unique": {
+ "name": "workspace_invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.permission_type": {
+ "name": "permission_type",
+ "schema": "public",
+ "values": ["admin", "write", "read"]
+ },
+ "public.workspace_invitation_status": {
+ "name": "workspace_invitation_status",
+ "schema": "public",
+ "values": ["pending", "accepted", "rejected", "cancelled"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json
index 7de75ccfb1..18ceddc8dc 100644
--- a/apps/sim/db/migrations/meta/_journal.json
+++ b/apps/sim/db/migrations/meta/_journal.json
@@ -575,6 +575,13 @@
"when": 1756767479124,
"tag": "0082_light_blockbuster",
"breakpoints": true
+ },
+ {
+ "idx": 83,
+ "version": "7",
+ "when": 1756768177306,
+ "tag": "0083_ambiguous_dreadnoughts",
+ "breakpoints": true
}
]
}
diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts
index 41992603d3..03daecea9a 100644
--- a/apps/sim/db/schema.ts
+++ b/apps/sim/db/schema.ts
@@ -639,9 +639,17 @@ export const workspace = pgTable('workspace', {
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})
-// Define the permission enum
export const permissionTypeEnum = pgEnum('permission_type', ['admin', 'write', 'read'])
+export const workspaceInvitationStatusEnum = pgEnum('workspace_invitation_status', [
+ 'pending',
+ 'accepted',
+ 'rejected',
+ 'cancelled',
+])
+
+export type WorkspaceInvitationStatus = (typeof workspaceInvitationStatusEnum.enumValues)[number]
+
export const workspaceInvitation = pgTable('workspace_invitation', {
id: text('id').primaryKey(),
workspaceId: text('workspace_id')
@@ -652,7 +660,7 @@ export const workspaceInvitation = pgTable('workspace_invitation', {
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
role: text('role').notNull().default('member'),
- status: text('status').notNull().default('pending'),
+ status: workspaceInvitationStatusEnum('status').notNull().default('pending'),
token: text('token').notNull().unique(),
permissions: permissionTypeEnum('permissions').notNull().default('admin'),
orgInvitationId: text('org_invitation_id'),
diff --git a/apps/sim/hooks/use-subscription-state.ts b/apps/sim/hooks/use-subscription-state.ts
index 0eeddeb519..6ef937d14a 100644
--- a/apps/sim/hooks/use-subscription-state.ts
+++ b/apps/sim/hooks/use-subscription-state.ts
@@ -153,7 +153,7 @@ export function useUsageLimit() {
setIsLoading(true)
setError(null)
- const response = await fetch('/api/usage-limits?context=user')
+ const response = await fetch('/api/usage?context=user')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
@@ -180,7 +180,7 @@ export function useUsageLimit() {
const updateLimit = async (newLimit: number) => {
try {
- const response = await fetch('/api/usage-limits?context=user', {
+ const response = await fetch('/api/usage?context=user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts
index d2c43755aa..74808ffe73 100644
--- a/apps/sim/lib/billing/validation/seat-management.ts
+++ b/apps/sim/lib/billing/validation/seat-management.ts
@@ -114,7 +114,6 @@ export async function getOrganizationSeatInfo(
organizationId: string
): Promise {
try {
- // Get organization details
const organizationData = await db
.select({
id: organization.id,
@@ -128,14 +127,12 @@ export async function getOrganizationSeatInfo(
return null
}
- // Get organization subscription directly (referenceId = organizationId)
const subscription = await getOrganizationSubscription(organizationId)
if (!subscription) {
return null
}
- // Get current member count
const memberCount = await db
.select({ count: count() })
.from(member)
@@ -143,11 +140,8 @@ export async function getOrganizationSeatInfo(
const currentSeats = memberCount[0]?.count || 0
- // Determine seat limits
const maxSeats = subscription.seats || 1
- // Enterprise plans have fixed seats (can't self-serve changes)
- // Team plans can add seats through Stripe
const canAddSeats = subscription.plan !== 'enterprise'
const availableSeats = Math.max(0, maxSeats - currentSeats)
@@ -183,14 +177,12 @@ export async function validateBulkInvitations(
validationResult: SeatValidationResult
}> {
try {
- // Remove duplicates and validate email format
const uniqueEmails = [...new Set(emailList)]
const validEmails = uniqueEmails.filter(
(email) => quickValidateEmail(email.trim().toLowerCase()).isValid
)
const duplicateEmails = emailList.filter((email, index) => emailList.indexOf(email) !== index)
- // Check for existing members
const existingMembers = await db
.select({ userEmail: user.email })
.from(member)
@@ -200,7 +192,6 @@ export async function validateBulkInvitations(
const existingEmails = existingMembers.map((m) => m.userEmail)
const newEmails = validEmails.filter((email) => !existingEmails.includes(email))
- // Check for pending invitations
const pendingInvitations = await db
.select({ email: invitation.email })
.from(invitation)
@@ -209,7 +200,6 @@ export async function validateBulkInvitations(
const pendingEmails = pendingInvitations.map((i) => i.email)
const finalEmailsToInvite = newEmails.filter((email) => !pendingEmails.includes(email))
- // Validate seat availability
const seatsNeeded = finalEmailsToInvite.length
const validationResult = await validateSeatAvailability(organizationId, seatsNeeded)
@@ -258,14 +248,12 @@ export async function updateOrganizationSeats(
updatedBy: string
): Promise<{ success: boolean; error?: string }> {
try {
- // Get current organization subscription directly (referenceId = organizationId)
const subscriptionRecord = await getOrganizationSubscription(organizationId)
if (!subscriptionRecord) {
return { success: false, error: 'No active subscription found' }
}
- // Validate minimum seat requirements
const memberCount = await db
.select({ count: count() })
.from(member)
@@ -280,7 +268,6 @@ export async function updateOrganizationSeats(
}
}
- // Update subscription seat count
await db
.update(subscription)
.set({
@@ -320,7 +307,6 @@ export async function validateMemberRemoval(
removedBy: string
): Promise<{ canRemove: boolean; reason?: string }> {
try {
- // Get member details
const memberRecord = await db
.select({ role: member.role })
.from(member)
@@ -331,12 +317,10 @@ export async function validateMemberRemoval(
return { canRemove: false, reason: 'Member not found in organization' }
}
- // Check if trying to remove the organization owner
if (memberRecord[0].role === 'owner') {
return { canRemove: false, reason: 'Cannot remove organization owner' }
}
- // Check if the person removing has sufficient permissions
const removerMemberRecord = await db
.select({ role: member.role })
.from(member)
@@ -350,22 +334,18 @@ export async function validateMemberRemoval(
const removerRole = removerMemberRecord[0].role
const targetRole = memberRecord[0].role
- // Permission hierarchy: owner > admin > member
if (removerRole === 'owner') {
- // Owners can remove anyone except themselves
return userIdToRemove === removedBy
? { canRemove: false, reason: 'Cannot remove yourself as owner' }
: { canRemove: true }
}
if (removerRole === 'admin') {
- // Admins can remove members but not other admins or owners
return targetRole === 'member'
? { canRemove: true }
: { canRemove: false, reason: 'Insufficient permissions to remove this member' }
}
- // Members cannot remove other members
return { canRemove: false, reason: 'Insufficient permissions' }
} catch (error) {
logger.error('Failed to validate member removal', {
@@ -390,7 +370,6 @@ export async function getOrganizationSeatAnalytics(organizationId: string) {
return null
}
- // Get member activity data
const memberActivity = await db
.select({
userId: member.userId,
@@ -405,7 +384,6 @@ export async function getOrganizationSeatAnalytics(organizationId: string) {
.leftJoin(userStats, eq(member.userId, userStats.userId))
.where(eq(member.organizationId, organizationId))
- // Calculate utilization metrics
const utilizationRate =
seatInfo.maxSeats > 0 ? (seatInfo.currentSeats / seatInfo.maxSeats) * 100 : 0
diff --git a/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts b/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts
index 9094c2792d..c62f11e75e 100644
--- a/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts
+++ b/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts
@@ -31,7 +31,7 @@ export class ListUserWorkflowsClientTool extends BaseClientTool {
try {
this.setState(ClientToolCallState.executing)
- const res = await fetch('/api/workflows/sync', { method: 'GET' })
+ const res = await fetch('/api/workflows', { method: 'GET' })
if (!res.ok) {
const text = await res.text().catch(() => '')
await this.markToolComplete(res.status, text || 'Failed to fetch workflows')
diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts
index 3bf99217b9..c529ce0420 100644
--- a/apps/sim/providers/ollama/index.ts
+++ b/apps/sim/providers/ollama/index.ts
@@ -14,7 +14,7 @@ import {
prepareToolsWithUsageControl,
trackForcedToolUsage,
} from '@/providers/utils'
-import { useOllamaStore } from '@/stores/ollama/store'
+import { useProvidersStore } from '@/stores/providers/store'
import { executeTool } from '@/tools'
const logger = createLogger('OllamaProvider')
@@ -78,13 +78,13 @@ export const ollamaProvider: ProviderConfig = {
try {
const response = await fetch(`${OLLAMA_HOST}/api/tags`)
if (!response.ok) {
- useOllamaStore.getState().setModels([])
+ useProvidersStore.getState().setModels('ollama', [])
logger.warn('Ollama service is not available. The provider will be disabled.')
return
}
const data = (await response.json()) as ModelsObject
this.models = data.models.map((model) => model.name)
- useOllamaStore.getState().setModels(this.models)
+ useProvidersStore.getState().setModels('ollama', this.models)
} catch (error) {
logger.warn('Ollama model instantiation failed. The provider will be disabled.', {
error: error instanceof Error ? error.message : 'Unknown error',
diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts
index 96240d8e03..5a71dd4cc6 100644
--- a/apps/sim/providers/utils.ts
+++ b/apps/sim/providers/utils.ts
@@ -30,7 +30,7 @@ import { openRouterProvider } from '@/providers/openrouter'
import type { ProviderConfig, ProviderId, ProviderToolConfig } from '@/providers/types'
import { xAIProvider } from '@/providers/xai'
import { useCustomToolsStore } from '@/stores/custom-tools/store'
-import { useOllamaStore } from '@/stores/ollama/store'
+import { useProvidersStore } from '@/stores/providers/store'
const logger = createLogger('ProviderUtils')
@@ -576,7 +576,8 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str
const hasUserKey = !!userProvidedKey
// Ollama models don't require API keys - they run locally
- const isOllamaModel = provider === 'ollama' || useOllamaStore.getState().models.includes(model)
+ const isOllamaModel =
+ provider === 'ollama' || useProvidersStore.getState().providers.ollama.models.includes(model)
if (isOllamaModel) {
return 'empty' // Ollama uses 'empty' as a placeholder API key
}
diff --git a/apps/sim/stores/constants.ts b/apps/sim/stores/constants.ts
index eabae88e75..1438aee028 100644
--- a/apps/sim/stores/constants.ts
+++ b/apps/sim/stores/constants.ts
@@ -1,5 +1,4 @@
export const API_ENDPOINTS = {
- SYNC: '/api/workflows/sync',
ENVIRONMENT: '/api/environment',
SCHEDULE: '/api/schedules',
SETTINGS: '/api/settings',
@@ -8,9 +7,6 @@ export const API_ENDPOINTS = {
WORKSPACE_ENVIRONMENT: (id: string) => `/api/workspaces/${id}/environment`,
}
-// Removed SYNC_INTERVALS - Socket.IO handles real-time sync
-
-// Copilot tool display names - shared between client and server
export const COPILOT_TOOL_DISPLAY_NAMES: Record = {
search_documentation: 'Searching documentation',
get_user_workflow: 'Analyzing your workflow',
@@ -30,7 +26,6 @@ export const COPILOT_TOOL_DISPLAY_NAMES: Record = {
reason: 'Reasoning about your workflow',
} as const
-// Past tense versions for completed tool calls
export const COPILOT_TOOL_PAST_TENSE: Record = {
search_documentation: 'Searched documentation',
get_user_workflow: 'Analyzed your workflow',
@@ -50,7 +45,6 @@ export const COPILOT_TOOL_PAST_TENSE: Record = {
reason: 'Finished reasoning',
} as const
-// Error versions for failed tool calls
export const COPILOT_TOOL_ERROR_NAMES: Record = {
search_documentation: 'Errored searching documentation',
get_user_workflow: 'Errored analyzing your workflow',
diff --git a/apps/sim/stores/ollama/store.ts b/apps/sim/stores/ollama/store.ts
deleted file mode 100644
index 4d52d15160..0000000000
--- a/apps/sim/stores/ollama/store.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { create } from 'zustand'
-import { createLogger } from '@/lib/logs/console/logger'
-import { updateOllamaProviderModels } from '@/providers/utils'
-import type { OllamaStore } from '@/stores/ollama/types'
-
-const logger = createLogger('OllamaStore')
-
-// Fetch models from the server API when on client side
-const fetchOllamaModels = async (): Promise => {
- try {
- const response = await fetch('/api/providers/ollama/models')
- if (!response.ok) {
- logger.warn('Failed to fetch Ollama models from API', {
- status: response.status,
- statusText: response.statusText,
- })
- return []
- }
- const data = await response.json()
- return data.models || []
- } catch (error) {
- logger.error('Error fetching Ollama models', {
- error: error instanceof Error ? error.message : 'Unknown error',
- })
- return []
- }
-}
-
-export const useOllamaStore = create((set, get) => ({
- models: [],
- isLoading: false,
- setModels: (models) => {
- set({ models })
- // Update the providers when models change
- updateOllamaProviderModels(models)
- },
-
- // Fetch models from API (client-side only)
- fetchModels: async () => {
- if (typeof window === 'undefined') {
- logger.info('Skipping client-side model fetch on server')
- return
- }
-
- if (get().isLoading) {
- logger.info('Model fetch already in progress')
- return
- }
-
- logger.info('Fetching Ollama models from API')
- set({ isLoading: true })
-
- try {
- const models = await fetchOllamaModels()
- logger.info('Successfully fetched Ollama models', {
- count: models.length,
- models,
- })
- get().setModels(models)
- } catch (error) {
- logger.error('Failed to fetch Ollama models', {
- error: error instanceof Error ? error.message : 'Unknown error',
- })
- } finally {
- set({ isLoading: false })
- }
- },
-}))
-
-// Auto-fetch models when the store is first accessed on the client
-if (typeof window !== 'undefined') {
- // Delay to avoid hydration issues
- setTimeout(() => {
- useOllamaStore.getState().fetchModels()
- }, 1000)
-}
diff --git a/apps/sim/stores/ollama/types.ts b/apps/sim/stores/ollama/types.ts
deleted file mode 100644
index 77b0fa26cd..0000000000
--- a/apps/sim/stores/ollama/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface OllamaStore {
- models: string[]
- isLoading: boolean
- setModels: (models: string[]) => void
- fetchModels: () => Promise
-}
diff --git a/apps/sim/stores/openrouter/store.ts b/apps/sim/stores/openrouter/store.ts
deleted file mode 100644
index 8bb5121b76..0000000000
--- a/apps/sim/stores/openrouter/store.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { create } from 'zustand'
-import { createLogger } from '@/lib/logs/console/logger'
-import { updateOpenRouterProviderModels } from '@/providers/utils'
-import type { OpenRouterStore } from '@/stores/openrouter/types'
-
-const logger = createLogger('OpenRouterStore')
-
-const fetchOpenRouterModels = async (): Promise => {
- try {
- const response = await fetch('/api/providers/openrouter/models')
- if (!response.ok) {
- logger.warn('Failed to fetch OpenRouter models from API', {
- status: response.status,
- statusText: response.statusText,
- })
- return []
- }
- const data = await response.json()
- return data.models || []
- } catch (error) {
- logger.error('Error fetching OpenRouter models', {
- error: error instanceof Error ? error.message : 'Unknown error',
- })
- return []
- }
-}
-
-export const useOpenRouterStore = create((set, get) => ({
- models: [],
- isLoading: false,
- setModels: (models) => {
- const unique = Array.from(new Set(models))
- set({ models: unique })
- updateOpenRouterProviderModels(models)
- },
-
- fetchModels: async () => {
- if (typeof window === 'undefined') {
- logger.info('Skipping client-side model fetch on server')
- return
- }
- if (get().isLoading) {
- logger.info('Model fetch already in progress')
- return
- }
- logger.info('Fetching OpenRouter models from API')
- set({ isLoading: true })
- try {
- const models = await fetchOpenRouterModels()
- logger.info('Successfully fetched OpenRouter models', {
- count: models.length,
- })
- get().setModels(models)
- } catch (error) {
- logger.error('Failed to fetch OpenRouter models', {
- error: error instanceof Error ? error.message : 'Unknown error',
- })
- } finally {
- set({ isLoading: false })
- }
- },
-}))
-
-if (typeof window !== 'undefined') {
- setTimeout(() => {
- useOpenRouterStore.getState().fetchModels()
- }, 1000)
-}
diff --git a/apps/sim/stores/openrouter/types.ts b/apps/sim/stores/openrouter/types.ts
deleted file mode 100644
index 19f94f5e57..0000000000
--- a/apps/sim/stores/openrouter/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface OpenRouterStore {
- models: string[]
- isLoading: boolean
- setModels: (models: string[]) => void
- fetchModels: () => Promise
-}
diff --git a/apps/sim/stores/organization/types.ts b/apps/sim/stores/organization/types.ts
index 1d7fef0566..e61f7cde2f 100644
--- a/apps/sim/stores/organization/types.ts
+++ b/apps/sim/stores/organization/types.ts
@@ -59,7 +59,6 @@ export interface OrganizationFormData {
logo: string
}
-// Organization billing and usage types
export interface MemberUsageData {
userId: string
userName: string
diff --git a/apps/sim/stores/providers/store.ts b/apps/sim/stores/providers/store.ts
new file mode 100644
index 0000000000..5c258ffda8
--- /dev/null
+++ b/apps/sim/stores/providers/store.ts
@@ -0,0 +1,126 @@
+import { create } from 'zustand'
+import { createLogger } from '@/lib/logs/console/logger'
+import { updateOllamaProviderModels, updateOpenRouterProviderModels } from '@/providers/utils'
+import type { ProviderConfig, ProviderName, ProvidersStore } from './types'
+
+const logger = createLogger('ProvidersStore')
+
+const PROVIDER_CONFIGS: Record = {
+ ollama: {
+ apiEndpoint: '/api/providers/ollama/models',
+ updateFunction: updateOllamaProviderModels,
+ },
+ openrouter: {
+ apiEndpoint: '/api/providers/openrouter/models',
+ dedupeModels: true,
+ updateFunction: updateOpenRouterProviderModels,
+ },
+}
+
+const fetchProviderModels = async (provider: ProviderName): Promise => {
+ try {
+ const config = PROVIDER_CONFIGS[provider]
+ const response = await fetch(config.apiEndpoint)
+
+ if (!response.ok) {
+ logger.warn(`Failed to fetch ${provider} models from API`, {
+ status: response.status,
+ statusText: response.statusText,
+ })
+ return []
+ }
+
+ const data = await response.json()
+ return data.models || []
+ } catch (error) {
+ logger.error(`Error fetching ${provider} models`, {
+ error: error instanceof Error ? error.message : 'Unknown error',
+ })
+ return []
+ }
+}
+
+export const useProvidersStore = create((set, get) => ({
+ providers: {
+ ollama: { models: [], isLoading: false },
+ openrouter: { models: [], isLoading: false },
+ },
+
+ setModels: (provider, models) => {
+ const config = PROVIDER_CONFIGS[provider]
+
+ const processedModels = config.dedupeModels ? Array.from(new Set(models)) : models
+
+ set((state) => ({
+ providers: {
+ ...state.providers,
+ [provider]: {
+ ...state.providers[provider],
+ models: processedModels,
+ },
+ },
+ }))
+
+ config.updateFunction(models)
+ },
+
+ fetchModels: async (provider) => {
+ if (typeof window === 'undefined') {
+ logger.info(`Skipping client-side ${provider} model fetch on server`)
+ return
+ }
+
+ const currentState = get().providers[provider]
+ if (currentState.isLoading) {
+ logger.info(`${provider} model fetch already in progress`)
+ return
+ }
+
+ logger.info(`Fetching ${provider} models from API`)
+
+ set((state) => ({
+ providers: {
+ ...state.providers,
+ [provider]: {
+ ...state.providers[provider],
+ isLoading: true,
+ },
+ },
+ }))
+
+ try {
+ const models = await fetchProviderModels(provider)
+ logger.info(`Successfully fetched ${provider} models`, {
+ count: models.length,
+ ...(provider === 'ollama' ? { models } : {}),
+ })
+ get().setModels(provider, models)
+ } catch (error) {
+ logger.error(`Failed to fetch ${provider} models`, {
+ error: error instanceof Error ? error.message : 'Unknown error',
+ })
+ } finally {
+ set((state) => ({
+ providers: {
+ ...state.providers,
+ [provider]: {
+ ...state.providers[provider],
+ isLoading: false,
+ },
+ },
+ }))
+ }
+ },
+
+ getProvider: (provider) => {
+ return get().providers[provider]
+ },
+}))
+
+if (typeof window !== 'undefined') {
+ setTimeout(() => {
+ const store = useProvidersStore.getState()
+ store.fetchModels('ollama')
+ store.fetchModels('openrouter')
+ }, 1000)
+}
diff --git a/apps/sim/stores/providers/types.ts b/apps/sim/stores/providers/types.ts
new file mode 100644
index 0000000000..4aabdf4f80
--- /dev/null
+++ b/apps/sim/stores/providers/types.ts
@@ -0,0 +1,19 @@
+export type ProviderName = 'ollama' | 'openrouter'
+
+export interface ProviderState {
+ models: string[]
+ isLoading: boolean
+}
+
+export interface ProvidersStore {
+ providers: Record
+ setModels: (provider: ProviderName, models: string[]) => void
+ fetchModels: (provider: ProviderName) => Promise
+ getProvider: (provider: ProviderName) => ProviderState
+}
+
+export interface ProviderConfig {
+ apiEndpoint: string
+ dedupeModels?: boolean
+ updateFunction: (models: string[]) => void | Promise
+}
diff --git a/apps/sim/stores/settings/environment/store.ts b/apps/sim/stores/settings/environment/store.ts
index 5b2b9e72b9..30f05fc0e2 100644
--- a/apps/sim/stores/settings/environment/store.ts
+++ b/apps/sim/stores/settings/environment/store.ts
@@ -10,7 +10,6 @@ export const useEnvironmentStore = create()((set, get) => ({
isLoading: false,
error: null,
- // Load environment variables from DB
loadEnvironmentVariables: async () => {
try {
set({ isLoading: true, error: null })
@@ -43,12 +42,10 @@ export const useEnvironmentStore = create()((set, get) => ({
}
},
- // Save environment variables to DB
saveEnvironmentVariables: async (variables: Record) => {
try {
set({ isLoading: true, error: null })
- // Transform variables to the format expected by the store
const transformedVariables = Object.entries(variables).reduce(
(acc, [key, value]) => ({
...acc,
@@ -57,10 +54,8 @@ export const useEnvironmentStore = create()((set, get) => ({
{}
)
- // Update local state immediately (optimistic update)
set({ variables: transformedVariables })
- // Send to DB
const response = await fetch(API_ENDPOINTS.ENVIRONMENT, {
method: 'POST',
headers: {
@@ -89,12 +84,10 @@ export const useEnvironmentStore = create()((set, get) => ({
isLoading: false,
})
- // Reload from DB to ensure consistency
get().loadEnvironmentVariables()
}
},
- // Workspace environment actions
loadWorkspaceEnvironment: async (workspaceId: string) => {
try {
set({ isLoading: true, error: null })
@@ -105,7 +98,6 @@ export const useEnvironmentStore = create()((set, get) => ({
}
const { data } = await response.json()
- // The UI component for environment modal will handle workspace section state locally.
set({ isLoading: false })
return data as {
workspace: Record
@@ -155,15 +147,6 @@ export const useEnvironmentStore = create()((set, get) => ({
}
},
- // Legacy method updated to use the new saveEnvironmentVariables
- setVariables: (variables: Record) => {
- get().saveEnvironmentVariables(variables)
- },
-
- getVariable: (key: string): string | undefined => {
- return get().variables[key]?.value
- },
-
getAllVariables: (): Record => {
return get().variables
},
diff --git a/apps/sim/stores/settings/environment/types.ts b/apps/sim/stores/settings/environment/types.ts
index 8d95a242db..78c484aacc 100644
--- a/apps/sim/stores/settings/environment/types.ts
+++ b/apps/sim/stores/settings/environment/types.ts
@@ -10,14 +10,9 @@ export interface EnvironmentState {
}
export interface EnvironmentStore extends EnvironmentState {
- // Legacy method
- setVariables: (variables: Record) => void
-
- // New methods for direct DB interaction
loadEnvironmentVariables: () => Promise
saveEnvironmentVariables: (variables: Record) => Promise
- // Workspace environment
loadWorkspaceEnvironment: (workspaceId: string) => Promise<{
workspace: Record
personal: Record
@@ -29,7 +24,5 @@ export interface EnvironmentStore extends EnvironmentState {
) => Promise
removeWorkspaceEnvironmentKeys: (workspaceId: string, keys: string[]) => Promise
- // Utility methods
- getVariable: (key: string) => string | undefined
getAllVariables: () => Record
}
diff --git a/apps/sim/stores/subscription/store.ts b/apps/sim/stores/subscription/store.ts
index 617bca1a11..40ef9ac908 100644
--- a/apps/sim/stores/subscription/store.ts
+++ b/apps/sim/stores/subscription/store.ts
@@ -142,7 +142,7 @@ export const useSubscriptionStore = create()(
loadUsageLimitData: async () => {
try {
- const response = await fetch('/api/usage-limits?context=user')
+ const response = await fetch('/api/usage?context=user')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
@@ -168,7 +168,7 @@ export const useSubscriptionStore = create()(
updateUsageLimit: async (newLimit: number) => {
try {
- const response = await fetch('/api/usage-limits?context=user', {
+ const response = await fetch('/api/usage?context=user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -240,7 +240,7 @@ export const useSubscriptionStore = create()(
// Load both subscription and usage limit data in parallel
const [subscriptionResponse, usageLimitResponse] = await Promise.all([
fetch('/api/billing?context=user'),
- fetch('/api/usage-limits?context=user'),
+ fetch('/api/usage?context=user'),
])
if (!subscriptionResponse.ok) {
diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts
index 465e6bc742..c5edb5a068 100644
--- a/apps/sim/stores/workflows/registry/store.ts
+++ b/apps/sim/stores/workflows/registry/store.ts
@@ -34,7 +34,7 @@ async function fetchWorkflowsFromDB(workspaceId?: string): Promise {
try {
useWorkflowRegistry.getState().setLoading(true)
- const url = new URL(API_ENDPOINTS.SYNC, window.location.origin)
+ const url = new URL(API_ENDPOINTS.WORKFLOWS, window.location.origin)
if (workspaceId) {
url.searchParams.append('workspaceId', workspaceId)
@@ -940,7 +940,7 @@ export const useWorkflowRegistry = create()(
},
}
- const response = await fetch('/api/workflows/sync', {
+ const response = await fetch('/api/workflows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({