- Custom SMTP provider is required to update this configuration
+ Custom SMTP or Send Email hook is required to update this
+ configuration
- The built-in email service has a fixed rate limit. You will need
- to set up your own custom SMTP provider to update your email
- rate limit
+ The built-in email service has a fixed rate limit. Set up a
+ custom SMTP provider or enable the Send Email hook to update
+ your email rate limit
+
)
}
diff --git a/apps/studio/components/layouts/AppLayout/AdvisorButton.self-hosted.test.tsx b/apps/studio/components/layouts/AppLayout/AdvisorButton.self-hosted.test.tsx
new file mode 100644
index 0000000000000..9b575085b849d
--- /dev/null
+++ b/apps/studio/components/layouts/AppLayout/AdvisorButton.self-hosted.test.tsx
@@ -0,0 +1,70 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { AdvisorButton } from '@/components/layouts/AppLayout/AdvisorButton'
+import { render } from '@/tests/helpers'
+
+const {
+ mockUseProjectLintsQuery,
+ mockUseNotificationsV2Query,
+ mockUseAdvisorSignals,
+ mockToggleSidebar,
+} = vi.hoisted(() => ({
+ mockUseProjectLintsQuery: vi.fn(),
+ mockUseNotificationsV2Query: vi.fn(),
+ mockUseAdvisorSignals: vi.fn(),
+ mockToggleSidebar: vi.fn(),
+}))
+
+vi.mock('@/data/lint/lint-query', () => ({
+ useProjectLintsQuery: mockUseProjectLintsQuery,
+}))
+
+vi.mock('@/data/notifications/notifications-v2-query', () => ({
+ useNotificationsV2Query: mockUseNotificationsV2Query,
+}))
+
+vi.mock('@/components/ui/AdvisorPanel/useAdvisorSignals', () => ({
+ useAdvisorSignals: mockUseAdvisorSignals,
+}))
+
+vi.mock('@/lib/constants', async (importOriginal) => ({
+ ...(await importOriginal()),
+ IS_PLATFORM: false,
+}))
+
+vi.mock('@/state/sidebar-manager-state', () => ({
+ useSidebarManagerSnapshot: () => ({
+ toggleSidebar: mockToggleSidebar,
+ activeSidebar: undefined,
+ }),
+}))
+
+describe('AdvisorButton on self-hosted', () => {
+ beforeEach(() => {
+ mockUseProjectLintsQuery.mockReturnValue({ data: [], isPending: false, isError: false })
+ mockUseNotificationsV2Query.mockReturnValue({
+ data: { pages: [[]] },
+ isPending: false,
+ isError: false,
+ })
+ mockUseAdvisorSignals.mockReturnValue({
+ data: [],
+ isPending: false,
+ isError: false,
+ dismissSignal: vi.fn(),
+ })
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('disables the notifications query so no request is made to the platform endpoint', () => {
+ render()
+
+ expect(mockUseNotificationsV2Query).toHaveBeenCalledWith(
+ { filters: {}, limit: 20 },
+ { enabled: false }
+ )
+ })
+})
diff --git a/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx b/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx
index d9a1cb532f4c7..65cf75c02c01f 100644
--- a/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx
+++ b/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx
@@ -7,6 +7,7 @@ import { useAdvisorSignals } from '@/components/ui/AdvisorPanel/useAdvisorSignal
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useProjectLintsQuery } from '@/data/lint/lint-query'
import { useNotificationsV2Query } from '@/data/notifications/notifications-v2-query'
+import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
@@ -17,10 +18,10 @@ export const AdvisorButton = ({ projectRef }: { projectRef?: string }) => {
const { data: lints } = useProjectLintsQuery({ projectRef })
const { data: signalItems } = useAdvisorSignals({ projectRef })
- const { data: notificationsData } = useNotificationsV2Query({
- filters: {},
- limit: 20,
- })
+ const { data: notificationsData } = useNotificationsV2Query(
+ { filters: {}, limit: 20 },
+ { enabled: IS_PLATFORM }
+ )
const notifications = useMemo(() => {
return notificationsData?.pages.flatMap((page) => page) ?? []
}, [notificationsData?.pages])
diff --git a/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx b/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx
index b4517d5447c8e..3ee91192d7368 100644
--- a/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx
+++ b/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx
@@ -1,4 +1,4 @@
-import { LOCAL_STORAGE_KEYS } from 'common'
+import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useRef, type ReactNode } from 'react'
import {
@@ -44,7 +44,7 @@ export const FlyDeprecationBanner = () => {
const isExpired = Date.now() >= BANNER_EXPIRES_AT.getTime()
const onSignIn = router.pathname.startsWith('/sign-in')
- const shouldEvaluate = !isExpired && !onSignIn && isSuccess && !acknowledged
+ const shouldEvaluate = IS_PLATFORM && !isExpired && !onSignIn && isSuccess && !acknowledged
const { isReady, primaries, branches } = useFlyDeprecationProjects({ enabled: shouldEvaluate })
diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts
index 24a3ac0c1d6eb..77203ce9a6088 100644
--- a/apps/ui-library/config/docs.ts
+++ b/apps/ui-library/config/docs.ts
@@ -112,18 +112,18 @@ export const componentPages: SidebarNavGroup = {
},
{
title: 'Realtime Chat',
- supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
+ supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react', 'vue', 'nuxtjs'],
href: '/docs/nextjs/realtime-chat',
items: [],
commandItemLabel: 'Realtime Chat',
},
{
- title: 'Infinite Query Hook',
- supportedFrameworks: [],
- href: '/docs/infinite-query-hook',
+ title: 'Infinite Query',
+ supportedFrameworks: ['react', 'vue'],
+ href: '/docs/react/infinite-query',
new: true,
items: [],
- commandItemLabel: 'Infinite Query Hook',
+ commandItemLabel: 'Infinite Query',
},
],
}
diff --git a/apps/ui-library/content/docs/nuxtjs/realtime-chat.mdx b/apps/ui-library/content/docs/nuxtjs/realtime-chat.mdx
new file mode 100644
index 0000000000000..e1435d6941f35
--- /dev/null
+++ b/apps/ui-library/content/docs/nuxtjs/realtime-chat.mdx
@@ -0,0 +1,128 @@
+---
+title: Realtime Chat
+description: Real-time chat component for collaborative applications
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+This block assumes that you have already installed a Supabase client for Vue from the previous step.
+
+
+
+## Introduction
+
+The Realtime Chat component provides a complete chat interface that enables users to exchange messages in real-time within a shared room.
+
+## How it works under the hood
+
+This chat component uses **Supabase Realtime Broadcast** to send and receive messages between connected clients.
+
+Messages sent through Broadcast are:
+
+- delivered in real time to other connected clients
+- **not stored** unless you handle persistence yourself
+- **not guaranteed** to arrive if the client disconnects
+- scoped to a specific `roomName`, which corresponds to a broadcast channel
+
+This design keeps latency extremely low, but it means you should use the `onMessage` callback if you want to store messages permanently or show chat history on page load.
+
+## Usage
+
+### Basic usage
+
+```html
+
+
+
+
+
+```
+
+### With initial messages
+
+```html
+
+
+
+
+
+```
+
+### Storing messages
+
+```html
+
+
+
+
+
+```
+
+## Features
+
+- Real-time message synchronization
+- Message persistence support with `onMessage` and `messages` props
+- Customizable message appearance
+- Automatic scroll-to-bottom on new messages
+- Room-based isolation for scoped conversations
+- Low-latency updates using Supabase Realtime
+
+## Props
+
+| Prop | Type | Description |
+| ------------ | ----------------------------------- | ------------------------------------------------------------- |
+| `roomName` | `string` | Unique identifier for the shared chat room. |
+| `username` | `string` | Name of the current user; used to identify message senders. |
+| `onMessage?` | `(messages: ChatMessage[]) => void` | Optional callback to handle messages, useful for persistence. |
+| `messages?` | `ChatMessage[]` | Optional initial messages to display in the chat. |
+
+### ChatMessage type
+
+```typescript
+interface ChatMessage {
+ id: string
+ content: string
+ user: {
+ name: string
+ }
+ createdAt: string
+}
+```
+
+## Further reading
+
+- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
+- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
diff --git a/apps/ui-library/content/docs/infinite-query-hook.mdx b/apps/ui-library/content/docs/react/infinite-query.mdx
similarity index 100%
rename from apps/ui-library/content/docs/infinite-query-hook.mdx
rename to apps/ui-library/content/docs/react/infinite-query.mdx
diff --git a/apps/ui-library/content/docs/vue/infinite-query.mdx b/apps/ui-library/content/docs/vue/infinite-query.mdx
new file mode 100644
index 0000000000000..fd713011c63ea
--- /dev/null
+++ b/apps/ui-library/content/docs/vue/infinite-query.mdx
@@ -0,0 +1,281 @@
+---
+title: Infinite Query Composable
+description: Vue Composable for infinite lists, fetching data from Supabase.
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+
+
+## Introduction
+
+The Infinite Query Composable provides a single Vue Composable which will make it easier to load data progressively from your Supabase database. It handles data fetching and pagination state, It is meant to be used with infinite lists or tables.
+The Composable is fully typed, provided you have generated and setup your database types.
+
+## Adding types
+
+Before using this composable, we **highly** recommend you setup database types in your project. This will make the composable fully-typesafe. More info about generating Typescript types from database schema [here](https://supabase.com/docs/guides/api/rest/generating-types)
+
+## Props
+
+| Prop | Type | Description |
+| --------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
+| `tableName` | `string` | **Required.** The name of the Supabase table to fetch data from. |
+| `columns` | `string` | Columns to select from the table. Defaults to `'*'`. |
+| `pageSize` | `number` | Number of items to fetch per page. Defaults to `20`. |
+| `trailingQuery` | `(query: SupabaseSelectBuilder) => SupabaseSelectBuilder` | Function to apply filters or sorting to the Supabase query. |
+
+## Return type
+
+data, count, isSuccess, isLoading, isFetching, error, hasMore, fetchNextPage
+
+| Prop | Type | Description |
+| --------------- | ------------- | ----------------------------------------------------------------------------------- |
+| `data` | `TableData[]` | An array of fetched items. |
+| `count` | `number` | Number of total items in the database. It takes `trailingQuery` into consideration. |
+| `isSuccess` | `boolean` | It's true if the last API call succeeded. |
+| `isLoading` | `boolean` | It's true only for the initial fetch. |
+| `isFetching` | `boolean` | It's true for the initial and all incremental fetches. |
+| `error` | `any` | The error from the last fetch. |
+| `hasMore` | `boolean` | Whether the query has finished fetching all items from the database |
+| `fetchNextPage` | `() => void` | Sends a new request for the next items |
+
+## Type safety
+
+The hook will use the typed defined on your Supabase client if they're setup ([more info](https://supabase.com/docs/reference/javascript/typescript-support)).
+
+The hook also supports an custom defined result type by using `useInfiniteQuery`. For example, if you have a custom type for `Product`, you can use it like this `useInfiniteQuery`.
+
+## Usage
+
+### With sorting
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+### With filtering on search params
+
+This example will filter based on a search param like `example.com/?q=hello`.
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+## Reusable components
+
+### Infinite list (fetches as you scroll)
+
+The following component abstracts the composable into a component. It includes few utility components for no results and end of the list.
+
+```vue
+
+
+
+
+
+
+
+
No results.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
You've reached the end.
+
+
+
+
+
+```
+
+Use the `InfiniteList` component with the [Todo List](https://supabase.com/dashboard/project/_/sql/quickstarts) quickstart.
+
+Add `` to a page to see it in action.
+Ensure the [Checkbox](https://ui.shadcn.com/docs/components/checkbox) component from shadcn/ui is installed, and [regenerate/download](https://supabase.com/docs/guides/api/rest/generating-types) types after running the quickstart.
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+ The Todo List table has Row Level Security (RLS) enabled by default. Feel free disable it
+ temporarily while testing. With RLS enabled, you will get an [empty
+ array](https://supabase.com/docs/guides/troubleshooting/why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx)
+ of results by default. [Read
+ more](https://supabase.com/docs/guides/database/postgres/row-level-security) about RLS.
+
+
+## Further reading
+
+- [Generating Typescript types from the database](https://supabase.com/docs/reference/javascript/typescript-support)
+- [Supabase Database API](https://supabase.com/docs/reference/javascript/select)
+- [Supabase Pagination](https://supabase.com/docs/reference/javascript/select#pagination)
+- [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
diff --git a/apps/ui-library/content/docs/vue/realtime-chat.mdx b/apps/ui-library/content/docs/vue/realtime-chat.mdx
new file mode 100644
index 0000000000000..2fc60e7f0c9c9
--- /dev/null
+++ b/apps/ui-library/content/docs/vue/realtime-chat.mdx
@@ -0,0 +1,115 @@
+---
+title: Realtime Chat
+description: Real-time chat component for collaborative applications
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+This block assumes that you have already installed a Supabase client for Vue from the previous step.
+
+
+
+## Introduction
+
+The Realtime Chat component provides a complete chat interface that enables users to exchange messages in real-time within a shared room.
+
+## Usage
+
+### Basic usage
+
+```html
+
+
+
+
+
+```
+
+### With initial messages
+
+```html
+
+
+
+
+
+```
+
+### Storing messages
+
+```html
+
+
+
+
+
+```
+
+## Features
+
+- Real-time message synchronization
+- Message persistence support with `onMessage` and `messages` props
+- Customizable message appearance
+- Automatic scroll-to-bottom on new messages
+- Room-based isolation for scoped conversations
+- Low-latency updates using Supabase Realtime
+
+## Props
+
+| Prop | Type | Description |
+| ------------ | ----------------------------------- | ------------------------------------------------------------- |
+| `roomName` | `string` | Unique identifier for the shared chat room. |
+| `username` | `string` | Name of the current user; used to identify message senders. |
+| `onMessage?` | `(messages: ChatMessage[]) => void` | Optional callback to handle messages, useful for persistence. |
+| `messages?` | `ChatMessage[]` | Optional initial messages to display in the chat. |
+
+### ChatMessage type
+
+```typescript
+interface ChatMessage {
+ id: string
+ content: string
+ user: {
+ name: string
+ }
+ createdAt: string
+}
+```
+
+## Further reading
+
+- [Realtime Broadcast](https://supabase.com/docs/guides/realtime/broadcast)
+- [Realtime authorization](https://supabase.com/docs/guides/realtime/authorization)
diff --git a/apps/ui-library/public/r/infinite-query-composable.json b/apps/ui-library/public/r/infinite-query-composable.json
new file mode 100644
index 0000000000000..c1003d9239c51
--- /dev/null
+++ b/apps/ui-library/public/r/infinite-query-composable.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "infinite-query-composable",
+ "type": "registry:component",
+ "title": "Infinite Query for Vue and Supabase",
+ "description": "Vue Composable for infinite lists, fetching data from Supabase.",
+ "dependencies": [
+ "@supabase/supabase-js@latest",
+ "@supabase/postgrest-js@*"
+ ],
+ "files": [
+ {
+ "path": "registry/default/infinite-query/vue/composables/useInfiniteQuery.ts",
+ "content": "import { PostgrestQueryBuilder, type PostgrestClientOptions } from '@supabase/postgrest-js'\nimport { type SupabaseClient } from '@supabase/supabase-js'\nimport { computed, onMounted, reactive, toRefs, watch } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\nconst supabase = createClient()\n\ntype SupabaseClientType = typeof supabase\n\ntype IfAny = 0 extends 1 & T ? Y : N\n\ntype Database =\n SupabaseClientType extends SupabaseClient\n ? IfAny<\n U,\n {\n public: {\n Tables: Record\n Views: Record\n Functions: Record\n }\n },\n U\n >\n : {\n public: {\n Tables: Record\n Views: Record\n Functions: Record\n }\n }\n\ntype DatabaseSchema = Database['public']\ntype SupabaseTableName = keyof DatabaseSchema['Tables']\ntype SupabaseTableData = DatabaseSchema['Tables'][T]['Row']\n\ntype DefaultClientOptions = PostgrestClientOptions\n\ntype SupabaseSelectBuilder = ReturnType<\n PostgrestQueryBuilder<\n DefaultClientOptions,\n DatabaseSchema,\n DatabaseSchema['Tables'][T],\n T\n >['select']\n>\n\nexport type SupabaseQueryHandler = (\n query: SupabaseSelectBuilder\n) => SupabaseSelectBuilder\n\nexport interface UseInfiniteQueryProps {\n tableName: T\n columns?: string\n pageSize?: number\n trailingQuery?: SupabaseQueryHandler\n}\n\ninterface State {\n data: TData[]\n count: number\n isSuccess: boolean\n isLoading: boolean\n isFetching: boolean\n error: Error | null\n hasInitialFetch: boolean\n requestCounter: number\n}\n\n// --------------------\n// Composable\n// --------------------\n\nexport function useInfiniteQuery<\n TData extends SupabaseTableData,\n T extends SupabaseTableName = SupabaseTableName,\n>(props: UseInfiniteQueryProps) {\n const state = reactive>({\n data: [],\n count: 0,\n isSuccess: false,\n isLoading: false,\n isFetching: false,\n error: null,\n hasInitialFetch: false,\n requestCounter: 0,\n })\n\n const pageSize = computed(() => props.pageSize ?? 20)\n const columns = computed(() => props.columns ?? '*')\n\n const fetchPage = async (skip: number) => {\n // Capture the current request token to validate this request later\n const requestToken = state.requestCounter\n\n if (state.hasInitialFetch && (state.isFetching || state.data.length >= state.count)) {\n return\n }\n\n // Early return if request has been invalidated\n if (requestToken !== state.requestCounter) {\n return\n }\n\n state.isFetching = true\n\n let query = supabase\n .from(props.tableName)\n .select(columns.value, { count: 'exact' }) as unknown as SupabaseSelectBuilder\n\n if (props.trailingQuery) {\n query = props.trailingQuery(query)\n }\n\n const { data, count, error } = await query.range(skip, skip + pageSize.value - 1)\n\n // Verify that this request is still valid before mutating state\n if (requestToken !== state.requestCounter) {\n state.isFetching = false\n return\n }\n\n if (error) {\n console.error(error)\n state.error = error\n } else {\n state.data.push(...(data as TData[]))\n state.count = count || 0\n state.isSuccess = true\n state.error = null\n }\n\n state.isFetching = false\n }\n\n const fetchNextPage = async () => {\n if (state.isFetching) return\n await fetchPage(state.data.length)\n }\n\n const reset = () => {\n state.data = []\n state.count = 0\n state.isSuccess = false\n state.error = null\n state.hasInitialFetch = false\n state.isFetching = false\n state.requestCounter++\n }\n\n const initialize = async () => {\n state.isLoading = true\n reset()\n\n await fetchNextPage()\n\n state.isLoading = false\n state.hasInitialFetch = true\n }\n\n // React-style deps → Vue watch\n watch(\n () => [props.tableName, props.columns, props.pageSize],\n () => {\n if (state.hasInitialFetch) {\n initialize()\n }\n }\n )\n\n onMounted(() => {\n if (!state.hasInitialFetch) {\n initialize()\n }\n })\n\n const hasMore = computed(() => state.count > state.data.length)\n\n return {\n ...toRefs(state),\n hasMore,\n fetchNextPage,\n refresh: initialize,\n }\n}\n",
+ "type": "registry:component",
+ "target": "composables/useInfiniteQuery.ts"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/ui-library/public/r/realtime-chat-nuxtjs.json b/apps/ui-library/public/r/realtime-chat-nuxtjs.json
new file mode 100644
index 0000000000000..8c84e2c84251e
--- /dev/null
+++ b/apps/ui-library/public/r/realtime-chat-nuxtjs.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "realtime-chat-nuxtjs",
+ "type": "registry:block",
+ "title": "Realtime Chat",
+ "description": "Component which provides a realtime chat interface.",
+ "dependencies": [
+ "@supabase/supabase-js@latest",
+ "lucide-vue-next@latest"
+ ],
+ "registryDependencies": [
+ "button",
+ "input"
+ ],
+ "files": [
+ {
+ "path": "registry/default/realtime-chat/nuxtjs/app/components/realtime-chat.vue",
+ "content": "\n\n\n