+ )}
+ 7 ? 'h-[210px]' : ''}>
+ {options.map((option) => (
+ {
+ setOpen(false)
+ onSelectOption(selectedValue)
+ }}
+ className="cursor-pointer"
+ >
+
+ {option.displayName}
+
+ ))}
+
+ {hasNextPage && }
+
+ >
+ )}
diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx
index d384746540b0e..ff60b0eca6b37 100644
--- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx
+++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx
@@ -3,14 +3,12 @@
import type {
Branch,
Org,
- Project,
Variable,
} from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils'
-import type { ProjectKeys, ProjectSettings } from '~/lib/fetch/projectApi'
import { Check, Copy } from 'lucide-react'
import Link from 'next/link'
-import { useEffect, useMemo } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import { withErrorBoundary } from 'react-error-boundary'
import { proxy, useSnapshot } from 'valtio'
@@ -31,11 +29,16 @@ import {
toOrgProjectValue,
} from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils'
import { useCopy } from '~/hooks/useCopy'
+import { useDebounce } from '~/hooks/useDebounce'
import { useBranchesQuery } from '~/lib/fetch/branches'
import { useOrganizationsQuery } from '~/lib/fetch/organizations'
-import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler'
-import { useProjectSettingsQuery, useProjectKeysQuery } from '~/lib/fetch/projectApi'
-import { isProjectPaused, useProjectsQuery } from '~/lib/fetch/projects'
+import { useSupavisorConfigQuery, type SupavisorConfigData } from '~/lib/fetch/pooler'
+import { useProjectKeysQuery, useProjectSettingsQuery } from '~/lib/fetch/projectApi'
+import {
+ isProjectPaused,
+ ProjectInfoInfinite,
+ useProjectsInfiniteQuery,
+} from '~/lib/fetch/projects-infinite'
import { retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
@@ -67,8 +70,8 @@ type VariableDataState =
const projectsStore = proxy({
selectedOrg: null as Org | null,
- selectedProject: null as Project | null,
- setSelectedOrgProject: (org: Org | null, project: Project | null) => {
+ selectedProject: null as ProjectInfoInfinite | null,
+ setSelectedOrgProject: (org: Org | null, project: ProjectInfoInfinite | null) => {
projectsStore.selectedOrg = org
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_ORG, org?.id.toString())
@@ -90,6 +93,9 @@ function OrgProjectSelector() {
const isUserLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()
+ const [search, setSearch] = useState('')
+ const debouncedSearch = useDebounce(search, 500)
+
const { selectedOrg, selectedProject, setSelectedOrgProject } = useSnapshot(projectsStore)
const {
@@ -97,11 +103,21 @@ function OrgProjectSelector() {
isPending: organizationsIsPending,
isError: organizationsIsError,
} = useOrganizationsQuery({ enabled: isLoggedIn })
+
const {
- data: projects,
+ data: projectsData,
isPending: projectsIsPending,
isError: projectsIsError,
- } = useProjectsQuery({ enabled: isLoggedIn })
+ isFetching,
+ isFetchingNextPage,
+ hasNextPage,
+ fetchNextPage,
+ } = useProjectsInfiniteQuery(
+ { search: search.length === 0 ? search : debouncedSearch },
+ { enabled: isLoggedIn }
+ )
+ const projects =
+ useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || []
const anyIsPending = organizationsIsPending || projectsIsPending
const anyIsError = organizationsIsError || projectsIsError
@@ -141,7 +157,7 @@ function OrgProjectSelector() {
const storedMaybeProjectRef = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT)
let storedOrg: Org | undefined
- let storedProject: Project | undefined
+ let storedProject: ProjectInfoInfinite | undefined
if (storedMaybeOrgId && storedMaybeProjectRef) {
storedOrg = organizations!.find((org) => org.id === Number(storedMaybeOrgId))
storedProject = projects!.find((project) => project.ref === storedMaybeProjectRef)
@@ -167,6 +183,11 @@ function OrgProjectSelector() {
stateSummary === 'loggedIn.dataSuccess.hasNoData'
}
options={formattedData}
+ selectedDisplayName={
+ selectedOrg && selectedProject
+ ? toDisplayNameOrgProject(selectedOrg, selectedProject)
+ : undefined
+ }
selectedOption={
selectedOrg && selectedProject ? toOrgProjectValue(selectedOrg, selectedProject) : undefined
}
@@ -181,6 +202,13 @@ function OrgProjectSelector() {
setSelectedOrgProject(org, project)
}
}}
+ search={search}
+ isFetching={isFetching}
+ isFetchingNextPage={isFetchingNextPage}
+ hasNextPage={hasNextPage}
+ fetchNextPage={fetchNextPage}
+ setSearch={setSearch}
+ useCommandSearch={false}
/>
)
}
@@ -190,6 +218,7 @@ function BranchSelector() {
const isLoggedIn = useIsLoggedIn()
const { selectedProject, selectedBranch, setSelectedBranch } = useSnapshot(projectsStore)
+ const [branchSearch, setBranchSearch] = useState('')
const projectPaused = isProjectPaused(selectedProject)
const hasBranches = selectedProject?.is_branch_enabled ?? false
@@ -253,7 +282,10 @@ function BranchSelector() {
stateSummary === 'loggedIn.branches.dataSuccess.noData'
}
options={formattedData}
+ selectedDisplayName={selectedBranch?.name}
selectedOption={selectedBranch ? toBranchValue(selectedBranch) : undefined}
+ search={branchSearch}
+ setSearch={setBranchSearch}
onSelectOption={(option) => {
const [branchId] = fromBranchValue(option)
if (branchId) {
diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts
index cb06766dbad7b..15e5c1128ae62 100644
--- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts
+++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts
@@ -1,9 +1,8 @@
import type { BranchesData } from '~/lib/fetch/branches'
import type { OrganizationsData } from '~/lib/fetch/organizations'
-import type { ProjectsData } from '~/lib/fetch/projects'
+import { ProjectInfoInfinite } from '~/lib/fetch/projects-infinite'
export type Org = OrganizationsData[number]
-export type Project = ProjectsData[number]
export type Branch = BranchesData[number]
export type Variable = 'url' | 'publishable' | 'anon' | 'sessionPooler'
@@ -39,11 +38,17 @@ type DeepReadonly = {
readonly [P in keyof T]: DeepReadonly
}
-export function toDisplayNameOrgProject(org: DeepReadonly, project: DeepReadonly) {
+export function toDisplayNameOrgProject(
+ org: DeepReadonly,
+ project: DeepReadonly
+) {
return `${org.name} / ${project.name}`
}
-export function toOrgProjectValue(org: DeepReadonly, project: DeepReadonly) {
+export function toOrgProjectValue(
+ org: DeepReadonly,
+ project: DeepReadonly
+) {
return escapeDoubleQuotes(
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
JSON.stringify([org.id, project.ref, removeDoubleQuotes(toDisplayNameOrgProject(org, project))])
diff --git a/apps/docs/content/_partials/auth_rate_limits.mdx b/apps/docs/content/_partials/auth_rate_limits.mdx
index 2b3f19c434a2a..b8d2debdc2be0 100644
--- a/apps/docs/content/_partials/auth_rate_limits.mdx
+++ b/apps/docs/content/_partials/auth_rate_limits.mdx
@@ -2,9 +2,9 @@
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 4 emails per hour as of 14th July 2023. As of 21 Oct 2023, this has been updated to auth.rate_limits.email.inbuilt_smtp_per_hour emails per hour. You can only change this with your own custom SMTP setup. |
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to auth.rate_limits.otp.requests_per_hour OTPs per hour. Is customizable. |
-| Send OTPs or magic links | `/auth/v1/otp` | Last request | Defaults to auth.rate_limits.otp.period window before a new request is allowed. Is customizable. |
-| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to auth.rate_limits.signup_confirmation.period window before a new request is allowed. Is customizable. |
-| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to auth.rate_limits.password_reset.period window before a new request is allowed. Is customizable. |
+| Send OTPs or magic links | `/auth/v1/otp` | Last request of the user | Defaults to auth.rate_limits.otp.period window before a new request is allowed to the same user. Is customizable. |
+| Signup confirmation request | `/auth/v1/signup` | Last request of the user | Defaults to auth.rate_limits.signup_confirmation.period window before a new request is allowed to the same user. Is customizable. |
+| Password Reset Request | `/auth/v1/recover` | Last request of the user | Defaults to auth.rate_limits.password_reset.period window before a new request is allowed to the same user. Is customizable. |
| Verification requests | `/auth/v1/verify` | IP Address | auth.rate_limits.verification.requests_per_hour requests per hour (with bursts up to auth.rate_limits.verification.requests_burst requests) |
| Token refresh requests | `/auth/v1/token` | IP Address | auth.rate_limits.token_refresh.requests_per_hour requests per hour (with bursts up to auth.rate_limits.token_refresh.requests_burst requests) |
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | auth.rate_limits.mfa.requests_per_hour requests per hour (with bursts up to auth.rate_limits.verification.mfa requests) |
diff --git a/apps/docs/content/guides/database/extensions/pgvector.mdx b/apps/docs/content/guides/database/extensions/pgvector.mdx
index e0d11c1fb7a52..56918c381ccdb 100644
--- a/apps/docs/content/guides/database/extensions/pgvector.mdx
+++ b/apps/docs/content/guides/database/extensions/pgvector.mdx
@@ -22,7 +22,7 @@ Vector similarity refers to a measure of the similarity between two related item
### Embeddings
-This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) for retrieval augmented generation.
+This is particularly useful if you're building AI applications with large language models. You can create and store [embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) for retrieval augmented generation (RAG).
## Usage
diff --git a/apps/docs/content/guides/database/full-text-search.mdx b/apps/docs/content/guides/database/full-text-search.mdx
index 27a08579f17d4..0446c7d3af042 100644
--- a/apps/docs/content/guides/database/full-text-search.mdx
+++ b/apps/docs/content/guides/database/full-text-search.mdx
@@ -3,20 +3,11 @@ id: 'full-text-search'
title: 'Full Text Search'
description: 'How to use full text search in PostgreSQL.'
subtitle: 'How to use full text search in PostgreSQL.'
-tocVideo: 'b-mgca_2Oe4'
+tocVideo: 'GRwIa-ce7RA'
---
Postgres has built-in functions to handle `Full Text Search` queries. This is like a "search engine" within Postgres.
-
-
-
-
## Preparation
For this guide we'll use the following example data:
@@ -100,6 +91,13 @@ Converts a query string into tokens to match. `to_tsquery()` stands for "to text
This conversion step is important because we will want to "fuzzy match" on keywords.
For example if a user searches for `eggs`, and a column has the value `egg`, we probably still want to return a match.
+Postgres provides several functions to create tsquery objects:
+
+- **`to_tsquery()`** - Requires manual specification of operators (`&`, `|`, `!`)
+- **`plainto_tsquery()`** - Converts plain text to an AND query: `plainto_tsquery('english', 'fat rats')` → `'fat' & 'rat'`
+- **`phraseto_tsquery()`** - Creates phrase queries: `phraseto_tsquery('english', 'fat rats')` → `'fat' <-> 'rat'`
+- **`websearch_to_tsquery()`** - Supports web search syntax with quotes, "or", and negation
+
### Match: `@@` [#match]
The `@@` symbol is the "match" symbol for Full Text Search. It returns any matches between a `to_tsvector` result and a `to_tsquery` result.
@@ -751,9 +749,84 @@ When you want the search term to include a phrase or multiple words, you can con
select * from search_books_by_title_prefix('Little+Puppy');
```
+## Web search syntax with `websearch_to_tsquery()` [#websearch-to-tsquery]
+
+The `websearch_to_tsquery()` function provides an intuitive search syntax similar to popular web search engines, making it ideal for user-facing search interfaces.
+
+### Basic usage
+
+
+
+
+```sql
+select *
+from books
+where to_tsvector(description) @@ websearch_to_tsquery('english', 'green eggs');
+```
+
+
+
+
+```js
+const { data, error } = await supabase
+ .from('books')
+ .select()
+ .textSearch('description', 'green eggs', { type: 'websearch' })
+```
+
+
+
+
+### Quoted phrases
+
+Use quotes to search for exact phrases:
+
+```sql
+select * from books
+where to_tsvector(description || ' ' || title) @@ websearch_to_tsquery('english', '"Green Eggs"');
+-- Matches documents containing "Green" immediately followed by "Eggs"
+```
+
+### OR searches
+
+Use "or" (case-insensitive) to search for multiple terms:
+
+```sql
+select * from books
+where to_tsvector(description) @@ websearch_to_tsquery('english', 'puppy or rabbit');
+-- Matches documents containing either "puppy" OR "rabbit"
+```
+
+### Negation
+
+Use a dash (-) to exclude terms:
+
+```sql
+select * from books
+where to_tsvector(description) @@ websearch_to_tsquery('english', 'animal -rabbit');
+-- Matches documents containing "animal" but NOT "rabbit"
+```
+
+### Complex queries
+
+Combine multiple operators for sophisticated searches:
+
+```sql
+select * from books
+where to_tsvector(description || ' ' || title) @@
+ websearch_to_tsquery('english', '"Harry Potter" or "Dr. Seuss" -vegetables');
+-- Matches books by "Harry Potter" or "Dr. Seuss" but excludes those mentioning vegetables
+```
+
## Creating indexes
-Now that we have Full Text Search working, let's create an `index`. This will allow Postgres to "build" the documents preemptively so that they
+Now that you have Full Text Search working, create an `index`. This allows Postgres to "build" the documents preemptively so that they
don't need to be created at the time we execute the query. This will make our queries much faster.
### Searchable columns
@@ -1142,6 +1215,323 @@ data = client.from_('books').select().text_search('description', "'big' & !'litt
$Show>
+## Ranking search results [#ranking]
+
+Postgres provides ranking functions to sort search results by relevance, helping you present the most relevant matches first. Since ranking functions need to be computed server-side, use RPC functions and generated columns.
+
+### Creating a search function with ranking [#search-function-ranking]
+
+First, create a Postgres function that handles search and ranking:
+
+```sql
+create or replace function search_books(search_query text)
+returns table(id int, title text, description text, rank real) as $$
+begin
+ return query
+ select
+ books.id,
+ books.title,
+ books.description,
+ ts_rank(to_tsvector('english', books.description), to_tsquery(search_query)) as rank
+ from books
+ where to_tsvector('english', books.description) @@ to_tsquery(search_query)
+ order by rank desc;
+end;
+$$ language plpgsql;
+```
+
+Now you can call this function from your client:
+
+
+
+
+```js
+const { data, error } = await supabase.rpc('search_books', { search_query: 'big' })
+```
+
+
+<$Show if="sdk:dart">
+
+
+```dart
+final result = await client
+ .rpc('search_books', params: { 'search_query': 'big' });
+```
+
+
+$Show>
+<$Show if="sdk:python">
+
+
+```python
+data = client.rpc('search_books', { 'search_query': 'big' }).execute()
+```
+
+
+$Show>
+
+
+```sql
+select * from search_books('big');
+```
+
+
+
+
+### Ranking with weighted columns [#weighted-ranking]
+
+Postgres allows you to assign different importance levels to different parts of your documents using weight labels. This is especially useful when you want matches in certain fields (like titles) to rank higher than matches in other fields (like descriptions).
+
+#### Understanding weight labels
+
+Postgres uses four weight labels: **A**, **B**, **C**, and **D**, where:
+
+- **A** = Highest importance (weight 1.0)
+- **B** = High importance (weight 0.4)
+- **C** = Medium importance (weight 0.2)
+- **D** = Low importance (weight 0.1)
+
+#### Creating weighted search columns
+
+First, create a weighted tsvector column that gives titles higher priority than descriptions:
+
+```sql
+-- Add a weighted fts column
+alter table books
+add column fts_weighted tsvector
+generated always as (
+ setweight(to_tsvector('english', title), 'A') ||
+ setweight(to_tsvector('english', description), 'B')
+) stored;
+
+-- Create index for the weighted column
+create index books_fts_weighted on books using gin (fts_weighted);
+```
+
+Now create a search function that uses this weighted column:
+
+```sql
+create or replace function search_books_weighted(search_query text)
+returns table(id int, title text, description text, rank real) as $$
+begin
+ return query
+ select
+ books.id,
+ books.title,
+ books.description,
+ ts_rank(books.fts_weighted, to_tsquery(search_query)) as rank
+ from books
+ where books.fts_weighted @@ to_tsquery(search_query)
+ order by rank desc;
+end;
+$$ language plpgsql;
+```
+
+#### Custom weight arrays
+
+You can also specify custom weights by providing a weight array to `ts_rank()`:
+
+```sql
+create or replace function search_books_custom_weights(search_query text)
+returns table(id int, title text, description text, rank real) as $$
+begin
+ return query
+ select
+ books.id,
+ books.title,
+ books.description,
+ ts_rank(
+ '{0.0, 0.2, 0.5, 1.0}'::real[], -- Custom weights {D, C, B, A}
+ books.fts_weighted,
+ to_tsquery(search_query)
+ ) as rank
+ from books
+ where books.fts_weighted @@ to_tsquery(search_query)
+ order by rank desc;
+end;
+$$ language plpgsql;
+```
+
+This example uses custom weights where:
+
+- A-labeled terms (titles) have maximum weight (1.0)
+- B-labeled terms (descriptions) have medium weight (0.5)
+- C-labeled terms have low weight (0.2)
+- D-labeled terms are ignored (0.0)
+
+#### Using the weighted search
+
+
+
+
+```js
+// Search with standard weighted ranking
+const { data, error } = await supabase.rpc('search_books_weighted', { search_query: 'Harry' })
+
+// Search with custom weights
+const { data: customData, error: customError } = await supabase.rpc('search_books_custom_weights', {
+ search_query: 'Harry',
+})
+```
+
+
+<$Show if="sdk:python">
+
+
+```python
+# Search with standard weighted ranking
+data = client.rpc('search_books_weighted', { 'search_query': 'Harry' }).execute()
+
+# Search with custom weights
+custom_data = client.rpc('search_books_custom_weights', { 'search_query': 'Harry' }).execute()
+```
+
+
+$Show>
+
+
+```sql
+-- Standard weighted search
+select * from search_books_weighted('Harry');
+
+-- Custom weighted search
+select * from search_books_custom_weights('Harry');
+```
+
+
+
+
+#### Practical example with results
+
+Say you search for "Harry". With weighted columns:
+
+1. **"Harry Potter and the Goblet of Fire"** (title match) gets weight A = 1.0
+2. **Books mentioning "Harry" in description** get weight B = 0.4
+
+This ensures that books with "Harry" in the title ranks significantly higher than books that only mention "Harry" in the description, providing more relevant search results for users.
+
+### Using ranking with indexes [#ranking-with-indexes]
+
+When using the `fts` column you created earlier, ranking becomes more efficient. Create a function that uses the indexed column:
+
+```sql
+create or replace function search_books_fts(search_query text)
+returns table(id int, title text, description text, rank real) as $$
+begin
+ return query
+ select
+ books.id,
+ books.title,
+ books.description,
+ ts_rank(books.fts, to_tsquery(search_query)) as rank
+ from books
+ where books.fts @@ to_tsquery(search_query)
+ order by rank desc;
+end;
+$$ language plpgsql;
+```
+
+
+
+
+```js
+const { data, error } = await supabase.rpc('search_books_fts', { search_query: 'little & big' })
+```
+
+
+<$Show if="sdk:dart">
+
+
+```dart
+final result = await client
+ .rpc('search_books_fts', params: { 'search_query': 'little & big' });
+```
+
+
+$Show>
+<$Show if="sdk:python">
+
+
+```python
+data = client.rpc('search_books_fts', { 'search_query': 'little & big' }).execute()
+```
+
+
+$Show>
+
+
+```sql
+select * from search_books_fts('little & big');
+```
+
+
+
+
+### Using web search syntax with ranking [#websearch-ranking]
+
+You can also create a function that combines `websearch_to_tsquery()` with ranking for user-friendly search:
+
+```sql
+create or replace function websearch_books(search_text text)
+returns table(id int, title text, description text, rank real) as $$
+begin
+ return query
+ select
+ books.id,
+ books.title,
+ books.description,
+ ts_rank(books.fts, websearch_to_tsquery('english', search_text)) as rank
+ from books
+ where books.fts @@ websearch_to_tsquery('english', search_text)
+ order by rank desc;
+end;
+$$ language plpgsql;
+```
+
+
+
+
+```js
+// Support natural search syntax
+const { data, error } = await supabase.rpc('websearch_books', {
+ search_text: '"little puppy" or train -vegetables',
+})
+```
+
+
+
+
+```sql
+select * from websearch_books('"little puppy" or train -vegetables');
+```
+
+
+
+
## Resources
- [Postgres: Text Search Functions and Operators](https://www.postgresql.org/docs/12/functions-textsearch.html)
diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx
index f7fd0d6292cfb..7e3c667c7f788 100644
--- a/apps/docs/content/guides/realtime/broadcast.mdx
+++ b/apps/docs/content/guides/realtime/broadcast.mdx
@@ -836,12 +836,6 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv
## Trigger broadcast messages from your database
-
-
-This feature is currently in Public Alpha. If you have any issues [submit a support ticket](https://supabase.help).
-
-
-
### How it works
Broadcast Changes allows you to trigger messages from your database. To achieve it Realtime is directly reading your WAL (Write Append Log) file using a publication against the `realtime.messages` table so whenever a new insert happens a message is sent to connected users.
@@ -948,3 +942,194 @@ const changes = supabase
.on('broadcast', { event: 'DELETE' }, (payload) => console.log(payload))
.subscribe()
```
+
+## Broadcast replay
+
+
+
+This feature is currently in Public Alpha. If you have any issues [submit a support ticket](https://supabase.help).
+
+
+
+### How it works
+
+Broadcast Replay enables **private** channels to access messages that were sent earlier. Only messages published via [Broadcast From the Database](#broadcast-from-the-database) are available for replay.
+
+You can configure replay with the following options:
+
+- **`since`** (Required): The epoch timestamp in milliseconds (e.g., `1697472000000`), specifying the earliest point from which messages should be retrieved.
+- **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25.
+
+
+
+
+
+ This is currently available only in the Supabase JavaScript client version 2.74.0 and later.
+
+
+
+ ```js
+ const config = {
+ private: true,
+ broadcast: {
+ replay: {
+ since: 1697472000000, // Unix timestamp in milliseconds
+ limit: 10
+ }
+ }
+ }
+ const channel = supabase.channel('main:room', { config })
+
+ // Broadcast callback receives meta field
+ channel.on('broadcast', { event: 'position' }, (payload) => {
+ if (payload?.meta?.replayed) {
+ console.log('Replayed message: ', payload)
+ } else {
+ console.log('This is a new message', payload)
+ }
+ // ...
+ })
+ .subscribe()
+ ```
+
+
+
+<$Show if="sdk:dart">
+
+
+
+
+ This is currently available only in the Supabase Dart client version 2.10.0 and later.
+
+
+ ```dart
+ // Configure broadcast with replay
+ final channel = supabase.channel(
+ 'my-channel',
+ RealtimeChannelConfig(
+ self: true,
+ ack: true,
+ private: true,
+ replay: ReplayOption(
+ since: 1697472000000, // Unix timestamp in milliseconds
+ limit: 25,
+ ),
+ ),
+ );
+
+ // Broadcast callback receives meta field
+ channel.onBroadcast(
+ event: 'position',
+ callback: (payload) {
+ final meta = payload['meta'] as Map?;
+ if (meta?['replayed'] == true) {
+ print('Replayed message: ${meta?['id']}');
+ }
+ },
+ ).subscribe();
+ ```
+
+
+ $Show>
+
+<$Show if="sdk:swift">
+
+
+
+
+ This is currently available only in the Supabase Swift client version 2.34.0 and later.
+
+
+ ```swift
+ // Configure broadcast with replay
+ let channel = supabase.realtimeV2.channel("my-channel") {
+ $0.isPrivate = true
+ $0.broadcast.acknowledgeBroadcasts = true
+ $0.broadcast.receiveOwnBroadcasts = true
+ $0.broadcast.replay = ReplayOption(
+ since: 1697472000000, // Unix timestamp in milliseconds
+ limit: 25
+ )
+ }
+
+ var subscriptions = Set()
+
+ // Broadcast callback receives meta field
+ channel.onBroadcast(event: "position") { message in
+ if let meta = message["payload"]?.objectValue?["meta"]?.objectValue,
+ let replayed = meta["replayed"]?.boolValue,
+ replayed {
+ print("Replayed message: \(meta["id"]?.stringValue ?? "")")
+ }
+ }
+ .store(in: &subscriptions)
+
+ await channel.subscribe()
+ ```
+
+
+ $Show>
+
+<$Show if="sdk:kotlin">
+
+
+
+
+ Unsupported in Kotlin for now.
+
+
+
+
+ $Show>
+
+<$Show if="sdk:python">
+
+
+
+
+ This is currently available only in the Supabase Python client version 2.22.0 and later.
+
+
+ ```python
+ # Configure broadcast with replay
+ channel = client.channel('my-channel', {
+ 'config': {
+ "private": True,
+ 'broadcast': {
+ 'self': True,
+ 'ack': True,
+ 'replay': {
+ 'since': 1697472000000,
+ 'limit': 100
+ }
+ }
+ }
+ })
+
+ # Broadcast callback receives meta field
+ def on_broadcast(payload):
+ if payload.get('meta', {}).get('replayed'):
+ print(f"Replayed message: {payload['meta']['id']}")
+
+ await channel.on_broadcast('position', on_broadcast)
+ await channel.subscribe()
+ ```
+
+
+ $Show>
+
+
+#### When to use Broadcast replay
+
+A few common use cases for Broadcast Replay include:
+
+- Displaying the most recent messages from a chat room
+- Loading the last events that happened during a sports event
+- Ensuring users always see the latest events after a page reload or network interruption
+- Highlighting the most recent sections that changed in a web page
diff --git a/apps/docs/content/guides/realtime/settings.mdx b/apps/docs/content/guides/realtime/settings.mdx
index fab3055d3a5ff..d7b0ea319af11 100644
--- a/apps/docs/content/guides/realtime/settings.mdx
+++ b/apps/docs/content/guides/realtime/settings.mdx
@@ -23,8 +23,11 @@ All changes made in this screen will disconnect all your connected clients to en
You can set the following settings using the Realtime Settings screen in your Dashboard:
+- Enable Realtime service: Determines if the Realtime service is enabled or disabled for your project.
- Channel Restrictions: You can toggle this settings to set Realtime to allow public channels or set it to use only private channels with [Realtime Authorization](/docs/guides/realtime/authorization).
- Database connection pool size: Determines the number of connections used for Realtime Authorization RLS checking
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
- Max concurrent clients: Determines the maximum number of clients that can be connected
- Max events per second: Determines the maximum number of events per second that can be sent
+- Max presence events per second: Determines the maximum number of presence events per second that can be sent
+- Max payload size in KB: Determines the maximum number of payload size in KB that can be sent
diff --git a/apps/docs/features/docs/Reference.api.utils.ts b/apps/docs/features/docs/Reference.api.utils.ts
index 841c433df0d1e..df0c9f6d1cea5 100644
--- a/apps/docs/features/docs/Reference.api.utils.ts
+++ b/apps/docs/features/docs/Reference.api.utils.ts
@@ -24,6 +24,7 @@ export interface IApiEndPoint {
}
tags?: Array
security?: Array
+ 'x-oauth-scope'?: string
}
export type ISchema =
diff --git a/apps/docs/features/docs/Reference.sections.tsx b/apps/docs/features/docs/Reference.sections.tsx
index ca9197dab149e..67c07fccd2f3f 100644
--- a/apps/docs/features/docs/Reference.sections.tsx
+++ b/apps/docs/features/docs/Reference.sections.tsx
@@ -341,6 +341,18 @@ async function ApiEndpointSection({ link, section, servicePath }: ApiEndpointSec
{endpointDetails.description}
)}
+ {endpointDetails['x-oauth-scope'] && (
+
+
diff --git a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx
index 91306f2d191d9..90e9c00edb242 100644
--- a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx
+++ b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx
@@ -5,6 +5,8 @@ import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
+import { LOCAL_STORAGE_KEYS } from 'common'
+import { NO_PROJECT_MARKER } from 'components/interfaces/Support/SupportForm.utils'
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProfile } from 'lib/profile'
@@ -26,7 +28,6 @@ import {
Input_Shadcn_,
Separator,
} from 'ui'
-import { LOCAL_STORAGE_KEYS } from 'common'
const setDeletionRequestFlag = () => {
const expiryDate = new Date()
@@ -92,7 +93,7 @@ export const DeleteAccountButton = () => {
severity: 'Low',
allowSupportAccess: false,
verified: true,
- projectRef: 'no-project',
+ projectRef: NO_PROJECT_MARKER,
}
submitSupportTicket(payload)
diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx
index b8d9c8f3db47d..b13784f317be1 100644
--- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx
+++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx
@@ -1,7 +1,9 @@
import type { PostgresPolicy } from '@supabase/postgres-meta'
import { noop } from 'lodash'
+import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
+import { InlineLink } from 'components/ui/InlineLink'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
import { useTablesRolesAccessQuery } from 'data/tables/tables-roles-access-query'
@@ -19,6 +21,7 @@ import {
TableHeader,
TableRow,
} from 'ui'
+import { Admonition } from 'ui-patterns'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { PolicyRow } from './PolicyRow'
import { PolicyTableRowHeader } from './PolicyTableRowHeader'
@@ -50,6 +53,7 @@ export const PolicyTableRow = ({
onSelectEditPolicy = noop,
onSelectDeletePolicy = noop,
}: PolicyTableRowProps) => {
+ const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
// [Joshen] Changes here are so that warnings are more accurate and granular instead of purely relying if RLS is disabled or enabled
@@ -92,7 +96,8 @@ export const PolicyTableRow = ({
- {(isPubliclyReadableWritable || rlsEnabledNoPolicies) && (
+ {!isTableExposedThroughAPI && (
+
+ No data will be selectable via Supabase APIs as this schema is not exposed. You may
+ configure this in your project's{' '}
+ API settings.
+
+ )}
+
+ {(isPubliclyReadableWritable || rlsEnabledNoPolicies) && isTableExposedThroughAPI && (
diff --git a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
index f8391f8339ce8..8f2d69bc386f8 100644
--- a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
+++ b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
@@ -376,8 +376,8 @@ export const SmtpForm = () => {
name="SMTP_MAX_FREQUENCY"
render={({ field }) => (
diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx
index 1dd9201fcf318..5b5deccf3c0dc 100644
--- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx
+++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx
@@ -3,6 +3,7 @@ import { CreditCardIcon } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'common'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
@@ -66,11 +67,14 @@ const CurrentPaymentMethod = () => {
- For larger discounted credit packages, please{' '}
-
- reach out.
-
+ support
+
+ .
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
index b6c3173f9e4b6..2e9a052a10e94 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
@@ -1,7 +1,8 @@
-import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
import { useFlag, useParams } from 'common'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import {
ScaffoldSection,
ScaffoldSectionContent,
@@ -109,15 +110,16 @@ const Subscription = () => {
variant="info"
title={`Unable to update plan from ${planName}`}
actions={[
-
-
-
- Contact support
-
-
-
,
+
+
+ Contact support
+
+ ,
]}
>
Please contact us if you'd like to change your plan.
diff --git a/apps/studio/components/interfaces/Organization/Documents/Documents.tsx b/apps/studio/components/interfaces/Organization/Documents/Documents.tsx
index 1b35df888dc4b..7190771e20761 100644
--- a/apps/studio/components/interfaces/Organization/Documents/Documents.tsx
+++ b/apps/studio/components/interfaces/Organization/Documents/Documents.tsx
@@ -1,6 +1,6 @@
-import Link from 'next/link'
-
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { ScaffoldContainer, ScaffoldDivider, ScaffoldSection } from 'components/layouts/Scaffold'
+import { InlineLinkClassName } from 'components/ui/InlineLink'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { Fragment } from 'react'
import { CustomDocument } from './CustomDocument'
@@ -59,10 +59,8 @@ const Documents = () => {
-
- Submit a support request
- {' '}
- if you require additional documents for financial or tax reasons, such as a W-9 form.
+ Submit a support request if
+ you require additional documents for financial or tax reasons, such as a W-9 form.
- Private mode is {isSettingToPrivate ? 'being ' : ''}
- enabled, but no RLS policies exists on the{' '}
- realtime.messages table. No
- messages will be received by users.
-
+ Private mode is {isSettingToPrivate ? 'being ' : ''}
+ enabled, but no RLS policies exists on the{' '}
+ realtime.messages table. No
+ messages will be received by users.
+
+ Spend cap needs to be disabled to configure this value
+
+
+ {isFreePlan
+ ? 'Upgrade to the Pro plan first to disable spend cap'
+ : 'You may adjust this setting in the organization billing settings'}
+
+
+
+ {isFreePlan ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+
+ )}
+ />
+
+
+ (
+
+ Sets maximum number of presence events per second that can be sent to
+ your Realtime service
+
+ }
+ >
+ Max presence events per second
+
+ }
+ >
+
+
+
+
+
+ {isSuccessOrganization && !isUsageBillingEnabled && !isRealtimeDisabed && (
+
+
+
+
+ Spend cap needs to be disabled to configure this value
+
+
+ {isFreePlan
+ ? 'Upgrade to the Pro plan first to disable spend cap'
+ : 'You may adjust this setting in the organization billing settings'}
+
+
+
+ {isFreePlan ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+
+ )}
+ />
+
+
+ (
+
+ Sets maximum number of payload size in KB that can be sent to your
+ Realtime service
+
+ }
+ >
+ Max payload size in KB
+
+ }
+ >
+
+
+
- {isSuccessOrganization && !isUsageBillingEnabled && (
+ {isSuccessOrganization && !isUsageBillingEnabled && !isRealtimeDisabed && (
diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx
index 0b200b9a810ac..79b6520407461 100644
--- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx
+++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx
@@ -5,6 +5,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { useMemo } from 'react'
+import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import {
getAddons,
@@ -12,6 +13,7 @@ import {
} from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { NoticeBar } from 'components/interfaces/DiskManagement/ui/NoticeBar'
import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import {
ScaffoldContainer,
@@ -446,7 +448,7 @@ export const Addons = () => {
- Contact support
+ Contact support
@@ -487,11 +489,15 @@ export const Addons = () => {
Reach out to us via support if you're interested
-
Contact support
-
+
diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx
index 172f0c9966c23..8e655eeb145ca 100644
--- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx
+++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx
@@ -7,6 +7,7 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
@@ -258,7 +259,7 @@ const PITRSidePanel = () => {
- Contact support
+ Contact support
diff --git a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx
index 58e6fd8d41378..46e1bd6e09abd 100644
--- a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx
@@ -7,6 +7,7 @@ import { toast } from 'sonner'
import { number, object } from 'yup'
import { useParams } from 'common'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -118,11 +119,15 @@ const DiskSizeConfigurationModal = ({
need more than this, contact us via support for help.
-
Contact support
-
+
diff --git a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
index 570e054c8cfef..b7d786ce54717 100644
--- a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
@@ -1,16 +1,17 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { template } from 'lodash'
import { Download, Loader2 } from 'lucide-react'
-import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DocsButton } from 'components/ui/DocsButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { FormPanel } from 'components/ui/Forms/FormPanel'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
+import { InlineLinkClassName } from 'components/ui/InlineLink'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSSLEnforcementQuery } from 'data/ssl-enforcement/ssl-enforcement-query'
import { useSSLEnforcementUpdateMutation } from 'data/ssl-enforcement/ssl-enforcement-update-mutation'
@@ -109,15 +110,8 @@ const SSLConfiguration = () => {
title="SSL enforcement was not updated successfully"
>
Please try updating again, or contact{' '}
-
- support
- {' '}
- if this error persists
+ support if this
+ error persists
)}
diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx
index f3ee01982a244..06bd6f23fcdb2 100644
--- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx
+++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx
@@ -1,13 +1,15 @@
import { AlertCircle } from 'lucide-react'
-import Link from 'next/link'
+import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { FormHeader } from 'components/ui/Forms/FormHeader'
+import { InlineLinkClassName } from 'components/ui/InlineLink'
import Panel from 'components/ui/Panel'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import {
- type CustomDomainsData,
useCustomDomainsQuery,
+ type CustomDomainsData,
} from 'data/custom-domains/custom-domains-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -91,9 +93,12 @@ export const CustomDomainConfig = () => {
Failed to retrieve custom domain configuration. Please try again later or{' '}
-
+
contact support
-
+
.
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
index e6f55fac5b269..3d975a7696167 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
+import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import {
calculateIOPSPrice,
@@ -12,6 +13,7 @@ import {
DISK_PRICING,
DiskType,
} from 'components/interfaces/DiskManagement/ui/DiskManagement.constants'
+import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { DocsButton } from 'components/ui/DocsButton'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { useEnablePhysicalBackupsMutation } from 'data/database/enable-physical-backups-mutation'
@@ -287,13 +289,16 @@ const DeployNewReplicaPanel = ({
-
Contact support
-
+
diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx
index 9b9700a3ee282..ab0edd298e8e5 100644
--- a/apps/studio/components/interfaces/Sidebar.tsx
+++ b/apps/studio/components/interfaces/Sidebar.tsx
@@ -273,7 +273,7 @@ const ProjectLinks = () => {
active={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
route={{
key: 'HOME',
- label: 'Project overview',
+ label: 'Project Overview',
icon: ,
link: `/project/${ref}`,
linkElement: ,
diff --git a/apps/studio/components/interfaces/SignIn/SessionTimeoutModal.tsx b/apps/studio/components/interfaces/SignIn/SessionTimeoutModal.tsx
index c4e05cf8bd51b..ddca9ff4c619d 100644
--- a/apps/studio/components/interfaces/SignIn/SessionTimeoutModal.tsx
+++ b/apps/studio/components/interfaces/SignIn/SessionTimeoutModal.tsx
@@ -1,9 +1,10 @@
import * as Sentry from '@sentry/nextjs'
import { useEffect } from 'react'
-import { InlineLink } from 'components/ui/InlineLink'
-import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink'
import { toast } from 'sonner'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { SupportLink } from '../Support/SupportLink'
interface SessionTimeoutModalProps {
visible: boolean
@@ -60,9 +61,12 @@ export const SessionTimeoutModal = ({
If none of these steps work, please{' '}
-
+
Contact support
-
+
.
diff --git a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx
index 31e22f38b2597..199cb417e00e1 100644
--- a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx
+++ b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx
@@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import z from 'zod'
+import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useAuthError } from 'common'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
@@ -17,6 +18,7 @@ import { useSignOut } from 'lib/auth'
import { getReturnToPath } from 'lib/gotrue'
import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { SupportLink } from '../Support/SupportLink'
const schema = z.object({
code: z.string().min(1, 'MFA Code is required'),
@@ -200,14 +202,15 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => {
-
Reach out to us via support
-
+
diff --git a/apps/studio/components/interfaces/Storage/StorageBucketsError.tsx b/apps/studio/components/interfaces/Storage/StorageBucketsError.tsx
index 6105523fcca46..a13720a5ff204 100644
--- a/apps/studio/components/interfaces/Storage/StorageBucketsError.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageBucketsError.tsx
@@ -1,7 +1,8 @@
+import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
-import Link from 'next/link'
import type { ResponseError } from 'types'
import { Alert, Button } from 'ui'
+import { SupportLink } from '../Support/SupportLink'
export interface StorageBucketsErrorProps {
error: ResponseError
@@ -19,11 +20,15 @@ const StorageBucketsError = ({ error }: StorageBucketsErrorProps) => {
title="Failed to fetch buckets"
actions={[
-
Contact support
-
+
,
]}
>
diff --git a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx
index ae10f3997b09f..d8bc51dd936de 100644
--- a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx
+++ b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx
@@ -22,6 +22,7 @@ import {
SEVERITY_OPTIONS,
} from './Support.constants'
import type { SupportFormValues } from './SupportForm.schema'
+import { NO_PROJECT_MARKER } from './SupportForm.utils'
interface CategoryAndSeverityInfoProps {
form: UseFormReturn
@@ -148,7 +149,7 @@ function SeveritySelector({ form }: SeveritySelectorProps) {
}
const IssueSuggestion = ({ category, projectRef }: { category: string; projectRef?: string }) => {
- const baseUrl = `/project/${projectRef === 'no-project' ? '_' : projectRef}`
+ const baseUrl = `/project/${projectRef === NO_PROJECT_MARKER ? '_' : projectRef}`
const className = 'col-span-2 mb-0'
diff --git a/apps/studio/components/interfaces/Support/DashboardLogsToggle.tsx b/apps/studio/components/interfaces/Support/DashboardLogsToggle.tsx
new file mode 100644
index 0000000000000..02a9792049747
--- /dev/null
+++ b/apps/studio/components/interfaces/Support/DashboardLogsToggle.tsx
@@ -0,0 +1,74 @@
+import { ChevronRight } from 'lucide-react'
+import { useMemo, useState } from 'react'
+import type { UseFormReturn } from 'react-hook-form'
+
+import {
+ Collapsible_Shadcn_,
+ CollapsibleContent_Shadcn_,
+ CollapsibleTrigger_Shadcn_,
+ FormField_Shadcn_,
+ Switch,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import type { SupportFormValues } from './SupportForm.schema'
+import { DASHBOARD_LOG_CATEGORIES, getSanitizedBreadcrumbs } from './dashboard-logs'
+
+interface DashboardLogsToggleProps {
+ form: UseFormReturn
+}
+
+export function DashboardLogsToggle({ form }: DashboardLogsToggleProps) {
+ const sanitizedLogJson = useMemo(() => JSON.stringify(getSanitizedBreadcrumbs(), null, 2), [])
+
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false)
+
+ if (!DASHBOARD_LOG_CATEGORIES.includes(form.getValues('category'))) return
+
+ return (
+ (
+
+ Include dashboard activity log
+
+ }
+ description={
+
+
+ Share sanitized logs of recent dashboard actions to help reproduce the issue.
+
+
+
+
+ Preview log
+
+
+