mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-13 07:55:09 -05:00
feat(internal): added internal api base url for internal calls (#3212)
* feat(internal): added internal api base url for internal calls * make validation on http more lax
This commit is contained in:
@@ -13,6 +13,7 @@ BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# NextJS (Required)
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL
|
||||
|
||||
# Security (Required)
|
||||
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
|
||||
/** A2A v0.3 JSON-RPC method names */
|
||||
export const A2A_METHODS = {
|
||||
@@ -118,7 +118,7 @@ export interface ExecuteRequestResult {
|
||||
export async function buildExecuteRequest(
|
||||
config: ExecuteRequestConfig
|
||||
): Promise<ExecuteRequestResult> {
|
||||
const url = `${getBaseUrl()}/api/workflows/${config.workflowId}/execute`
|
||||
const url = `${getInternalApiBaseUrl()}/api/workflows/${config.workflowId}/execute`
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
let useInternalAuth = false
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
||||
setupCommonApiMocks()
|
||||
mockCryptoUuid()
|
||||
|
||||
// Mock getBaseUrl to return localhost for tests
|
||||
vi.doMock('@/lib/core/utils/urls', () => ({
|
||||
getBaseUrl: vi.fn(() => 'http://localhost:3000'),
|
||||
getInternalApiBaseUrl: vi.fn(() => 'http://localhost:3000'),
|
||||
getBaseDomain: vi.fn(() => 'localhost:3000'),
|
||||
getEmailDomain: vi.fn(() => 'localhost:3000'),
|
||||
}))
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
createRequestTracker,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
import { isUuidV4 } from '@/executor/constants'
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const stateResponse = await fetch(
|
||||
`${getBaseUrl()}/api/workflows/${checkpoint.workflowId}/state`,
|
||||
`${getInternalApiBaseUrl()}/api/workflows/${checkpoint.workflowId}/state`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
||||
@@ -72,6 +72,7 @@ describe('MCP Serve Route', () => {
|
||||
}))
|
||||
vi.doMock('@/lib/core/utils/urls', () => ({
|
||||
getBaseUrl: () => 'http://localhost:3000',
|
||||
getInternalApiBaseUrl: () => 'http://localhost:3000',
|
||||
}))
|
||||
vi.doMock('@/lib/core/execution-limits', () => ({
|
||||
getMaxExecutionTimeout: () => 10_000,
|
||||
|
||||
@@ -22,7 +22,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServeAPI')
|
||||
@@ -285,7 +285,7 @@ async function handleToolsCall(
|
||||
)
|
||||
}
|
||||
|
||||
const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute`
|
||||
const executeUrl = `${getInternalApiBaseUrl()}/api/workflows/${tool.workflowId}/execute`
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
|
||||
if (publicServerOwnerId) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import {
|
||||
type RegenerateStateInput,
|
||||
regenerateWorkflowStateIds,
|
||||
@@ -115,15 +115,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
// Step 3: Save the workflow state using the existing state endpoint (like imports do)
|
||||
// Ensure variables in state are remapped for the new workflow as well
|
||||
const workflowStateWithVariables = { ...workflowState, variables: remappedVariables }
|
||||
const stateResponse = await fetch(`${getBaseUrl()}/api/workflows/${newWorkflowId}/state`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Forward the session cookie for authentication
|
||||
cookie: request.headers.get('cookie') || '',
|
||||
},
|
||||
body: JSON.stringify(workflowStateWithVariables),
|
||||
})
|
||||
const stateResponse = await fetch(
|
||||
`${getInternalApiBaseUrl()}/api/workflows/${newWorkflowId}/state`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Forward the session cookie for authentication
|
||||
cookie: request.headers.get('cookie') || '',
|
||||
},
|
||||
body: JSON.stringify(workflowStateWithVariables),
|
||||
}
|
||||
)
|
||||
|
||||
if (!stateResponse.ok) {
|
||||
logger.error(`[${requestId}] Failed to save workflow state for template use`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
@@ -79,7 +79,7 @@ export class RouterBlockHandler implements BlockHandler {
|
||||
const providerId = getProviderFromModel(routerConfig.model)
|
||||
|
||||
try {
|
||||
const url = new URL('/api/providers', getBaseUrl())
|
||||
const url = new URL('/api/providers', getInternalApiBaseUrl())
|
||||
if (ctx.userId) url.searchParams.set('userId', ctx.userId)
|
||||
|
||||
const messages = [{ role: 'user', content: routerConfig.prompt }]
|
||||
@@ -209,7 +209,7 @@ export class RouterBlockHandler implements BlockHandler {
|
||||
const providerId = getProviderFromModel(routerConfig.model)
|
||||
|
||||
try {
|
||||
const url = new URL('/api/providers', getBaseUrl())
|
||||
const url = new URL('/api/providers', getInternalApiBaseUrl())
|
||||
if (ctx.userId) url.searchParams.set('userId', ctx.userId)
|
||||
|
||||
const messages = [{ role: 'user', content: routerConfig.context }]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { HTTP } from '@/executor/constants'
|
||||
|
||||
export async function buildAuthHeaders(): Promise<Record<string, string>> {
|
||||
@@ -16,7 +16,8 @@ export async function buildAuthHeaders(): Promise<Record<string, string>> {
|
||||
}
|
||||
|
||||
export function buildAPIUrl(path: string, params?: Record<string, string>): URL {
|
||||
const url = new URL(path, getBaseUrl())
|
||||
const baseUrl = path.startsWith('/api/') ? getInternalApiBaseUrl() : getBaseUrl()
|
||||
const url = new URL(path, baseUrl)
|
||||
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
|
||||
@@ -220,6 +220,7 @@ export const env = createEnv({
|
||||
SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features
|
||||
SOCKET_PORT: z.number().optional(), // Port for WebSocket server
|
||||
PORT: z.number().optional(), // Main application port
|
||||
INTERNAL_API_BASE_URL: z.string().optional(), // Optional internal base URL for server-side self-calls; must include protocol if set (e.g., http://sim-app.namespace.svc.cluster.local:3000)
|
||||
ALLOWED_ORIGINS: z.string().optional(), // CORS allowed origins
|
||||
|
||||
// OAuth Integration Credentials - All optional, enables third-party integrations
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
import { isProd } from '@/lib/core/config/feature-flags'
|
||||
|
||||
function hasHttpProtocol(url: string): boolean {
|
||||
return /^https?:\/\//i.test(url)
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url: string): string {
|
||||
if (hasHttpProtocol(url)) {
|
||||
return url
|
||||
}
|
||||
|
||||
const protocol = isProd ? 'https://' : 'http://'
|
||||
return `${protocol}${url}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base URL of the application from NEXT_PUBLIC_APP_URL
|
||||
* This ensures webhooks, callbacks, and other integrations always use the correct public URL
|
||||
@@ -8,7 +21,7 @@ import { isProd } from '@/lib/core/config/feature-flags'
|
||||
* @throws Error if NEXT_PUBLIC_APP_URL is not configured
|
||||
*/
|
||||
export function getBaseUrl(): string {
|
||||
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')
|
||||
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')?.trim()
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
@@ -16,12 +29,26 @@ export function getBaseUrl(): string {
|
||||
)
|
||||
}
|
||||
|
||||
if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
|
||||
return baseUrl
|
||||
return normalizeBaseUrl(baseUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base URL used by server-side internal API calls.
|
||||
* Falls back to NEXT_PUBLIC_APP_URL when INTERNAL_API_BASE_URL is not set.
|
||||
*/
|
||||
export function getInternalApiBaseUrl(): string {
|
||||
const internalBaseUrl = getEnv('INTERNAL_API_BASE_URL')?.trim()
|
||||
if (!internalBaseUrl) {
|
||||
return getBaseUrl()
|
||||
}
|
||||
|
||||
const protocol = isProd ? 'https://' : 'http://'
|
||||
return `${protocol}${baseUrl}`
|
||||
if (!hasHttpProtocol(internalBaseUrl)) {
|
||||
throw new Error(
|
||||
'INTERNAL_API_BASE_URL must include protocol (http:// or https://), e.g. http://sim-app.default.svc.cluster.local:3000'
|
||||
)
|
||||
}
|
||||
|
||||
return internalBaseUrl
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
import { getProviderFromModel } from '@/providers/utils'
|
||||
@@ -61,7 +61,7 @@ async function queryKnowledgeBase(
|
||||
})
|
||||
|
||||
// Call the knowledge base search API directly
|
||||
const searchUrl = `${getBaseUrl()}/api/knowledge/search`
|
||||
const searchUrl = `${getInternalApiBaseUrl()}/api/knowledge/search`
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -539,8 +539,8 @@ async function executeMistralOCRRequest(
|
||||
const isInternalRoute = url.startsWith('/')
|
||||
|
||||
if (isInternalRoute) {
|
||||
const { getBaseUrl } = await import('@/lib/core/utils/urls')
|
||||
url = `${getBaseUrl()}${url}`
|
||||
const { getInternalApiBaseUrl } = await import('@/lib/core/utils/urls')
|
||||
url = `${getInternalApiBaseUrl()}${url}`
|
||||
}
|
||||
|
||||
let headers =
|
||||
|
||||
@@ -11,7 +11,7 @@ import { and, eq, isNull, or, sql } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing'
|
||||
import { pollingIdempotency } from '@/lib/core/idempotency/service'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import type { GmailAttachment } from '@/tools/gmail/types'
|
||||
import { downloadAttachments, extractAttachmentInfo } from '@/tools/gmail/utils'
|
||||
@@ -691,7 +691,7 @@ async function processEmails(
|
||||
`[${requestId}] Sending ${config.includeRawEmail ? 'simplified + raw' : 'simplified'} email payload for ${email.id}`
|
||||
)
|
||||
|
||||
const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { FetchMessageObject, MailboxLockObject } from 'imapflow'
|
||||
import { ImapFlow } from 'imapflow'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { pollingIdempotency } from '@/lib/core/idempotency/service'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'
|
||||
|
||||
const logger = createLogger('ImapPollingService')
|
||||
@@ -639,7 +639,7 @@ async function processEmails(
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -12,7 +12,7 @@ import { htmlToText } from 'html-to-text'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing'
|
||||
import { pollingIdempotency } from '@/lib/core/idempotency'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'
|
||||
|
||||
@@ -601,7 +601,7 @@ async function processOutlookEmails(
|
||||
`[${requestId}] Processing email: ${email.subject} from ${email.from?.emailAddress?.address}`
|
||||
)
|
||||
|
||||
const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'
|
||||
|
||||
const logger = createLogger('RssPollingService')
|
||||
@@ -376,7 +376,7 @@ async function processRssItems(
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { parseMcpToolId } from '@/lib/mcp/utils'
|
||||
import { isCustomTool, isMcpTool } from '@/executor/constants'
|
||||
import { resolveSkillContent } from '@/executor/handlers/agent/skills-resolver'
|
||||
@@ -285,7 +285,7 @@ export async function executeTool(
|
||||
`[${requestId}] Tool ${toolId} needs access token for credential: ${contextParams.credential}`
|
||||
)
|
||||
try {
|
||||
const baseUrl = getBaseUrl()
|
||||
const baseUrl = getInternalApiBaseUrl()
|
||||
|
||||
const workflowId = contextParams._context?.workflowId
|
||||
const userId = contextParams._context?.userId
|
||||
@@ -597,12 +597,12 @@ async function executeToolRequest(
|
||||
const requestParams = formatRequestParams(tool, params)
|
||||
|
||||
try {
|
||||
const baseUrl = getBaseUrl()
|
||||
const endpointUrl =
|
||||
typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
|
||||
const isInternalRoute = endpointUrl.startsWith('/api/')
|
||||
const baseUrl = isInternalRoute ? getInternalApiBaseUrl() : getBaseUrl()
|
||||
|
||||
const fullUrlObj = new URL(endpointUrl, baseUrl)
|
||||
const isInternalRoute = endpointUrl.startsWith('/api/')
|
||||
|
||||
if (isInternalRoute) {
|
||||
const workflowId = params._context?.workflowId
|
||||
@@ -922,7 +922,7 @@ async function executeMcpTool(
|
||||
|
||||
const { serverId, toolName } = parseMcpToolId(toolId)
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const baseUrl = getInternalApiBaseUrl()
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import type { BaseImageRequestBody } from '@/tools/openai/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
@@ -122,7 +122,7 @@ export const imageTool: ToolConfig = {
|
||||
if (imageUrl && !base64Image) {
|
||||
try {
|
||||
logger.info('Fetching image from URL via proxy...')
|
||||
const baseUrl = getBaseUrl()
|
||||
const baseUrl = getInternalApiBaseUrl()
|
||||
const proxyUrl = new URL('/api/tools/image', baseUrl)
|
||||
proxyUrl.searchParams.append('url', imageUrl)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { AGENT, isCustomTool } from '@/executor/constants'
|
||||
import { getCustomTool } from '@/hooks/queries/custom-tools'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
@@ -373,7 +373,7 @@ async function fetchCustomToolFromAPI(
|
||||
const identifier = customToolId.replace('custom_', '')
|
||||
|
||||
try {
|
||||
const baseUrl = getBaseUrl()
|
||||
const baseUrl = getInternalApiBaseUrl()
|
||||
const url = new URL('/api/tools/custom', baseUrl)
|
||||
|
||||
if (workflowId) {
|
||||
|
||||
@@ -120,6 +120,18 @@
|
||||
"format": "uri",
|
||||
"description": "Public application URL"
|
||||
},
|
||||
"INTERNAL_API_BASE_URL": {
|
||||
"type": "string",
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "uri"
|
||||
},
|
||||
{
|
||||
"const": ""
|
||||
}
|
||||
],
|
||||
"description": "Optional server-side internal base URL for internal /api self-calls (must include http:// or https://); defaults to NEXT_PUBLIC_APP_URL when unset"
|
||||
},
|
||||
"BETTER_AUTH_URL": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
|
||||
@@ -70,6 +70,7 @@ app:
|
||||
# Application URLs
|
||||
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
|
||||
BETTER_AUTH_URL: "http://localhost:3000"
|
||||
INTERNAL_API_BASE_URL: "" # Optional server-side internal base URL for /api self-calls (include http:// or https://); falls back to NEXT_PUBLIC_APP_URL when empty
|
||||
# SOCKET_SERVER_URL: Auto-detected when realtime.enabled=true (uses internal service)
|
||||
# Only set this if using an external WebSocket service with realtime.enabled=false
|
||||
NEXT_PUBLIC_SOCKET_URL: "http://localhost:3002" # Public WebSocket URL for browsers
|
||||
|
||||
Reference in New Issue
Block a user