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
14 changes: 14 additions & 0 deletions apps/docs/content/guides/ai-tools/ai-skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ Add skills for all detected agents at the same time by passing `--all`. See the

You can also install the agent skills together with the Supabase MCP server using the [Supabase Plugin for AI Coding Agents](/docs/guides/ai-tools/plugins) for a combined one-step setup.

## Updating skills

<Admonition type="note">

We update our agent skills frequently, so be sure to check for and install updates regularly to get the latest improvements.

</Admonition>

```bash
npx skills update
```

This updates all skills you have installed. To update specific skills instead, pass their names to the command, e.g. `npx skills update SKILL_NAME`. See the [`skills update` docs](https://github.com/vercel-labs/skills#skills-update) for more options.

## Available skills

<AiSkillsIndex />
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/ai-tools/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The Supabase MCP server provides tools organized into feature groups. All groups

### Debugging

- `get_logs` - Retrieve service logs (API, Postgres, Edge Functions, Auth, Storage, Realtime)
- `query_logs` - Run a read-only SQL query against project logs to filter, aggregate, or join across log fields
- `get_advisors` - Get security and performance advisors

### Development
Expand Down
36 changes: 36 additions & 0 deletions apps/docs/content/guides/auth/sessions/pkce-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,42 @@ Behind the scenes, the code exchange requires a code verifier. Both the code in

The code verifier is created and stored locally when the Auth flow is first initiated. That means the code exchange must be initiated on the same browser and device where the flow was started.

## Overlapping flows

If more than one PKCE flow is started on the same browser before either one completes (for example, `signInWithOAuth()` called in two tabs), the code verifier stored for the earlier flow is overwritten by the later one, and exchanging the first flow's code fails.

<Admonition type="caution" title="Experimental">

Support for overlapping flows is currently experimental and requires explicit opt-in as the API may change without notice.

</Admonition>

To keep each flow's verifier separate, set the `appendPkceFlowIdToRedirects` option when creating the client:

```js
const supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
experimental: { appendPkceFlowIdToRedirects: true },
},
})
```

With this enabled, the client library appends a `sb_flow_id` query parameter to `redirectTo`, so your OAuth callback page can read it back and use it to select the matching verifier. You can also get the flow ID directly from the response of `signInWithOAuth()`:

```js
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
})

const flowId = data.flowId
```

Pass the flow ID to `exchangeCodeForSession()` to make sure the correct verifier is used, whether you read it from `data.flowId` or from the `sb_flow_id` query parameter in the redirect URL:

```js
const { data, error } = await supabase.auth.exchangeCodeForSession(authCode, { flowId })
```

## Resources

- [OAuth 2.0 guide](https://oauth.net/2/pkce/) to PKCE flow
2 changes: 1 addition & 1 deletion apps/docs/content/guides/database/inspect.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 'Debugging and monitoring'
title: 'Database debugging and monitoring'
description: 'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.'
---

Expand Down
6 changes: 3 additions & 3 deletions apps/docs/content/guides/database/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ mkdir -p ./supabase/tests/database
Create a new file with the `.sql` extension which will contain the test.

```bash
touch ./supabase/tests/database/hello_world.test.sql
touch ./supabase/tests/database/hello_world_test.sql
```

### Writing tests

All `sql` files use [pgTAP](/docs/guides/database/extensions/pgtap) as the test runner.

Write a test to check that our `auth.users` table has an ID column. Open `hello_world.test.sql` and add the following code:
Write a test to check that our `auth.users` table has an ID column. Open `hello_world_test.sql` and add the following code:

```sql
begin;
Expand Down Expand Up @@ -63,7 +63,7 @@ This will produce the following output:

```bash
$ supabase test db
supabase/tests/database/hello_world.test.sql .. ok
supabase/tests/database/hello_world_test.sql .. ok
All tests successful.
Files=1, Tests=1, 1 wallclock secs ( 0.01 usr 0.00 sys + 0.04 cusr 0.02 csys = 0.07 CPU)
Result: PASS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ To stop the missing-schema log entries:

PostgREST now exposes an empty schema, which stops the missing-schema errors.

## Re-enable the Data API
## Re-enabling the Data API in the future

Before you re-enable the data API:
You may decide that you wish to re-enable the Data API in the future.

If you do so, to reverse the workaround, you can run the following statements before re-enabling the Data API:

1. Open the SQL Editor.
2. Run the following statements:
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Márton Boros
Matthew Hambright
Matt Hudson
Matt Johnston
Matt Robinson
Matt Rossman
Matt Smiley
Matthias Luft
Expand Down Expand Up @@ -238,6 +239,7 @@ Pedro Rodrigues
Peter Lyn
Peter Soderberg
Pierre Ducroquet
Pierre Frances
Qiao Han
Quintin Willison
Rafael Chacón
Expand Down Expand Up @@ -309,6 +311,7 @@ Victor Farazdagi
Warwick Mitchell
Wen Bo Xie
Wendie Cheung
Yara Lacerda
Yorvi Arias
Yuliya Marinova
Zach Marinov
Expand Down
22 changes: 22 additions & 0 deletions apps/docs/spec/supabase_dart_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4600,6 +4600,15 @@ functions:
- `.lt('column', value)` listens to rows where the column is less than the value
- `.lte('column', value)` listens to rows where the column is less than or equal to the value
- `.inFilter('column', [val1, val2, val3])` listens to rows where the column is one of the values
- `.like('column', pattern)` listens to rows where the column matches the given `LIKE` pattern
- `.ilike('column', pattern)` listens to rows where the column matches the given case-insensitive `LIKE` pattern
- `.matchRegex('column', pattern)` listens to rows where the column matches the given PostgreSQL regular expression, case-sensitive
- `.imatchRegex('column', pattern)` listens to rows where the column matches the given PostgreSQL regular expression, case-insensitive
- `.isFilter('column', value)` listens to rows where the column `IS` the given value (e.g. `null`, `true`, `false`)
- `.isDistinct('column', value)` listens to rows where the column `IS DISTINCT FROM` the given value
- Multiple filters can be chained together on the same `stream()` call, and they are combined with `AND` both when fetching the initial data and when filtering realtime changes.
- For `UPDATE` events, a filter such as `.eq()` is only re-evaluated against the new row. If a row stops matching the filter after an update, it is not removed from the stream and will remain in its last known state until it is deleted or the stream is restarted.
- `DELETE` events only include the primary key columns of the deleted row by default, not the full previous row.
examples:
- id: listen-to-table
name: Listen to a table
Expand Down Expand Up @@ -4632,6 +4641,19 @@ functions:
.order('name')
.limit(10);
```
- id: with-multiple-filters
name: With multiple filters
description: |
Multiple filters can be chained together and are combined with `AND`.
code: |
```dart
supabase.from('countries')
.stream(primaryKey: ['id'])
.eq('continent', 'Asia')
.like('name', '%Republic%')
.order('name')
.limit(10);
```
- id: using-stream-with-stream-builder
name: Using `stream()` with `StreamBuilder`
description: |
Expand Down
74 changes: 74 additions & 0 deletions apps/docs/spec/supabase_swift_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,62 @@ functions:
)
```

- id: generate-link
title: 'generateLink()'
description: |
Generates an email link for a specific action without sending it. This is useful for custom admin functionality where you want to build the email or OTP flow yourself.
notes: |
- `GenerateLinkParams` exposes a static factory for each link type: `.signUp(email:password:redirectTo:)`, `.invite(email:redirectTo:)`, `.magicLink(email:redirectTo:)`, `.recovery(email:redirectTo:)`, `.emailChangeCurrent(email:newEmail:redirectTo:)`, and `.emailChangeNew(email:newEmail:redirectTo:)`.
- `generateLink()` creates the user for `.signUp` and `.invite` if one doesn't already exist.
examples:
- id: generate-a-signup-link
name: Generate a signup link
isSpotlight: true
code: |
```swift
let response = try await supabase.auth.admin.generateLink(
params: .signUp(
email: "email@example.com",
password: "secret"
)
)

let actionLink = response.properties.actionLink
```
- id: generate-a-recovery-link
name: Generate a recovery link
code: |
```swift
let response = try await supabase.auth.admin.generateLink(
params: .recovery(
email: "email@example.com",
redirectTo: URL(string: "https://example.com/reset-password")
)
)
```

- id: auth-js-gotrueadminapi-signout
title: 'signOut()'
description: |
Signs out a specific user by revoking their session(s), using that user's access token (JWT).
notes: |
- Unlike `supabase.auth.signOut()`, this method takes the target user's access token (JWT), not a user ID.
- By default, `signOut()` uses the `.global` scope, which revokes every session for the user. Pass `.local` to revoke only the session tied to the given JWT, or `.others` to keep that session and revoke all the rest.
examples:
- id: sign-out-a-user
name: Sign out a user
isSpotlight: true
code: |
```swift
try await supabase.auth.admin.signOut(jwt: jwt)
```
- id: sign-out-a-user-with-scope
name: Sign out a user with a scope
code: |
```swift
try await supabase.auth.admin.signOut(jwt: jwt, scope: .others)
```

- id: admin-oauth-list-clients
title: 'admin.oauth.listClients()'
description: |
Expand Down Expand Up @@ -4607,6 +4663,7 @@ functions:
- Requires an Authorization header.
- When you pass in a body to your function, we automatically attach the Content-Type header for `String`, and `Data`. If it doesn't match any of these types we assume the payload is `json`, serialize it and attach the `Content-Type` header as `application/json`. You can override this behaviour by passing in a `Content-Type` header of your own.
- When a region is specified, both the `x-region` header and `forceFunctionRegion` query parameter are set to ensure proper function routing.
- By default, function invocations use a 150-second idle timeout. You can override this per-call by passing `timeoutInterval` to `FunctionInvokeOptions`. This only controls the client's request timeout — it cannot extend function execution beyond the platform's [150-second gateway idle timeout](/docs/guides/functions/limits), after which a 504 Gateway Timeout is returned regardless of the value passed.
examples:
- id: invocation-with-decodable
name: Invocation with `Decodable` response
Expand Down Expand Up @@ -4781,6 +4838,23 @@ functions:
)
)
```
- id: invocation-with-timeout-override
name: Invocation with a custom timeout
description: |
Override the default 150-second idle timeout for a single invocation by passing `timeoutInterval`.
isSpotlight: true
code: |
```swift
let response = try await supabase.functions
.invoke(
"hello",
options: FunctionInvokeOptions(
body: ["foo": "bar"],
timeoutInterval: 30
)
)
```

- id: subscribe
title: on().subscribe()
notes: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { act, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it } from 'vitest'

import { useSnippetEditor } from './useSnippetEditor'
import { sqlEditorState } from '@/state/sql-editor/sql-editor-state'
import { createMockProfileContext } from '@/tests/lib/profile-helpers'
import {
renderSqlEditorHook,
resetSqlEditorStores,
seedSnippet,
setupSqlEditorMocks,
} from '@/tests/lib/sql-editor-test-utils'

const PROFILE_CONTEXT = createMockProfileContext()

describe('useSnippetEditor', () => {
beforeEach(() => {
resetSqlEditorStores()
setupSqlEditorMocks()
})

it('writing in a newly opened tab does not overwrite an already-open tab', async () => {
seedSnippet({ id: 'existing-tab', sql: 'select existing;' })

// Mount the editor for the already-open tab, mirroring the keyed
// MonacoEditor mount for that snippet id, then unmount it the way opening
// a new tab would (the `key={id}` wrapper swaps to a brand new instance).
const first = renderSqlEditorHook(useSnippetEditor, {
initialProps: { id: 'existing-tab', snippetName: 'Existing tab' },
profileContext: PROFILE_CONTEXT,
})
await waitFor(() => expect(first.result.current.snippet).toBeDefined())
first.unmount()

// Mount a fresh instance for a brand new tab, as clicking "+" would.
const second = renderSqlEditorHook(useSnippetEditor, {
initialProps: { id: 'new-tab-id', snippetName: 'New query' },
profileContext: PROFILE_CONTEXT,
})

// `handleEditorChange` only creates the new snippet once the project has
// loaded, so retry the keystroke until that happens.
await waitFor(() => {
act(() => {
second.result.current.handleEditorChange('select typed content;')
})
expect(sqlEditorState.snippets['new-tab-id']?.snippet.content?.unchecked_sql).toContain(
'typed content'
)
})

expect(sqlEditorState.snippets['existing-tab'].snippet.content?.unchecked_sql).toContain(
'select existing'
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { act, waitFor } from '@testing-library/react'
import mockRouter from 'next-router-mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { useSnippetIdentity } from './useSnippetIdentity'
import {
renderSqlEditorHook,
resetSqlEditorStores,
seedSnippet,
} from '@/tests/lib/sql-editor-test-utils'

// `common`'s `useParams` is globally stubbed (see tests/vitestSetup.ts) to a
// constant `{ ref: 'default' }` so most tests don't have to think about
// routing. This suite is specifically about behavior across route changes, so
// it swaps in the real implementation (still backed by the `next-router-mock`
// router the rest of the app is wired to in tests).
vi.mock('common', async (importOriginal) => {
const actual = (await importOriginal()) as object
return { ...actual }
})

describe('useSnippetIdentity across route changes', () => {
beforeEach(() => {
resetSqlEditorStores()
mockRouter.setCurrentUrl('/project/default/sql/existing-tab')
})

it('generates a fresh id for each new tab, never reusing the previous one', async () => {
seedSnippet({ id: 'existing-tab', sql: 'select existing;' })

const { result } = renderSqlEditorHook(useSnippetIdentity)

await waitFor(() => expect(result.current.id).toEqual('existing-tab'))

// Click "+" to open a new tab.
await act(async () => {
await mockRouter.push('/project/default/sql/new?skip=true')
})
const firstNewTabId = result.current.id
expect(firstNewTabId).not.toEqual('existing-tab')

// Typing in the new tab creates its snippet and shallow-navigates to it
// (mirrors the `router.push` inside `useSnippetEditor.handleEditorChange`).
seedSnippet({ id: firstNewTabId, sql: 'select typed content;' })
await act(async () => {
await mockRouter.push(`/project/default/sql/${firstNewTabId}`)
})
await waitFor(() => expect(result.current.id).toEqual(firstNewTabId))

// Click "+" again to open a second new tab.
await act(async () => {
await mockRouter.push('/project/default/sql/new?skip=true')
})
const secondNewTabId = result.current.id

expect(secondNewTabId).not.toEqual(firstNewTabId)
expect(secondNewTabId).not.toEqual('existing-tab')
})
})
Loading
Loading