Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/docs/content/_partials/quickstart_db_setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

</StepHikeCompact.Details>

<StepHikeCompact.Code>

```bash
# First, get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
Expand All @@ -24,9 +28,13 @@ curl -X POST https://api.supabase.com/v1/projects \
}'
```

</StepHikeCompact.Code>

<StepHikeCompact.Details>

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.

</StepHikeCompact.Details>

Expand Down
5 changes: 4 additions & 1 deletion apps/docs/content/guides/functions/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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": "" }} />

Expand Down
10 changes: 9 additions & 1 deletion apps/docs/content/guides/getting-started/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,19 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
<SheetContent
showClose={false}
size="default"
className={cn('bg-surface-200 p-0 flex flex-row gap-0 !min-w-[400px]')}
className="bg-surface-200 p-0 flex flex-row gap-0 md:w-[600px] lg:w-[600px] w-full"
>
<div className={cn('flex flex-col grow w-full')}>
<SheetHeader
className={cn('py-3 flex flex-row justify-between gap-x-4 items-center border-b')}
>
<div className="flex flex-col grow w-full">
<SheetHeader className="py-3 flex flex-row justify-between gap-x-4 items-center border-b bg-transparent">
<p className="truncate" title={`Manage access for ${member.username}`}>
Manage access for {member.username}
</p>
<DocsButton href={`${DOCS_URL}/guides/platform/access-control`} />
</SheetHeader>

<SheetSection className="h-full overflow-auto flex flex-col gap-y-4">
<SheetSection className="h-full overflow-auto flex flex-col">
{isOptedIntoProjectLevelPermissions && (
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-4 border-b border-border pb-4">
<Switch
disabled={cannotAddAnyRoles}
checked={isApplyingRoleToAllProjects}
Expand Down Expand Up @@ -234,7 +232,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
</Collapsible_Shadcn_>
)}

<div className="flex flex-col gap-y-2">
<div className="flex flex-col divide-y divide-border">
{projectsRoleConfiguration.map((project) => {
const name = project.ref === undefined ? 'All projects' : project.name
const role = orgScopedRoles.find((r) => {
Expand All @@ -246,7 +244,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
return (
<div
key={`${project.ref}-${project.roleId}`}
className="flex items-center justify-between"
className="flex items-center justify-between py-2"
>
<p className="text-sm">{name}</p>

Expand All @@ -269,7 +267,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
>
<SelectTrigger_Shadcn_
className={cn(
'text-sm h-10 w-56',
' w-40',
role?.name === undefined && 'text-foreground-light'
)}
>
Expand Down Expand Up @@ -323,6 +321,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
<OrganizationProjectSelector
open={showProjectDropdown}
setOpen={setShowProjectDropdown}
modal={true}
onSelect={onSelectProject}
renderTrigger={() => (
<Button type="default" className="w-min">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ const GitHubIntegrationConnectionForm = ({
<Button
type="default"
className="justify-start h-[34px] w-full"
disabled={isLoadingGitHubRepos}
disabled={disabled || isLoadingGitHubRepos}
loading={isLoadingGitHubRepos}
icon={
<div className="bg-black shadow rounded p-1 w-6 h-6 flex justify-center items-center">
Expand Down Expand Up @@ -704,7 +704,7 @@ const GitHubIntegrationConnectionForm = ({
<Button
type="outline"
onClick={handleRemoveIntegration}
disabled={disabled || isDeletingConnection}
disabled={isDeletingConnection}
loading={isDeletingConnection}
>
Disable integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { cn } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import GitHubIntegrationConnectionForm from './GitHubIntegrationConnectionForm'

Expand Down Expand Up @@ -71,20 +70,21 @@ export const GitHubSection = () => {
branch, keep your production branch in sync, and automatically create preview
branches for every pull request.
</p>
{promptProPlanUpgrade ? (

{promptProPlanUpgrade && (
<div className="mb-6">
<UpgradeToPro
source="github-integration"
featureProposition="use GitHub integrations"
primaryText="Upgrade to unlock GitHub integration"
primaryText={`Upgrade to ${!!existingConnection ? 'manage' : 'unlock'} GitHub integration`}
secondaryText="Connect your GitHub repository to automatically sync preview branches and deploy changes."
/>
</div>
) : (
<div className={cn(promptProPlanUpgrade && 'opacity-25 pointer-events-none')}>
<GitHubIntegrationConnectionForm connection={existingConnection} />
</div>
)}
<GitHubIntegrationConnectionForm
disabled={promptProPlanUpgrade}
connection={existingConnection}
/>
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
4 changes: 3 additions & 1 deletion apps/studio/components/ui/OrganizationProjectSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface OrganizationProjectSelectorSelectorProps {
onInitialLoad?: (projects: OrgProject[]) => void
isOptionDisabled?: (project: OrgProject) => boolean
fetchOnMount?: boolean
modal?: boolean
}

export const OrganizationProjectSelector = ({
Expand All @@ -61,6 +62,7 @@ export const OrganizationProjectSelector = ({
onInitialLoad,
isOptionDisabled,
fetchOnMount = false,
modal = false,
}: OrganizationProjectSelectorSelectorProps) => {
const { data: organization } = useSelectedOrganizationQuery()
const slug = _slug ?? organization?.slug
Expand Down Expand Up @@ -126,7 +128,7 @@ export const OrganizationProjectSelector = ({
}, [isLoadingProjects, isSuccessProjects])

return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={modal}>
<PopoverTrigger_Shadcn_ asChild>
{renderTrigger ? (
renderTrigger({ isLoading: isLoadingProjects || isFetching, project: selectedProject })
Expand Down
74 changes: 15 additions & 59 deletions apps/studio/hooks/misc/useRealtimeExperiment.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,68 @@
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',
TRIGGERS = 'triggers',
}

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 {
const track = useTrack()
const realtimeButtonVariant = usePHFlag<RealtimeButtonVariant>('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,
}
}
Loading
Loading