diff --git a/apps/docs/content/_partials/quickstart_db_setup.mdx b/apps/docs/content/_partials/quickstart_db_setup.mdx index 76d441a365007..7c9489d64bb17 100644 --- a/apps/docs/content/_partials/quickstart_db_setup.mdx +++ b/apps/docs/content/_partials/quickstart_db_setup.mdx @@ -4,6 +4,10 @@ Go to [database.new](https://database.new) and create a new Supabase project. Alternatively, you can create a project using the Management API: + + + + ```bash # First, get your access token from https://supabase.com/dashboard/account/tokens export SUPABASE_ACCESS_TOKEN="your-access-token" @@ -24,9 +28,13 @@ curl -X POST https://api.supabase.com/v1/projects \ }' ``` + + + + When your project is up and running, go to the [Table Editor](/dashboard/project/_/editor), create a new table and insert some data. -Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create a `instruments` table with some sample data. +Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create an `instruments` table with some sample data. diff --git a/apps/docs/content/guides/functions/auth.mdx b/apps/docs/content/guides/functions/auth.mdx index 47f7be07c0f83..13895c1946be0 100644 --- a/apps/docs/content/guides/functions/auth.mdx +++ b/apps/docs/content/guides/functions/auth.mdx @@ -32,7 +32,10 @@ Following the [upcoming API key changes](https://github.com/orgs/supabase/discus ## Integrating with Supabase Auth -The simplest way to secure your endpoints is by using Supabase Auth to verify users. +Important notes to consider: + +- This is done _inside_ the `Deno.serve()` callback argument, so that the Authorization header is set for each request. +- Use `Deno.env.get('SUPABASE_URL')` to get the URL associated with your project. Using a value such as `http://localhost:54321` for local development will fail due to Docker containerization. <$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} /> diff --git a/apps/docs/content/guides/getting-started/mcp.mdx b/apps/docs/content/guides/getting-started/mcp.mdx index ca5a5fb17e12c..60459e4947d1c 100644 --- a/apps/docs/content/guides/getting-started/mcp.mdx +++ b/apps/docs/content/guides/getting-started/mcp.mdx @@ -22,7 +22,11 @@ Choose your Supabase platform, project, and MCP client and follow the installati ### Next steps -Your AI tool is now connected to your Supabase project or account using remote MCP. Try asking the AI tool to query your database using natural language commands. +Your MCP client automatically redirects you to log in to Supabase during setup. This opens a browser window where you can log in to your Supabase account and grant access to the MCP client. Be sure to choose the organization that contains the project you wish to work with. + +After you log in, check that the MCP server is connected. For instance, in Cursor, navigate to **Settings > Cursor Settings > Tools & MCP**. Depending on the client, you may need to restart it to connect and detect all tools after authorization. + +To verify the client has access to the MCP server tools, try asking it to query your project or database using natural language. For example: "What tables are there in the database? Use MCP tools." ## Manual authentication @@ -106,3 +110,7 @@ We recommend the following best practices to mitigate security risks when using - **Project scoping**: Scope your MCP server to a [specific project](https://github.com/supabase-community/supabase-mcp#project-scoped-mode), limiting access to only that project's resources. This prevents LLMs from accessing data from other projects in your Supabase account. - **Branching**: Use Supabase's [branching feature](/docs/guides/deployment/branching) to create a development branch for your database. This allows you to test changes in a safe environment before merging them to production. - **Feature groups**: The server allows you to enable or disable specific [tool groups](https://github.com/supabase-community/supabase-mcp#feature-groups), so you can control which tools are available to the LLM. This helps reduce the attack surface and limits the actions that LLMs can perform to only those that you need. + +## On GitHub + +The MCP server repository is available at [github.com/supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp). diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx index 88efee47e75eb..095acb0f487fd 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx @@ -152,7 +152,8 @@ export const CreateFunction = ({ config_params: convertConfigParams(func?.config_params).value, }) } - }, [visible, func]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible, func?.id]) const { data: protectedSchemas } = useProtectedSchemas() diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx index ec5556e88e826..6f6f3d2a77c03 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx @@ -168,21 +168,19 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP -
- +
+

Manage access for {member.username}

- + {isOptedIntoProjectLevelPermissions && ( -
+
)} -
+
{projectsRoleConfiguration.map((project) => { const name = project.ref === undefined ? 'All projects' : project.name const role = orgScopedRoles.find((r) => { @@ -246,7 +244,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP return (

{name}

@@ -269,7 +267,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP > @@ -323,6 +321,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP (
)} diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index 0c91f6e92a066..edb42974657e3 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -113,7 +113,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp const isRealtimeEnabled = realtimeEnabledTables.some((t) => t.id === table?.id) const { activeVariant: activeRealtimeVariant } = useRealtimeExperiment({ - projectInsertedAt: project?.inserted_at, isTable, isRealtimeEnabled, }) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 9b71ffaeba82b..ab128d6c7a4b2 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -130,7 +130,6 @@ export const TableEditor = ({ : realtimeEnabledTables.some((t) => t.id === table?.id) const { activeVariant: activeRealtimeVariant } = useRealtimeExperiment({ - projectInsertedAt: project?.inserted_at, isTable: !isNewRecord, isRealtimeEnabled, }) diff --git a/apps/studio/components/ui/OrganizationProjectSelector.tsx b/apps/studio/components/ui/OrganizationProjectSelector.tsx index d49d54eaa35a5..83481a26a5b93 100644 --- a/apps/studio/components/ui/OrganizationProjectSelector.tsx +++ b/apps/studio/components/ui/OrganizationProjectSelector.tsx @@ -44,6 +44,7 @@ interface OrganizationProjectSelectorSelectorProps { onInitialLoad?: (projects: OrgProject[]) => void isOptionDisabled?: (project: OrgProject) => boolean fetchOnMount?: boolean + modal?: boolean } export const OrganizationProjectSelector = ({ @@ -61,6 +62,7 @@ export const OrganizationProjectSelector = ({ onInitialLoad, isOptionDisabled, fetchOnMount = false, + modal = false, }: OrganizationProjectSelectorSelectorProps) => { const { data: organization } = useSelectedOrganizationQuery() const slug = _slug ?? organization?.slug @@ -126,7 +128,7 @@ export const OrganizationProjectSelector = ({ }, [isLoadingProjects, isSuccessProjects]) return ( - + {renderTrigger ? ( renderTrigger({ isLoading: isLoadingProjects || isFetching, project: selectedProject }) diff --git a/apps/studio/hooks/misc/useRealtimeExperiment.ts b/apps/studio/hooks/misc/useRealtimeExperiment.ts index aca021b312fe6..bf7740989d721 100644 --- a/apps/studio/hooks/misc/useRealtimeExperiment.ts +++ b/apps/studio/hooks/misc/useRealtimeExperiment.ts @@ -1,18 +1,9 @@ -import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' import { useEffect, useMemo, useRef } from 'react' import { usePHFlag } from 'hooks/ui/useFlag' import { IS_PLATFORM } from 'lib/constants' import { useTrack } from 'lib/telemetry/track' -dayjs.extend(utc) - -/** - * Days after project creation to be considered "new" for experiment targeting - */ -export const NEW_PROJECT_THRESHOLD_DAYS = 7 - export enum RealtimeButtonVariant { CONTROL = 'control', HIDE_BUTTON = 'hide-button', @@ -20,40 +11,22 @@ export enum RealtimeButtonVariant { } interface UseRealtimeExperimentOptions { - /** - * Project creation timestamp - */ - projectInsertedAt?: string - /** - * Whether the current context is a table (not a view/foreign table) - */ + /** Whether the current context is a table (not a view/foreign table) */ isTable?: boolean - /** - * Whether realtime is currently enabled for the table - */ + /** Whether realtime is currently enabled for the table */ isRealtimeEnabled?: boolean } interface UseRealtimeExperimentResult { - /** - * The active variant for this user/project, or null if not in experiment - */ + /** The active variant for this user, or null if not in experiment */ activeVariant: RealtimeButtonVariant | null - /** - * Whether this project is considered "new" for experiment targeting - */ - isNewProject: boolean } /** - * Hook to manage the realtime button A/B experiment logic. - * Handles variant determination, exposure tracking, and date validation. - * - * @param options Configuration for experiment targeting - * @returns Experiment state including active variant and project age + * Hook to manage the realtime button A/B experiment. + * User targeting is handled via PostHog experiment configuration. */ export function useRealtimeExperiment({ - projectInsertedAt, isTable = false, isRealtimeEnabled = false, }: UseRealtimeExperimentOptions): UseRealtimeExperimentResult { @@ -61,52 +34,35 @@ export function useRealtimeExperiment({ const realtimeButtonVariant = usePHFlag('realtimeButtonVariant') const hasTrackedExposure = useRef(false) - const isNewProject = useMemo(() => { - if (!projectInsertedAt) return false - - const insertedDate = dayjs.utc(projectInsertedAt) - if (!insertedDate.isValid()) { - return false - } - - return dayjs.utc().diff(insertedDate, 'day') < NEW_PROJECT_THRESHOLD_DAYS - }, [projectInsertedAt]) - const activeVariant = useMemo(() => { if (!IS_PLATFORM) return null - if (!isTable || !isNewProject) return null - if (!realtimeButtonVariant || realtimeButtonVariant === RealtimeButtonVariant.CONTROL) { - return null - } + if (!isTable) return null + if (!realtimeButtonVariant) return null + if (realtimeButtonVariant === RealtimeButtonVariant.CONTROL) return null return realtimeButtonVariant - }, [isTable, isNewProject, realtimeButtonVariant]) + }, [isTable, realtimeButtonVariant]) useEffect(() => { if (!IS_PLATFORM) return if (hasTrackedExposure.current) return - if (!isTable || !isNewProject || !projectInsertedAt) return - if (!realtimeButtonVariant) return + if (!isTable || !realtimeButtonVariant) return hasTrackedExposure.current = true try { - const insertedDate = dayjs.utc(projectInsertedAt) - if (!insertedDate.isValid()) return - - const daysSinceCreation = dayjs.utc().diff(insertedDate, 'day') - track('realtime_experiment_exposed', { + experiment_id: 'realtimeButtonVariant', variant: realtimeButtonVariant, table_has_realtime_enabled: isRealtimeEnabled, - days_since_project_creation: daysSinceCreation, }) - } catch { + } catch (error) { + // Reset tracking flag on error to allow retry hasTrackedExposure.current = false + console.error('Failed to track realtime experiment exposure:', error) } - }, [isTable, isNewProject, realtimeButtonVariant, projectInsertedAt, isRealtimeEnabled, track]) + }, [isTable, realtimeButtonVariant, isRealtimeEnabled, track]) return { activeVariant, - isNewProject, } } diff --git a/apps/www/data/contribute/index.ts b/apps/www/data/contribute/index.ts index 16a9aadaa8301..41e910e42d1c3 100644 --- a/apps/www/data/contribute/index.ts +++ b/apps/www/data/contribute/index.ts @@ -65,7 +65,9 @@ export async function getChannelCounts( sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 30) const since = sevenDaysAgo.toISOString() - let query = supabase.from('contribute_threads').select('source', { count: 'exact', head: false }) + let query = supabase + .from('v_contribute_threads') + .select('source', { count: 'exact', head: false }) // When searching, don't apply time/status filters to allow finding any matching threads if (!search || !search.trim()) { @@ -123,7 +125,7 @@ export async function getUnansweredThreads( const since = sevenDaysAgo.toISOString() let query = supabase - .from('contribute_threads') + .from('v_contribute_threads') .select( 'thread_id, subject, status, author, external_activity_url, created_at, source, product_areas, stack, category, sub_category, summary, first_msg_time, message_count' ) @@ -174,7 +176,7 @@ export async function getThreadById(id: string): Promise { const supabase = createClient(supabaseUrl, supabasePublishableKey) const { data, error } = await supabase - .from('contribute_threads') + .from('v_contribute_threads') .select( 'thread_id, subject, status, author, conversation, external_activity_url, created_at, source, product_areas, stack, category, sub_category, summary, first_msg_time, message_count, thread_key' ) @@ -222,7 +224,7 @@ export async function getAllProductAreas(): Promise { const supabase = createClient(supabaseUrl, supabasePublishableKey) const { data, error } = await supabase - .from('contribute_threads') + .from('v_contribute_threads') .select('product_areas') .in('status', ['unanswered', 'unresolved']) @@ -245,7 +247,7 @@ export async function getAllStacks(): Promise { const supabase = createClient(supabaseUrl, supabasePublishableKey) const { data, error } = await supabase - .from('contribute_threads') + .from('v_contribute_threads') .select('stack') .in('status', ['unanswered', 'unresolved']) @@ -283,7 +285,7 @@ export async function getUserActivity(author: string) { // Get user's threads const { data: threads, error: threadsError } = await supabase - .from('contribute_threads') + .from('v_contribute_threads') .select( 'thread_id, subject, status, author, external_activity_url, created_at, source, product_areas, stack, category, sub_category, summary, first_msg_time, message_count, thread_key' ) @@ -314,7 +316,7 @@ export async function getUserActivity(author: string) { if (threadKeys.length > 0) { const { data: replyThreadsData, error: replyThreadsError } = await supabase - .from('contribute_threads') + .from('v_contribute_threads') .select( 'thread_id, subject, status, author, external_activity_url, created_at, source, product_areas, stack, category, sub_category, summary, first_msg_time, message_count, thread_key' ) diff --git a/packages/common/auth.tsx b/packages/common/auth.tsx index 661c67daac193..ee8a1dacd1da7 100644 --- a/packages/common/auth.tsx +++ b/packages/common/auth.tsx @@ -159,27 +159,21 @@ gotrueClient.onAuthStateChange((event, session) => { }) /** - * Grabs the currently available access token, or calls getSession. + * Gets a current access token. + * + * Calls getSession, which will refresh the token if needed. */ export async function getAccessToken() { // ignore if server-side if (typeof window === 'undefined') return undefined - const aboutToExpire = currentSession?.expires_at - ? currentSession.expires_at - Math.ceil(Date.now() / 1000) < 30 - : false - - if (!currentSession || aboutToExpire) { - const { - data: { session }, - error, - } = await gotrueClient.getSession() - if (error) { - throw error - } - - return session?.access_token + const { + data: { session }, + error, + } = await gotrueClient.getSession() + if (error) { + throw error } - return currentSession.access_token + return session?.access_token } diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index a44fe3ed50f0c..bd6374e9f93ab 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -141,6 +141,7 @@ const LOCAL_STORAGE_KEYS_ALLOWLIST = [ LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN, LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0, LOCAL_STORAGE_KEYS.LINTER_SHOW_FOOTER, + LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR, ] export function clearLocalStorage() { diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index b0b87eb132961..443027b85d8f1 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -1716,18 +1716,12 @@ export interface HomeActivityStatClickedEvent { export interface RealtimeExperimentExposedEvent { action: 'realtime_experiment_exposed' properties: { - /** - * The experiment variant shown to the user - */ + /** The PostHog experiment/feature flag name */ + experiment_id: 'realtimeButtonVariant' + /** The experiment variant shown to the user */ variant: 'control' | 'hide-button' | 'triggers' - /** - * Whether the table already has realtime enabled - */ + /** Whether the table already has realtime enabled */ table_has_realtime_enabled: boolean - /** - * Days since project creation (to segment by new user cohorts) - */ - days_since_project_creation: number } groups: TelemetryGroups }