mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
self hosted fixes
This commit is contained in:
1 parent
cab2639a54
commit
7c243afd17
5 files changed
+107
-39
No files matched your search
+3
-2
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user