self hosted fixes

This commit is contained in:
Saxon Fletcher committed 2026-03-16 11:10:13 +10:00
1 parent cab2639a54
commit 7c243afd17
5 files changed
+107 -39

No files matched your search

@@ -4,6 +4,7 @@ import useLatest from 'hooks/misc/useLatest'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { IS_PLATFORM } from 'lib/constants'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { parseAsString, useQueryState } from 'nuqs'
@@ -43,7 +44,7 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
const { data: org } = useSelectedOrganizationQuery()
const { mutate: sendEvent } = useSendEventMutation()
const { openSidebar, closeSidebar, activeSidebar } = useSidebarManagerSnapshot()
const useAgentChatSidebar = true // useFlag('agentChatSidebar')
const useAgentChatSidebar = !IS_PLATFORM || true // useFlag('agentChatSidebar')
const [sidebarURLParam, setSidebarUrlParam] = useQueryState('sidebar', parseAsString)
const [sidebarLocalStorage, setSidebarLocalStorage, { isSuccess: isLoadedLocalStorage }] =
@@ -52,6 +53,7 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
const sidebarURLParamRef = useLatest(sidebarURLParam)
const sidebarLocalStorageRef = useLatest(sidebarLocalStorage)
const renderAssistantSidebar = () => {
console.log('[sidebar] useAgentChatSidebar:', useAgentChatSidebar, 'IS_PLATFORM:', IS_PLATFORM, 'project?.ref:', project?.ref, 'project:', !!project)
if (useAgentChatSidebar && project?.ref) {
return (
<AgentChat
@@ -122,7 +124,6 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
sidebarURLParamRef.current as (typeof SIDEBAR_KEYS)[keyof typeof SIDEBAR_KEYS]
)
) {
console.log('Open sidebar based on URL')
openSidebar(sidebarURLParamRef.current)
} else if (
!!sidebarLocalStorageRef.current &&
@@ -31,25 +31,21 @@ export default async function handler(req: NextRequest) {
const authHeader = req.headers.get('Authorization') ?? ''
const token = authHeader.replace(/^Bearer\s+/i, '').trim()
if (!token) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
// Decode the JWT payload to get the user's sub claim.
// We trust the token is valid — the session was established by GoTrueClient.
// In self-hosted mode, there's no GoTrue session so the token may be absent.
let userId: string | undefined
try {
const payloadB64 = token.split('.')[1]
const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')))
userId = payload.sub ?? undefined
} catch {
return new Response(JSON.stringify({ error: 'Invalid token' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
if (token) {
try {
const payloadB64 = token.split('.')[1]
const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')))
userId = payload.sub ?? undefined
} catch {
return new Response(JSON.stringify({ error: 'Invalid token' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
}
const serviceKey = process.env.SUPABASE_SERVICE_KEY
@@ -74,7 +70,7 @@ export default async function handler(req: NextRequest) {
...(userId ? { 'X-Agent-User-Id': userId } : {}),
...(ref ? { 'X-Project-Ref': ref } : {}),
// Forward user's platform JWT for MCP server auth (mcp.supabase.com)
'X-User-Token': token,
...(token ? { 'X-User-Token': token } : {}),
},
body,
})
+5
View File
@@ -461,6 +461,8 @@ services:
context: ../packages/project-meta-api
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 3001:3001
depends_on:
db:
condition: service_healthy
@@ -478,6 +480,9 @@ services:
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_URL: http://kong:8000
PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
# Self-hosted: Logflare analytics for logs queries
LOGFLARE_URL: http://analytics:4000
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
# IS_PLATFORM only: set PROJECT_REF + SUPABASE_ACCESS_TOKEN (PAT) to enable
# querying the project database via the Supabase Management API.
# Leave PROJECT_REF unset for self-hosted (uses local pg-meta instead).
+1 -1
View File
@@ -19,11 +19,11 @@
"hono": "^4.10.6",
"jose": "^5.10.0",
"postgres": "^3.4.7",
"tsx": "4.20.3",
"zod": "3.25.76"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "4.20.3",
"typescript": "~5.9.0"
}
}
@@ -1,5 +1,6 @@
import { tool } from 'ai'
import { z } from 'zod'
import { PROJECT_REF } from '../db.js'
import { getToolContext } from './context.js'
const SUPABASE_API_URL = process.env.SUPABASE_API_URL ?? 'https://api.supabase.com'
@@ -8,6 +9,10 @@ const SERVICE_ROLE_KEY =
const SUPABASE_ACCESS_TOKEN = process.env.SUPABASE_ACCESS_TOKEN ?? null
const DEFAULT_LOGS_RANGE_MS = 60 * 60 * 1000
// Self-hosted: query Logflare directly (same as Studio's self-hosted logs path)
const LOGFLARE_URL = process.env.LOGFLARE_URL ?? null
const LOGFLARE_PRIVATE_ACCESS_TOKEN = process.env.LOGFLARE_PRIVATE_ACCESS_TOKEN ?? null
function resolveLogsDateRange(start?: string, end?: string) {
const now = new Date()
@@ -27,9 +32,7 @@ function resolveManagementApiAuthToken(userToken?: string | null): string | null
return normalized ?? SUPABASE_ACCESS_TOKEN
}
export const monitoringTools = {
executeLogsQuery: tool({
description: `Execute a SQL query against project logs via the Supabase Analytics API (BigQuery SQL dialect).
const BIGQUERY_LOGS_DESCRIPTION = `Execute a SQL query against project logs via the Supabase Analytics API (BigQuery SQL dialect).
Available log tables: edge_logs, function_logs, function_edge_logs, postgres_logs, auth_logs, realtime_logs, storage_logs.
@@ -89,30 +92,87 @@ Example — auth_logs:
Timestamp filtering: prefer iso_timestamp_start/iso_timestamp_end params over SQL WHERE on timestamp. Max range is 24h. If omitted, the tool defaults to the last hour through now.
Always include ORDER BY timestamp DESC and a LIMIT (max 1000).
When the user should also inspect or rerun the logs SQL themselves, first execute the query with this tool to inspect the results, then call renderSql with the same logs SQL and date range so both the assistant and user can interpret the same query output.`,
When the user should also inspect or rerun the logs SQL themselves, first execute the query with this tool to inspect the results, then call renderSql with the same logs SQL and date range so both the assistant and user can interpret the same query output.`
const POSTGRES_LOGS_DESCRIPTION = `Execute a SQL query against project logs via the Logflare Analytics API (PostgreSQL SQL dialect).
Available log tables: edge_logs, function_logs, function_edge_logs, postgres_logs, auth_logs, realtime_logs, storage_logs.
Columns are flat — do NOT use cross join unnest() or datetime(). Use standard PostgreSQL syntax.
Example — postgres_logs:
select id, timestamp, event_message
from postgres_logs
order by timestamp desc
limit 25
Example — edge_logs (API requests):
select id, timestamp, event_message
from edge_logs
order by timestamp desc
limit 25
Example — auth_logs:
select id, timestamp, event_message
from auth_logs
order by timestamp desc
limit 25
Timestamp filtering: prefer iso_timestamp_start/iso_timestamp_end params over SQL WHERE on timestamp. If omitted, the tool defaults to the last hour through now.
Always include ORDER BY timestamp DESC and a LIMIT (max 1000).
When the user should also inspect or rerun the logs SQL themselves, first execute the query with this tool to inspect the results, then call renderSql with the same logs SQL and date range so both the assistant and user can interpret the same query output.`
export const monitoringTools = {
executeLogsQuery: tool({
description: PROJECT_REF ? BIGQUERY_LOGS_DESCRIPTION : POSTGRES_LOGS_DESCRIPTION,
inputSchema: z.object({
sql: z.string().optional().describe('BigQuery SQL query. Use cross join unnest() to access nested metadata fields. Do not filter by timestamp in SQL — use iso_timestamp_start/iso_timestamp_end params instead.'),
sql: z.string().optional().describe('SQL query for logs. Do not filter by timestamp in SQL — use iso_timestamp_start/iso_timestamp_end params instead.'),
iso_timestamp_start: z.string().optional().describe('Optional ISO-8601 start timestamp (e.g. "2025-01-15T00:00:00Z"). If omitted, defaults to one hour before iso_timestamp_end or now.'),
iso_timestamp_end: z.string().optional().describe('Optional ISO-8601 end timestamp. If omitted, defaults to now. Range max 24h.'),
}),
execute: async ({ sql: sqlQuery, iso_timestamp_start, iso_timestamp_end }) => {
const ctx = getToolContext()
if (!ctx.projectRef) return { error: 'Project ref is not available' }
const authToken = resolveManagementApiAuthToken(ctx.userToken)
if (!authToken) {
return {
error:
'No auth token available for logs query. Provide a user token or set SUPABASE_ACCESS_TOKEN for background runners.',
}
}
const resolvedDateRange = resolveLogsDateRange(iso_timestamp_start, iso_timestamp_end)
const params = new URLSearchParams()
if (sqlQuery) params.set('sql', sqlQuery)
params.set('iso_timestamp_start', resolvedDateRange.start)
params.set('iso_timestamp_end', resolvedDateRange.end)
const qs = params.toString()
const url = `${SUPABASE_API_URL}/v1/projects/${ctx.projectRef}/analytics/endpoints/logs.all${qs ? `?${qs}` : ''}`
const resp = await fetch(url, { headers: { Authorization: `Bearer ${authToken}` } })
let resp: Response
if (PROJECT_REF) {
// Platform: call the Management API
const authToken = resolveManagementApiAuthToken(ctx.userToken)
if (!authToken) {
return {
error:
'No auth token available for logs query. Provide a user token or set SUPABASE_ACCESS_TOKEN for background runners.',
}
}
const params = new URLSearchParams()
if (sqlQuery) params.set('sql', sqlQuery)
params.set('iso_timestamp_start', resolvedDateRange.start)
params.set('iso_timestamp_end', resolvedDateRange.end)
const qs = params.toString()
const url = `${SUPABASE_API_URL}/v1/projects/${ctx.projectRef}/analytics/endpoints/logs.all${qs ? `?${qs}` : ''}`
resp = await fetch(url, { headers: { Authorization: `Bearer ${authToken}` } })
} else {
// Self-hosted: query Logflare directly (same as Studio's self-hosted logs path)
if (!LOGFLARE_URL || !LOGFLARE_PRIVATE_ACCESS_TOKEN) {
return { error: 'Logs are not available: LOGFLARE_URL and LOGFLARE_PRIVATE_ACCESS_TOKEN must be configured.' }
}
const url = new URL(`${LOGFLARE_URL}/api/endpoints/query/logs.all`)
url.searchParams.set('project', ctx.projectRef)
if (sqlQuery) url.searchParams.set('sql', sqlQuery)
url.searchParams.set('iso_timestamp_start', resolvedDateRange.start)
url.searchParams.set('iso_timestamp_end', resolvedDateRange.end)
resp = await fetch(url, {
headers: {
'x-api-key': LOGFLARE_PRIVATE_ACCESS_TOKEN,
'Content-Type': 'application/json',
Accept: 'application/json',
},
})
}
if (!resp.ok) return { error: `Logs API error (HTTP ${resp.status}): ${await resp.text()}` }
const data = await resp.json() as { result?: unknown[]; error?: unknown }
if (data.error) return { error: typeof data.error === 'string' ? data.error : JSON.stringify(data.error) }
@@ -126,6 +186,12 @@ When the user should also inspect or rerun the logs SQL themselves, first execut
execute: async () => {
const ctx = getToolContext()
if (!ctx.projectRef) return { error: 'Project ref is not available' }
// Disk utilization is only available via the Management API (platform only)
if (!PROJECT_REF) {
return { error: 'Disk utilization metrics are not available in self-hosted mode. Use database tools to query pg_stat or system catalogs instead.' }
}
const authToken = resolveManagementApiAuthToken(ctx.userToken)
if (!authToken) {
return {