Merge branch 'master' of https://github.com/supabase/supabase into feat/event-collections

This commit is contained in:
Jordi Enric committed 2024-04-18 15:59:37 +02:00
commit 014e8a8150
329 files changed
+16087 -3517

No files matched your search

+2 -2
View File
@@ -14,7 +14,7 @@ To ensure a positive and inclusive environment, please read our [code of conduc
If you find a bug, please create an Issue and we’ll triage it.
- Please search [existing Issues](https://github.com/supabase/supabase) before creating a new one.
- Please search [existing Issues](https://github.com/supabase/supabase/issues) before creating a new one.
- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here.
## Pull Requests
@@ -23,7 +23,7 @@ We actively welcome your Pull Requests! A couple of things to keep in mind befor
- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s).
- We will always try to accept the first viable PR that resolves the Issue.
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/pulls?q=is%3Aopen+is%3Apr+label%3A%22good+first+issue%22).
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/labels/good%20first%20issue).
- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed.
- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed.
+9 -9
View File
@@ -111,6 +111,15 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
</tr>
<tr>
<td>Swift</td>
<td><a href="https://github.com/supabase/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/PostgREST" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Auth" target="_blank" rel="noopener noreferrer">auth-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Realtime" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Storage" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Functions" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
</tr>
<!-- /notranslate -->
<th colspan="7">💚 Community 💚</th>
<!-- notranslate -->
@@ -177,15 +186,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Swift</td>
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
</tr>
<tr>
<td>Godot Engine (GDScript)</td>
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
+2 -2
View File
@@ -19,10 +19,10 @@
"common": "*",
"config": "*",
"dayjs": "^1.11.10",
"eslint-config-next": "^14.1.4",
"eslint-config-next": "^14.2.0",
"lodash": "^4.17.21",
"lucide-react": "^0.338.0",
"next": "^14.1.4",
"next": "^14.2.0",
"openai": "^4.20.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+2 -1
View File
@@ -16,6 +16,7 @@ import { Button, cn } from 'ui'
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'
import { getSanitizedTabParams } from './Feedback.utils'
const FeedbackButton = forwardRef<
HTMLButtonElement,
@@ -89,7 +90,7 @@ function Feedback() {
vote: response,
page: pathname,
metadata: {
query: Object.fromEntries(new URLSearchParams(window.location.search).entries()),
query: getSanitizedTabParams(),
},
})
if (error) console.error(error)
@@ -0,0 +1,16 @@
import { pick } from 'lodash'
/**
* Gets the tab selection state from the URL search params.
*
* Sanitizes by including only those search params that are explicitly marked
* as query groups.
*/
const getSanitizedTabParams = () => {
const searchParams = new URLSearchParams(window.location.search)
const queryGroups = searchParams.getAll('queryGroups')
return pick(Object.fromEntries(searchParams.entries()), queryGroups)
}
export { getSanitizedTabParams }
@@ -112,6 +112,12 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
href: '/reference/dart/introduction',
level: 'reference_dart',
},
{
label: 'Swift',
icon: 'reference-swift',
href: '/reference/swift/introduction',
level: 'reference_swift',
},
{
label: 'Python',
icon: 'reference-python',
@@ -126,13 +132,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
level: 'reference_csharp',
community: true,
},
{
label: 'Swift',
icon: 'reference-swift',
href: '/reference/swift/introduction',
level: 'reference_swift',
community: true,
},
{
label: 'Kotlin',
icon: 'reference-kotlin',
@@ -1140,6 +1139,10 @@ export const functions: NavMenuConstant = {
name: 'Handling Routing in Functions',
url: '/guides/functions/routing',
},
{
name: 'Running AI Models',
url: '/guides/functions/ai-models',
},
{
name: 'Regional invocations',
url: '/guides/functions/regional-invocation',
@@ -1188,6 +1191,10 @@ export const functions: NavMenuConstant = {
name: 'Generating OG images ',
url: '/guides/functions/examples/og-image',
},
{
name: 'Semantic AI Search',
url: '/guides/functions/examples/semantic-search',
},
{
name: 'CAPTCHA support with Cloudflare Turnstile',
url: '/guides/functions/examples/cloudflare-turnstile',
@@ -1,15 +1,11 @@
---
id: 'ai-generating-embeddings'
title: 'Generate Embeddings'
subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.'
subtitle: 'Generate text embeddings using Edge Functions.'
breadcrumb: 'AI Quickstarts'
---
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using Transformers.js. Inference is performed directly in Edge Functions using open source Hugging Face models, so no external API is required.
## What is Transformers.js?
[Transformers.js](https://huggingface.co/docs/transformers.js/index) is a library that allows you to perform inference using the JavaScript runtime. You can run Hugging Face embedding models through Transformers.js directly in Supabase [Edge Functions](/docs/guides/functions).
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using its built-in AI inference API, so no external API is required.
## Build the Edge Function
@@ -60,89 +56,45 @@ Let's build an Edge Function that will accept an input string and generate an em
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Import & configure Transformer.js" fullWidth>
<StepHikeCompact.Details title="Setup Inference Session" fullWidth>
Let's modify the Edge Function to import the Transformers.js client and configure it for the Deno runtime.
Let's create a new inference session to be used in the lifetime of this function. Multiple requests can use the same inference session.
```ts ./supabase/functions/embed/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { env, pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.5.0'
// Configuration for Deno runtime
env.useBrowserCache = false;
env.allowLocalModels = false;
```
Currently, only the `gte-small` (https://huggingface.co/Supabase/gte-small) text embedding model is supported in Supabase's Edge Runtime.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts ./supabase/functions/embed/index.ts
const session = new Supabase.ai.Session('gte-small');
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Construct embedding pipeline">
Next let's construct the `pipe()` function we'll use to generate the embeddings. We use the Transformer.js `pipeline()` function to create this.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts ./supabase/functions/embed/index.ts
const pipe = await pipeline(
'feature-extraction',
'Supabase/gte-small',
);
```
</StepHikeCompact.Code>
<StepHikeCompact.Details fullWidth>
Note the two arguments we pass to `pipeline()`:
1. The first argument specifies the type of [inference task](https://huggingface.co/docs/transformers.js/index#tasks) to perform. `feature-extraction` is used for embedding generation.
2. The second argument specifies which Hugging Face model to use for the embedding generation. Supabase officially supports models available on the [Supabase org](https://huggingface.co/Supabase).
<Admonition>
We intentially construct the pipeline before the `serve()` function. This allows us to reuse the pre-loaded model in future warm-start requests, significantly speeding up future queries. Note that you'll only notice this speedup after deploying your function to Supabase - requests in local development will always be a cold-start.
</Admonition>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Implement request handler">
Modify our `serve()` request handler to accept an `input` string from the POST request JSON body.
Modify our request handler to accept an `input` string from the POST request JSON body.
Finally let's generate the embedding by:
1. Calling `pipe()` to perform the embedding generation
2. Extracting the embedding from the output as a number array
3. Returning it as a JSON response
Then generate the embedding by calling `session.run(input)`.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts ./supabase/functions/embed/index.ts
serve(async (req) => {
Deno.serve(async (req) => {
// Extract input string from JSON body
const { input } = await req.json();
// Generate the embedding from the user input
const output = await pipe(input, {
pooling: 'mean',
const embedding = await session.run(input, {
mean_pool: true,
normalize: true,
});
// Extract the embedding output
const embedding = Array.from(output.data);
// Return the embedding
return new Response(
JSON.stringify({ embedding }),
@@ -155,16 +107,16 @@ Let's build an Edge Function that will accept an input string and generate an em
<StepHikeCompact.Details fullWidth>
Note the two options we pass to `pipe()`:
Note the two options we pass to `session.run()`:
- `pooling`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings and is the method supported by Transformers.js.
- `normalize`: The second option tells Transformers.js to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
- `mean_pool`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings.
- `normalize`: The second option tells to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Test it!">
@@ -26,7 +26,7 @@ Embeddings are generated using a language model, and embeddings are compared to
## Embedding models
There are many embedding models available today. Some can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results.
@@ -13,6 +13,7 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
| Dart/Flutter | [supabase-flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
| Swift | [supabase-swift](https://github.com/supabase/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
## Community libraries
@@ -23,5 +24,4 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](https://supabase.com/docs/reference/kotlin/introduction) |
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
@@ -4,13 +4,7 @@ title: 'Anonymous Sign-Ins'
subtitle: 'Create and use anonymous users to authenticate with Supabase'
---
Enable Anonymous Sign-In to build apps which provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, they can attach an authentication method to their account.
It's most useful for:
- E-commerce applications, such as shopping carts before check-out
- Full-feature demos without collecting personal information
- Temporary or throw-away accounts
[Enable Anonymous Sign-Ins](/dashboard/project/_/settings/auth) to build apps which provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, the user can link an authentication method to their account.
<Admonition type="note" label="Anonymous user vs the anon key">
@@ -22,7 +16,11 @@ This is different from the `anon` API key which does not create a user and can b
</Admonition>
To get started, [enable anonymous sign-ins](/dashboard/project/_/settings/auth) for your Supabase Project.
Anonymous sign-ins can be used to build:
- E-commerce applications, such as shopping carts before check-out
- Full-feature demos without collecting personal information
- Temporary or throw-away accounts
<Admonition type="caution">
@@ -87,7 +85,7 @@ supabase.auth.signInAnonymously()
## Convert an anonymous user to a permanent user
Converting an anonymous user to a permanent user requires linking an identity to the user.
Converting an anonymous user to a permanent user requires [linking an identity](/docs/guides/auth/auth-identity-linking#manual-linking-beta) to the user. This requires you to [enable manual linking](/dashboard/project/_/settings/auth) in your Supabase project.
### Link an Email / Phone identity
@@ -100,10 +98,16 @@ Converting an anonymous user to a permanent user requires linking an identity to
>
<TabPanel id="js" label="JavaScript">
You can use the [`updateUser()`](/docs/reference/javascript/auth-updateuser) method to link an email or phone identity to the anonymous user.
You can use the [`updateUser()`](/docs/reference/javascript/auth-updateuser) method to link an email or phone identity to the anonymous user. To add a password for the anonymous user, the user's email or phone number needs to be verified first.
```js
const { data, error } = await supabase.auth.updateUser({ email: 'example@email.com' })
// verify the user's email by clicking on the email change link
// or entering the 6-digit OTP sent to the email address
// once the user has been verified, update the password
const { data, error } = await supabase.auth.updateUser({ password: 'password' })
```
</TabPanel>
@@ -129,10 +133,10 @@ try await supabase.auth.updateUser(
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
You can use the [`modifyUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user.
You can use the [`updateUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user.
```kotlin
supabase.auth.modifyUser {
supabase.auth.updateUser {
email = "example@email.com"
}
```
@@ -233,7 +237,7 @@ where is_anonymous is true and created_at < now() - interval '30 days';
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
+1 -1
View File
@@ -207,7 +207,7 @@ suspend fun logout() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
@@ -76,6 +76,22 @@ const { data, error } = await supabase.auth.signInWithOAuth({
})
```
## Email templates when using `redirectTo`
When using a `redirectTo` option, you may need to replace the `{{ .SiteURL }}` with `{{ .RedirectTo }}` in your email templates. See the [Email Templates guide](/docs/guides/auth/auth-email-templates) for more information.
For example, change the following:
```html
<!-- Old -->
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup">Confirm your mail</a>
<!-- New -->
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup"
>Confirm your mail</a
>
```
## Mobile deep linking URIs
For mobile applications you can use deep linking URIs. For example, for your `SITE_URL` you can specify something like `com.supabase://login-callback/` and for additional redirect URLs something like `com.supabase.staging://login-callback/` if needed.
@@ -157,7 +157,7 @@ If successful the user will now be logged in and you should receive a valid sess
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Phone SMS & WhatsApp OTP](/docs/guides/auth/phone-login)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
@@ -158,6 +158,6 @@ suspend fun logout() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
+2 -2
View File
@@ -149,10 +149,10 @@ try await supabase.auth.updateUser(user: UserAttributes(password: newPassword))
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
To update the password, call the [`.modifyUser`](/docs/reference/kotlin/auth-updateuser) method with the new password.
To update the password, call the [`.updateUser`](/docs/reference/kotlin/auth-updateuser) method with the new password.
```kotlin
supabase.gotrue.modifyUser {
supabase.auth.updateUser {
password = "new_password"
}
```
@@ -237,7 +237,7 @@ In the server, you need to handle the redirect to the OAuth provider's authentic
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: '/auth/callback',
redirectTo: 'http://example.com/auth/callback',
},
})
@@ -112,6 +112,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Bitbucket Account](https://bitbucket.org)
@@ -113,7 +113,7 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Discord Account](https://discord.com)
- [Discord Developer Portal](https://discord.com/developers)
@@ -131,6 +131,6 @@ You can read more about App Review [here](https://developers.facebook.com/docs/a
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Facebook Developers Dashboard](https://developers.facebook.com/)
@@ -112,6 +112,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Figma Developers page](https://www.figma.com/developers)
@@ -123,6 +123,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [GitHub Developer Settings](https://github.com/settings/developers)
@@ -109,6 +109,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [GitLab Account](https://gitlab.com)
@@ -135,6 +135,6 @@ Do reach out to support if you have any concerns around this change.
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps)
@@ -112,7 +112,7 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Notion Account](https://notion.so)
- [Notion Developer Portal](https://www.notion.so/my-integrations)
@@ -130,6 +130,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Slack Developer Dashboard](https://api.slack.com/apps)
@@ -124,6 +124,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
@@ -127,7 +127,7 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Twitch Account](https://twitch.tv)
- [Twitch Developer Console](https://dev.twitch.tv/console)
@@ -118,6 +118,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard)
@@ -125,6 +125,6 @@ suspend fun signOut() {
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Zoom App Marketplace](https://marketplace.zoom.us/)
+1 -1
View File
@@ -210,5 +210,5 @@ returns:
## Resources
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [PostgreSQL Arrays](https://www.postgresql.org/docs/15/arrays.html)
@@ -4,7 +4,20 @@ title: 'index_advisor: query optimization'
description: 'Automatically optimize SQL queries'
---
[Index advisor](https://database.dev/olirice/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
[Index advisor](https://github.com/supabase/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
Features:
- Supports generic parameters e.g. `$1`, `$2`
- Supports materialized views
- Identifies tables/columns obfuscated by views
- Skips duplicate indexes
index_advisor is accessible directly through Supabase Studio by navigating to the [Query Performance Report](/dashboard/project/*/database/query-performance) and selecting a query and then the "indexes" tab.
![Supabase Studio index_advisor integration.](/docs/img/index_advisor_studio.png)
Alternatively, you can use index_advisor directly via SQL.
For example:
@@ -20,25 +33,12 @@ from
(1 row)
```
Features:
- Supports generic parameters e.g. `$1`, `$2`
- Supports materialized views
- Identifies tables/columns obfuscated by views
- Skips duplicate indexes
## Installation
index_advisor is a trusted language extension, which means it is directly installable by users from the [database.dev](https://database.dev/) SQL package repository.
To get started, enable the dbdev client by executing the [setup SQL script](https://database.dev/installer).
Then, install index_advisor by running
To get started, enable index_advisor by running
```sql
select dbdev.install('olirice-index_advisor');
create extension if not exists hypopg;
create extension "olirice-index_advisor";
create extension index_advisor;
```
## API
@@ -152,5 +152,4 @@ from
## Resources
- dbdev [`index_advisor`](https://database.dev/olirice/index_advisor) docs
- dbdev [Github Repository](https://github.com/supabase/dbdev)
- [`index_advisor`](https://github.com/supabase/index_advisor) repo
@@ -104,3 +104,27 @@ reindex table concurrently persons;
```
Take note that `reindex` can be used inside a transaction, but `reindex [index/table] concurrently` cannot.
## Index Advisor
Indexes can improve query performance of your tables as they grow. The Supabase Dashboard offers an Index Advisor, which suggests potential indexes to add to your tables.
For more information on the Index Advisor and its suggestions, see the [`index_advisor` extension](/docs/guides/database/extensions/index_advisor).
To use the Dashboard Index Advisor:
1. Go to the [Query Performance](/dashboard/project/_/database/query-performance) page.
1. Click on a query to bring up the Details side panel.
1. Select the Indexes tab.
1. Enable Index Advisor if prompted.
### Understanding Index Advisor results
The Indexes tab shows the existing indexes used in the selected query. Note that indexes suggested in the "New Index Recommendations" section may not be used when you create them. Postgres' query planner may intentionally ignore an available index if it determines that the query will be faster without. For example, on a small table, a sequential scan might be faster than an index scan. In that case, the planner will switch to using the index as the table size grows, helping to future proof the query.
If additional indexes might improve your query, the Index Advisor shows the suggested indexes with the estimated improvement in startup and total costs:
- Startup cost is the cost to fetch the first row
- Total cost is the cost to fetch all the rows
Costs are in arbitrary units, where a single sequential page read costs 1.0 units.
@@ -362,7 +362,7 @@ Always use the Role of inside your policies, specified by the `TO` operator. For
```sql
create policy "rls_test_select" on rls_test
using ( (select auth.uid()) = user_id );
using ( auth.uid() = user_id );
```
Use:
@@ -77,7 +77,7 @@ const { count, error } = await supabase
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase - Get started for free](https://supabase.com)
- [Postgrest Operators](https://postgrest.org/en/stable/api.html#operators)
- [Supabase API: JavaScript select](/docs/reference/javascript/select)
- [Supabase API: JavaScript modifiers](/docs/reference/javascript/using-modifiers)
@@ -0,0 +1,171 @@
---
id: 'function-ai-models'
title: 'Running AI Models'
description: 'How to run AI models in Edge Functions.'
subtitle: 'How to run AI models in Edge Functions.'
---
[Supabase Edge Runtime](https://github.com/supabase/edge-runtime) has a built-in API for running AI models. You can use this API to generate embeddings, build conversational workflows, and do other AI related tasks in your Edge Functions.
## Setup
There are no external dependencies or packages to install to enable the API.
You can create a new inference session by doing:
```ts
const model = new Supabase.ai.Session('model-name')
```
<Admonition type="tip">
To get type hints and checks for the API we recommend adding this triple-slash directive to the start of the Edge Function code: `/// <reference types="https://esm.sh/@supabase/functions-js/src/edge-runtime.d.ts" />`
</Admonition>
## Running a model inference
Once the session is instantiated, you can call it with inputs to perform inferences. Depending on the model you run, you may need to provide different options (discussed below).
```ts
const output = await model.run(input, options)
```
## How to generate text embeddings
Now let's see how to write an Edge Function using the `Supabase.ai` API to generate text embeddings. Currently, `Supabase.ai` API only supports the [gte-small](https://huggingface.co/Supabase/gte-small) model.
<Admonition type="tip">
`gte-small` model exclusively caters to English texts, and any lengthy texts will be truncated to a maximum of 512 tokens. While you can provide inputs longer than 512 tokens, truncation may affect the accuracy.
</Admonition>
```ts
const model = new Supabase.ai.Session('gte-small')
Deno.serve(async (req: Request) => {
const params = new URL(req.url).searchParams
const input = params.get('input')
const output = await model.run(input, { mean_pool: true, normalize: true })
return new Response(JSON.stringify(output), {
headers: {
'Content-Type': 'application/json',
Connection: 'keep-alive',
},
})
})
```
## Using Large Language Models
Inference via larger models is supported via [Ollama](https://ollama.com/). In the first iteration, the `llama2` and `mistral` models are supported and you would need to manage the Ollama server yourself.
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/edge-functions-inference-2.mp4"
type="video/mp4"
/>
</video>
### Running locally
1. Install Ollama and pull the Mistral model
```
ollama pull mistral
```
1. Run the Ollama server locally
```
ollama serve
```
1. Set a function secret called AI_INFERENCE_API_HOST to point to the Ollama server
```
echo "AI_INFERENCE_API_HOST=http://host.docker.internal:11434/api/generate" >> supabase/functions/.env
```
1. Create a new function with the following code
```
supabase functions new ollama-test
```
```ts
/// <reference types="https://esm.sh/@supabase/functions-js/src/edge-runtime.d.ts" />
const session = new Supabase.ai.Session('mistral')
Deno.serve(async (req: Request) => {
const params = new URL(req.url).searchParams
const prompt = params.get('prompt') ?? ''
// Get the output as a stream
const output = await session.run(prompt, { stream: true })
const headers = new Headers({
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
})
// Create a stream
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
try {
for await (const chunk of output) {
controller.enqueue(encoder.encode(chunk.response ?? ''))
}
} catch (err) {
console.error('Stream error:', err)
} finally {
controller.close()
}
},
})
// Return the stream to the user
return new Response(stream, {
headers,
})
})
```
1. Execute the function
```
curl --get "http://localhost:54321/functions/v1/ollama-test" \
--data-urlencode "prompt=write a short rap song about Supabase, the Postgres Developer platform, as sung by Nicki Minaj" \
-H "Authorization: $ANON_KEY"
```
### Deploying to production
Once the function is working locally, it's time to deploy to production.
1. Deploy a Ollama server and set a function secret called `AI_INFERENCE_API_HOST` to point to the deployed Ollama server
```
supabase secrets set AI_INFERENCE_API_HOST=https://path-to-your-ollama-server/
```
2. Deploy the Supabase function
```
supabase functions deploy ollama-test
```
3. Execute the function
```
curl --get "https://project-ref.supabase.co/functions/v1/ollama-test" \
--data-urlencode "prompt=write a short rap song about Supabase, the Postgres Developer platform, as sung by Nicki Minaj" \
-H "Authorization: $ANON_KEY"
```
As demonstrated in the video above, running Ollama locally is typically slower than running it in on a server with dedicated GPUs. We are collaborating with the Ollama team to improve local performance.
In the future, a hosted Ollama API, will be provided as part of the Supabase platform. Supabase will scale and manage the API and GPUs for you. To sign up for early access, fill up [this form](https://forms.supabase.com/supabase.ai-llm-early-access).
@@ -0,0 +1,129 @@
---
id: 'function-ai-models'
title: 'Semantic Search'
description: 'Semantic Search with pgvector and Supabase Edge Functions'
subtitle: 'Semantic Search with pgvector and Supabase Edge Functions'
tocVideo: 'w4Rr_1whU-U'
---
[Semantic search](/docs/guides/ai/semantic-search) interprets the meaning behind user queries rather than exact [keywords](/docs/guides/ai/keyword-search). It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships.
Since Supabase Edge Runtime [v1.36.0](https://github.com/supabase/edge-runtime/releases/tag/v1.36.0) you can run the [`gte-small` model](https://huggingface.co/Supabase/gte-small) natively within Supabase Edge Functions without any external dependencies! This allows you to easily generate text embeddings without calling any external APIs!
In this tutorial you're implementing three parts:
1. A [`generate-embedding`](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) database webhook edge function which generates embeddings when a content row is added (or updated) in the [`public.embeddings`](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql) table.
2. A [`query_embeddings` Postgres function](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an egde function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
3. A [`search` edge function](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/functions/search/index.ts) which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result.
You can find the complete example code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions)
### Create the database table and webhook
Given the [following table definition](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql):
```sql
create extension if not exists vector with schema extensions;
create table embeddings (
id bigint primary key generated always as identity,
content text not null,
embedding vector (384)
);
alter table embeddings enable row level security;
create index on embeddings using hnsw (embedding vector_ip_ops);
```
You can deploy the [following edge function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) as a [database webhook](/docs/guides/database/webhooks) to generate the embeddings for any text content inserted into the table:
```tsx
const model = new Supabase.ai.Session('gte-small')
Deno.serve(async (req) => {
const payload: WebhookPayload = await req.json()
const { content, id } = payload.record
// Generate embedding.
const embedding = await model.run(content, {
mean_pool: true,
normalize: true,
})
// Store in database.
const { error } = await supabase
.from('embeddings')
.update({ embedding: JSON.stringify(embedding) })
.eq('id', id)
if (error) console.warn(error.message)
return new Response('ok')
})
```
## Create a Postgres Function and RPC
With the embeddings now stored in your Postgres database table, you can query them from Supabase Edge Functions by utilizing [Remote Procedure Calls (RPC)](/docs/guides/database/functions?language=js).
Given the [following Postgres Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql):
```sql
-- Matches document sections using vector similarity search on embeddings
--
-- Returns a setof embeddings so that we can use PostgREST resource embeddings (joins with other tables)
-- Additional filtering like limits can be chained to this function call
create or replace function query_embeddings(embedding vector(384), match_threshold float)
returns setof embeddings
language plpgsql
as $$
begin
return query
select *
from embeddings
-- The inner product is negative, so we negate match_threshold
where embeddings.embedding <#> embedding < -match_threshold
-- Our embeddings are normalized to length 1, so cosine similarity
-- and inner product will produce the same query results.
-- Using inner product which can be computed faster.
--
-- For the different distance functions, see https://github.com/pgvector/pgvector
order by embeddings.embedding <#> embedding;
end;
$$;
```
## Query vectors in Supabase Edge Functions
You can use `supabase-js` to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your [Supabase Edge Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/search/index.ts):
```tsx
const model = new Supabase.ai.Session('gte-small')
Deno.serve(async (req) => {
const { search } = await req.json()
if (!search) return new Response('Please provide a search param!')
// Generate embedding for search term.
const embedding = await model.run(search, {
mean_pool: true,
normalize: true,
})
// Query embeddings.
const { data: result, error } = await supabase
.rpc('query_embeddings', {
embedding,
match_threshold: 0.8,
})
.select('content')
.limit(3)
if (error) {
return Response.json(error)
}
return Response.json({ search, result })
})
```
That's it, you now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions!
@@ -66,6 +66,20 @@ These are the currently supported region values you can provide for `x-region` h
- `us-west-1`
- `us-west-2`
## Using the client library
You can also specify the region when invoking a Function using the Supabase client library:
```js
const { createClient, FunctionRegion } = require('@supabase/supabase-js')
const { data: ret, error } = await supabase.functions.invoke('my-function-name', {
headers: { 'Content-Type': 'application/json' },
method: 'GET',
body: {},
region: FunctionRegion.UsEast1,
})
```
## Handling regional outages
If you explicitly specify the region via `x-region` header, requests **will NOT** be automatically re-routed to another region and you should consider temporarily changing regions during the outage.
@@ -58,7 +58,7 @@ You can deploy the function to Supabase via:
supabase functions deploy hello-world
```
Once the function is deployed, you can try to call the two ednpoints using cURL (or Postman).
Once the function is deployed, you can try to call the two endpoints using cURL (or Postman).
```bash
# https://supabase.com/docs/guides/functions/deploy#invoking-remote-functions
@@ -145,7 +145,7 @@ Less, but better.
To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product.
We use existing standards to increase portability (like `pg_dump` and CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
This forces us compete on experience. We aim to be the best Postgres hosting service.
This forces us to compete on user experience. We aim to be the best Postgres hosting service.
### Play the long game
@@ -12,51 +12,89 @@ This is a non-exhaustive list of features that Supabase provides for every proje
Every project is a full Postgres database. [Docs](/docs/guides/database).
### Database extensions
### Vector database
Every database comes with a full set of Postgres extensions. [Docs](/docs/guides/database/extensions).
Store vector embeddings right next to the rest of your data. [Docs](/docs/guides/ai).
### Database functions
### Auto-generated Rest API via PostgREST
Create custom database functions which you can call from the browser. [Docs](/docs/guides/database/functions).
RESTful APIs are auto-generated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
### Database triggers
### Auto-generated GraphQL API via pg_graphql
Attach triggers to your tables to handle database changes. [Docs](/docs/guides/auth/managing-user-data#using-triggers).
Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guides/graphql/api).
### Database webhooks
Send database changes to any external service using Webhooks. [Link](https://supabase.com/blog/supabase-functions-updates#database-webhooks-alpha).
### Database backups
Projects are backed up daily with the option to upgrade to Point in Time recovery.
### Search
Build search functionality using Postgres Full Text Search. [Docs](/docs/guides/database/full-text-search).
Send database changes to any external service using Webhooks. [Docs](/docs/guides/database/webhooks).
### Secrets and encryption
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Link](https://supabase.com/blog/supabase-vault).
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Docs](/docs/guides/database/vault).
### Database migrations
## Platform
Develop locally and push your changes to your production database using migrations. [Docs](/docs/guides/getting-started/local-development#database-migrations)
### Database backups
Projects are backed up daily with the option to upgrade to Point in Time recovery. [Docs](/docs/guides/platform/backups).
### Custom domains
White-label the Supabase APIs to create a branded experience for your users. [Docs](/docs/guides/platform/custom-domains).
### Network restrictions
Restrict IP ranges that can connect to your database. [Docs](/docs/guides/platform/network-restrictions).
### SSL enforcement
Enforce Postgres clients to connect via SSL. [Docs](/docs/guides/platform/ssl-enforcement).
### Branching
Use Supabase Branches to test and preview changes. [Docs](/docs/guides/platform/branching).
### Terraform provider
Manage Supabase infrastructure via Terraform, an Infrastructure as Code tool. [Docs](/docs/guides/platform/terraform).
### Read replicas
Deploy read-only databases across multiple regions, for lower latency and better resource management. [Docs](/docs/guides/platform/read-replicas).
### Fly Postgres
Launch the Supabase stack on Fly.io edge network. [Docs](/docs/guides/platform/fly-postgres).
## Studio
### Studio Single Sign On
Login to the Supabase dashboard via SSO. [Docs](/docs/guides/platform/sso).
<br />
## Realtime
### Postgres changes
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-changes).
### Broadcast
Send messages between connected users through websockets. [Docs](/docs/guides/realtime/broadcast).
### Presence
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime/presence).
## Auth
### Email & password logins
### Email login
Build email logins for your application or website. [Docs](/docs/guides/auth/auth-email).
### Magic links
Build passwordless logins for your application or website.[Docs](/docs/guides/auth/auth-magic-link).
### Social logins
### Social login
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/social-login).
@@ -64,46 +102,21 @@ Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs
Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/phone-login).
### Row Level Security
### Passwordless login
Build passwordless logins via magic links for your application or website.[Docs](/docs/guides/auth/auth-magic-link).
### Authorization via Row Level Security
Control the data each user can access with Postgres Policies. [Docs](/docs/guides/auth/row-level-security).
### Server-side Auth helpers
### Captcha protection
Helpers for implementing user authentication in popular frameworks like [Next.js](/docs/guides/auth/auth-helpers/nextjs) and [SvelteKit](/docs/guides/auth/auth-helpers/sveltekit)
Add Captcha to your sign-in, sign-up, and password reset forms. [Docs](/docs/guides/auth/auth-captcha).
### Auth UI kit
### Server-side Auth
Build login and registration pages with custom themes. [Docs](/docs/guides/auth/auth-helpers/auth-ui).
<br />
## APIs & client libraries
### Auto-generated REST API
RESTful APIs are auto-generated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
### Auto-generated GraphQL API
Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guides/graphql/api).
### Realtime database changes
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-changes).
### User broadcasting
Send messages between connected users through websockets. [Docs](/docs/guides/realtime/broadcast).
### User presence
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime/presence).
### Client libraries
Official client libraries for [JavaScript](/docs/reference/javascript/) and [Dart](/docs/reference/dart).
Unofficial libraries [supported by the community](https://github.com/supabase-community#client-libraries).
Helpers for implementing user authentication in popular server-side languages and frameworks like Next.js, SvelteKit and Remix. [Docs](/docs/guides/auth/server-side/overview).
<br />
@@ -113,19 +126,21 @@ Unofficial libraries [supported by the community](https://github.com/supabase-co
Supabase Storage makes it simple to store and serve files. [Docs](/docs/guides/storage).
### Resumable uploads
Upload large files easily using resumable uploads. [Docs](/docs/guides/storage/uploads/resumable-uploads).
### Storage CDN
### Content Delivery Network
Cache large files using the Supabase CDN. [Docs](/docs/guides/storage/cdn/fundamentals).
### Smart Content Delivery Network
Automatically revalidate assets at the edge via the Smart CDN. [Docs](/docs/guides/storage/cdn/smart-cdn).
### Image transformations
Transform images on the fly. [Docs](/docs/guides/storage/serving/image-transformations).
<br />
### Resumable uploads
Upload large files using resumable uploads. [Docs](/docs/guides/storage/uploads/resumable-uploads).
## Edge Functions
@@ -133,7 +148,13 @@ Transform images on the fly. [Docs](/docs/guides/storage/serving/image-transform
Globally distributed TypeScript functions to execute custom business logic. [Docs](/docs/guides/functions).
<br />
### Regional invocations
Execute an Edge Function in a region close to your database. [Docs](/docs/guides/functions/regional-invocation).
### NPM compatibility
Edge functions natively support NPM modules and Node built-in APIs. [Link](https://supabase.com/blog/edge-functions-node-npm).
## Project management
@@ -145,42 +166,75 @@ Use our CLI to develop your project locally and deploy to the Supabase Platform.
Manage your projects programmatically. [Docs](/docs/reference/api).
<br />
## Client libraries
Official client libraries for [JavaScript](/docs/reference/javascript/start), [Flutter](docs/reference/dart/initializing) and [Swift](/docs/reference/swift/introduction).
Unofficial libraries are supported by the community.
## Feature status
Both Postgres and the Supabase Platform are production-ready. Some tools we offer on top of Postgres are still under development.
Supabase Features are in 4 different states - Private Alpha, Public Alpha, Beta and Generally Available.
| Product | Feature | Stage | Available on self-hosted |
| -------------------------- | ---------------------- | ------ | ------------------------------------------- |
| Database | Postgres | `GA` | ✅ |
| Database | Triggers | `GA` | ✅ |
| Database | Functions | `GA` | ✅ |
| Database | Extensions | `GA` | ✅ |
| Database | Full Text Search | `GA` | ✅ |
| Database | Webhooks | `GA` | ✅ |
| Database | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
| Database | Vault | `beta` | ✅ |
| Studio | | `GA` | ✅ |
| Realtime | Postgres Changes | `GA` | ✅ |
| Realtime | Broadcast | `beta` | ✅ |
| Realtime | Presence | `beta` | ✅ |
| Storage | | `GA` | ✅ |
| Storage | S3 Backend | `GA` | ✅ |
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Smart CDN | `beta` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Image Transformations | `GA` | ✅ |
| Storage | Resumable Uploads | `beta` | ✅ |
| Edge Functions | | `beta` | ✅ |
| Auth | OAuth Providers | `GA` | ✅ |
| Auth | Passwordless | `GA` | ✅ |
| Auth | Next.js Auth Helpers | `beta` | ✅ |
| Auth | SvelteKit Auth Helpers | `beta` | ✅ |
| Auth | Remix Auth Helpers | `beta` | ✅ |
| CLI | | `beta` | ✅ Works with self-hosted |
| Management API | | `beta` | N/A |
| Client Library: JavaScript | | `GA` | N/A |
| Client Library: Dart | | `beta` | N/A |
### Private Alpha
Features are initially launched as a private alpha to gather feedback from the community. To join our early access program, send an email to product-ops@supabase.io.
### Public Alpha
he alpha stage indicates that the API might change in the future, not that the service isn’t stable. Even though the [uptime Service Level Agreement](https://supabase.com/sla) does not cover products in Alpha, we do our best to have the service as stable as possible.
### Beta
Features in Beta are tested by an external penetration tester for security issues. The API is guaranteed to be stable and there is a strict communication process for breaking changes.
### Generally Available
In addition to the Beta requirements, features in GA are covered by the [uptime SLA](https://supabase.com/sla).
| Product | Feature | Stage | Available on self-hosted |
| -------------- | -------------------------- | --------------- | ------------------------------------------- |
| Database | Postgres | `GA` | ✅ |
| Database | Vector Database | `GA` | ✅ |
| Database | Auto-generated Rest API | `GA` | ✅ |
| Database | Auto-generated GraphQL API | `GA` | ✅ |
| Database | Webhooks | `beta` | ✅ |
| Database | Vault | `public alpha` | ✅ |
| Platform | | `GA` | ✅ |
| Platform | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
| Platform | Custom Domains | `GA` | N/A |
| Platform | Network Restrictions | `beta` | N/A |
| Platform | SSL enforcement | `beta` | N/A |
| Platform | Branching | `public alpha` | N/A |
| Platform | Terraform Provider | `public alpha` | N/A |
| Platform | Read Replicas | `private alpha` | N/A |
| Platform | Fly Postgres | `private alpha` | N/A |
| Studio | | `GA` | ✅ |
| Studio | SSO | `GA` | ✅ |
| Realtime | Postgres Changes | `GA` | ✅ |
| Realtime | Broadcast | `GA` | ✅ |
| Realtime | Presence | `GA` | ✅ |
| Storage | | `GA` | ✅ |
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Smart CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Image Transformations | `GA` | ✅ |
| Storage | Resumable Uploads | `GA` | ✅ |
| Edge Functions | | `beta` | ✅ |
| Edge Functions | Regional Invocations | `beta` | ✅ |
| Edge Functions | NPM compatibility | `beta` | ✅ |
| Auth | | `GA` | ✅ |
| Auth | Email login | `GA` | ✅ |
| Auth | Social login | `GA` | ✅ |
| Auth | Phone login | `GA` | ✅ |
| Auth | Passwordless login | `GA` | ✅ |
| Auth | SSO with SAML | `GA` | ✅ |
| Auth | Authorization via RLS | `GA` | ✅ |
| Auth | Captcha protection | `GA` | ✅ |
| Auth | Server-side Auth | `beta` | ✅ |
| CLI | | `GA` | ✅ Works with self-hosted |
| Management API | | `GA` | N/A |
| Client Library | JavaScript | `GA` | N/A |
| Client Library | Flutter | `beta` | N/A |
| Client Library | Swift | `beta` | N/A |
- ✅ = Fully Available
- 🚧 = Available, but requires external tools or configuration
@@ -143,14 +143,14 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
export const load = (async ({ locals: { safeGetSession } }) => {
const { session, user } = await safeGetSession()
return {
session,
user,
}
}
}) satisfies LayoutServerLoad
```
> Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project.
@@ -163,7 +163,7 @@ import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/publi
import type { LayoutLoad } from './$types'
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr'
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
export const load = (async ({ fetch, data, depends }) => {
depends('supabase:auth')
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
@@ -192,7 +192,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
} = await supabase.auth.getSession()
return { supabase, session }
}
}) satisfies LayoutLoad
```
Update your `src/routes/+layout.svelte`:
@@ -23,9 +23,9 @@ Let's start building the SwiftUI app from scratch.
Open Xcode and create a new SwiftUI project.
Add the [supabase-swift](https://github.com/supabase-community/supabase-swift) dependency.
Add the [supabase-swift](https://github.com/supabase/supabase-swift) dependency.
Add the `https://github.com/supabase-community/supabase-swift` package to your app. For instructions, see the [Apple tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
Add the `https://github.com/supabase/supabase-swift` package to your app. For instructions, see the [Apple tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
Create a helper file to initialize the Supabase client.
You need the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
@@ -1,16 +1,17 @@
---
title: 'Read Replicas'
description: 'Deploy additional read-only databases in multiple regions for your project.'
subtitle: 'Deploy additional read-only databases in multiple regions for your project'
description: 'Deploy read-only databases across multiple regions, for lower latency.'
subtitle: 'Deploy read-only databases across multiple regions, for lower latency and better resource management.'
---
<Admonition type="note" label="Read Replicas are available to Pro, Team, and Enterprise organizations who have signed up for early access.">
Read Replicas are additional databases that are kept in sync with your Primary database. You can read your data from a Read Replica, which helps with:
[Request early access](https://forms.supabase.com/read-replica) and you'll be notified once Read Replicas are available to your organization.
</Admonition>
- **Load balancing:** Read Replicas reduce load on the Primary database. For example, you can use a Read Replica for complex analytical queries and reserve the Primary for user-facing create, update, and delete operations.
- **Improved latency:** For projects with a global user base, additional databases can be deployed closer to users to reduce latency.
- **Redundancy:** Read Replicas provide data redundancy.
<figure className="max-w-[700px] mx-auto">
<Image
className="hidden dark:block"
alt="Map view of all project databases"
@@ -25,57 +26,74 @@ subtitle: 'Deploy additional read-only databases in multiple regions for your pr
</figure>
Read Replicas are Postgres databases that are continuously synced to a Supabase project's Primary database. You can both read from and write to a Primary database, but you can _only_ read from a Read Replica, such as using `select` statements.
## About Read Replicas
The database you start with when launching a Supabase project is your Primary database. Read Replicas are kept in sync with the Primary through a process called "replication." Replication is asynchronous to ensure that transactions on the Primary aren't blocked. There is a delay between an update on the Primary and the time that a Read Replica receives the change. This delay is called "replication lag."
You can only read data from a Read Replica. This is in contrast to a Primary database, where you can both read and write:
| | select | insert | update | delete |
| ------------ | ------ | ------ | ------ | ------ |
| Primary | ✅ | ✅ | ✅ | ✅ |
| Read Replica | ✅ | - | - | - |
A database is spun up automatically with every Supabase project. This database is the Primary database. You can choose to add Read Replicas to your project and those Replicas will receive and synchronize data from the Primary database.
Replication is asynchronous and happens in the background so transactions on the Primary aren't blocked. The time delay between writing data to the Primary and the Read Replica receiving the change is replication lag.
Read Replicas have many applications:
- They reduce read load on the Primary database.
- For example, you can use a Read Replica for complex analytical queries and reserve the Primary for user-facing create, update, and delete operations.
- For projects that need to cover a global audience, additional databases can be deployed closer to users in any of our twelve supported regions to reduce latency.
- Read Replicas provide data redundancy.
## Prerequisites
A project needs to satisfy these requirements to deploy a Read Replica:
<Admonition type="note" label="Read Replicas are available on the Team and Enterprise plans">
Read replicas are available to projects created on the Teams and Enterprise plans after 2024 March 20. They will be progressively rolled out to older projects, and eventually available on the Pro plan.
If your project doesn't meet these criteria, you can join the [early access queue](https://forms.supabase.com/read-replica).
</Admonition>
Projects must meet these requirements to use Read Replicas:
1. Running on AWS.
- Support for projects on Fly.io is coming.
1. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons).
- Read Replicas are currently placed on the same compute instance as the database to make sure they can keep up with the Primary's activities.
1. Running on Postgres 15.
- For projects running on older versions of PostgreSQL, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
- Read Replicas are started on the same compute instance as the Primary to keep up with changes.
1. Running on Postgres 15+.
- For projects running on older versions of Postgres, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
1. Using [physical backups](/docs/guides/platform/backups#point-in-time-recovery)
- Physical backups are automatically enabled if using [PITR](/docs/guides/platform/backups#point-in-time-recovery)
- If you're not using PITR, you'll be able to switch to physical backups as part of the Read Replica setup process. Note that physical backups can't be downloaded from the dashboard in the way logical backups can.
## Getting started
Read Replicas can be managed from the [infrastructure settings page](https://supabase.com/dashboard/project/_/settings/infrastructure). For now, each project can only deploy up to two Read Replicas and all Read Replicas inherit the compute size of their Primary database.
To add a Read Replica, go to the [Infrastructure Settings page](/dashboard/project/_/settings/infrastructure) in your dashboard. Each project can deploy up to two Read Replicas and all Read Replicas inherit the compute size of their Primary database.
## Features
Read Replicas offer the following features:
### Dedicated endpoints
Each Read Replica has its own dedicated database and API endpoints. The database endpoint can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection info**. The API endpoint can be found in [dashboard API settings](https://supabase.com/dashboard/project/_/settings/api) under **Project URL**.
Each Read Replica has its own dedicated database and API endpoints.
A Read Replica's API endpoint only supports `GET` requests to the [REST API](https://supabase.com/docs/guides/api). Requests towards other Supabase products, such as Auth, Storage, and Realtime, are not able to utilize a Read Replica nor its API endpoint for now. We are working on adding Read Replica support for them.
- Find the database endpoint on the [Database Settings page](/dashboard/project/_/settings/database) under **Connection Info**
- Find the API endpoint on the [API Settings page](/dashboard/project/_/settings/api) under **Project URL**
### Dedicated connection pool
Read Replicas only support `GET` requests and the [REST API](/docs/guides/api). Requests to other Supabase products, such as Auth, Storage, and Realtime, aren't able to use a Read Replica or its API endpoint. Support for more products will be added in the future.
A connection pool through Supavisor is also available for each Read Replica. Its unique connection string can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection string**.
## Dedicated connection pool
A connection pool through Supavisor is also available for each Read Replica. Find the connection string on the [Database Settings page](/dashboard/project/_/settings/database) under **Connection String**.
## API load balancer
A load balancer is deployed to balance requests between your Primary database and Read Replicas. Find its endpoint on the [API Settings page](/dashboard/project/_/settings/api).
The load balancer uses a round-robin strategy to route `GET` requests to all available API endpoints, including the Primary database. Non-`GET` requests can also be sent through this endpoint, and will be routed to the Primary database. Read-only Postgres functions can be called using a [`GET` request](https://github.com/supabase/supabase-js/blob/v2.42.3/src/SupabaseClient.ts#L176-L177) as well.
If you remove all Read Replicas from your project, the load balancer and its endpoint are removed as well. Make sure to redirect requests back to your Primary database before removal.
### Querying through the SQL editor
In the SQL editor, you can choose if you want to run the query on a particular Read Replica.
<figure className="max-w-[700px] mx-auto">
<Image
className="hidden dark:block"
alt="SQL editor view."
@@ -90,23 +108,28 @@ A connection pool through Supavisor is also available for each Read Replica. Its
</figure>
In the SQL editor, you can choose if you want to run the query on the Primary or one of the Replicas.
### Metrics
### API load balancer
Observability and metrics for Read Replicas are not currently available in the Supabase Dashboard.
When Read Replicas are deployed, an API load balancer becomes available for the project. An endpoint for it is provisioned and can be found [here](https://supabase.com/dashboard/project/_/settings/api).
The load balancer uses a round-robin strategy to route `GET` requests to any of the API endpoints available, including that of the Primary database. Non-`GET` requests can also be sent through this endpoint. They are immediately redirected to the Primary database.
Similar to the Read Replica API endpoints, only requests to the REST API are available for now. Support for other Supabase products will become available once Read Replica support has been completed for them.
When a project no longer has any Read Replicas, the load balancer and its endpoint are brought down as well.
If you're ingesting your [project's metrics](/docs/guides/platform/metrics#accessing-the-metrics-endpoint) into your own environment, you can update [your scraping setup](https://github.com/supabase/supabase-grafana?tab=readme-ov-file#read-replica-support) to additionally ingest metrics from your Read Replicas.
### Centralized configuration management
All settings made through the dashboard will be propagated across all databases of a project. This ensures that no Read Replica get out of sync with its Primary database or with other Read Replicas.
All settings configured through the dashboard will be propagated across all databases of a project. This ensures that no Read Replica get out of sync with the Primary database or with other Read Replicas.
## How are Read Replicas made?
## Operations blocked by Read Replicas
### Project upgrades and data restorations
The following procedures require all Read Replicas for a project to be brought down before they can be performed:
1. [Project upgrades](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade)
1. [Data restorations](/docs/guides/platform/backups#pitr-restoration-process)
These operations need to be completed before Read Replicas can be re-deployed.
## About replication
We use a hybrid approach to replicate data from a Primary to its Read Replicas, combining the native methods of streaming replication and file-based log shipping.
@@ -135,14 +158,3 @@ In this replication method, the Primary continuously buffers WAL changes to a lo
We bring these two methods together to achieve quick, stable, and reliable replication. Each method addresses the limitations of the other. Streaming replication minimizes replication lag, while file-based log shipping provides a fallback. For file-based log shipping, we use our existing Point In Time Recovery (PITR) infrastructure. We regularly archive files from the Primary using [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, and ship the WAL files to S3.
We combine it with streaming replication to reduce replication lag. Once WAL-G files have been synced from S3, Read Replicas connect to the Primary and stream the WAL directly.
## Operations blocked by Read Replicas
### Project upgrades and data restorations
The following procedures require all Read Replicas for a project to be brought down before they can be performed:
1. [Project upgrades](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade)
1. [Data restorations](/docs/guides/platform/backups#pitr-restoration-process)
These operations need to be completed before Read Replicas can be re-deployed.
+1 -1
View File
@@ -14,7 +14,7 @@ The Management API is in `beta`. It is usable in it's current state, but it's li
## Authentication
All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization Bearer <supabase_personal_token`.
All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization: Bearer <supabase_personal_token>`.
To generate or manage your API token, visit your [account](https://supabase.com/dashboard/account/tokens) page.
Your API tokens carry the same privileges as your user account, so be sure to keep it secret.
+63
View File
@@ -0,0 +1,63 @@
---
id: installing
title: 'Installing'
slug: installing
custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml
---
### Install using Swift Package Manager
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can install Supabase package using Swift Package Manager.
The package exposes multiple libraries, you can choose between adding all of them using Supabase, or some of:
- `Auth`
- `Realtime`
- `Postgrest`
- `Functions`
- `Storage`
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
size="small"
type="underlined"
defaultActiveId="swift"
queryGroup="framework"
>
<TabPanel id="swift" label="Package.swift">
```swift
let package = Package(
...
dependencies: [
...
.package(
url: "https://github.com/supabase/supabase-swift.git",
from: "2.0.0"
),
],
targets: [
.target(
name: "YourTargetName",
dependencies: [
.product(
name: "Supabase", // Auth, Realtime, Postgrest, Functions, or Storage
package: "supabase-swift"
),
]
)
]
)
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
+2 -14
View File
@@ -8,25 +8,13 @@ hideTitle: true
<IconMenuSwift width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-foreground m-0">Swift Client Library</h1>
<h2 className="text-base font-mono text-foreground-light">
@supabase-community/supabase-swift
</h2>
<h2 className="text-base font-mono text-foreground-light">@supabase/supabase-swift</h2>
</div>
</div>
{/* prettier-ignore */}
<div className="max-w-xl">
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase-community/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Swift projects.
</div>
{/* prettier-ignore */}
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
The Swift client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.
Huge thanks to official maintainers, [Guilherme](https://github.com/grdsdev) and [Maail](https://github.com/maail).
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
</div>
+12 -5
View File
@@ -1,12 +1,17 @@
import { useState, useCallback, useEffect } from 'react'
const useHash = () => {
const [hash, setHash] = useState(() =>
typeof window !== 'undefined' ? window.location.hash.split('#')[1] : undefined
)
const [hash, setHash] = useState(() => {
if (typeof window !== 'undefined') {
const url = new URL(window.location.href)
return url.hash.substring(1) // Removes the leading '#'
}
return undefined
})
const hashChangeHandler = useCallback(() => {
setHash(window.location.hash.split('#')[1])
const url = new URL(window.location.href)
setHash(url.hash.substring(1))
}, [])
useEffect(() => {
@@ -18,7 +23,9 @@ const useHash = () => {
const updateHash = useCallback(
(newHash) => {
if (newHash !== hash) window.location.hash = newHash
if (newHash !== hash) {
window.location.hash = newHash
}
},
[hash]
)
+3 -3
View File
@@ -22,8 +22,8 @@
"@code-hike/mdx": "^0.9.0",
"@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.3.0",
"@next/bundle-analyzer": "^14.1.4",
"@next/mdx": "^14.1.4",
"@next/bundle-analyzer": "^14.2.0",
"@next/mdx": "^14.2.0",
"@octokit/auth-app": "^6.0.3",
"@octokit/core": "^5.1.0",
"@octokit/graphql": "^7.0.2",
@@ -54,7 +54,7 @@
"mdx-mermaid": "2.0.0-rc3",
"mermaid": "^10.0.2",
"micromark-extension-mdxjs": "^1.0.0",
"next": "^14.1.4",
"next": "^14.2.0",
"next-mdx-remote": "^4.4.1",
"next-plugin-yaml": "^1.0.1",
"next-seo": "^6.5.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+79 -9
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 1.151.1
version: 1.157.2
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -10,6 +10,8 @@ info:
description: |
Supabase CLI provides you with tools to develop your application locally, and deploy your application to the Supabase platform.
tags:
- id: quick-start
title: Quick Start
- id: local-dev
title: Local Development
- id: management-api
@@ -1349,19 +1351,21 @@ commands:
If your local and remote migration history goes out of sync, you can repair the remote history by marking specific migrations as `--status applied` or `--status reverted`. Marking as `reverted` will delete an existing record from the migration history table while marking as `applied` will insert a new record.
For example, your migration history table may look like this after running `db pull` for the first time.
For example, your migration history may look like the table below, with missing entries in either local or remote.
```bash
$ supabase migration list
LOCAL │ REMOTE │ TIME (UTC)
─────────────────┼────────────────┼──────────────────────
20230103054303 │ 20230103054303 │ 2023-01-03 05:43:03
│ 20230103054303 │ 2023-01-03 05:43:03
20230103054315 │ │ 2023-01-03 05:43:15
```
To reset your migration history to a clean state, first delete your local migration file.
```bash
$ rm supabase/migrations/20230103054303_remote_commit.sql
$ rm supabase/migrations/20230103054315_remote_commit.sql
$ supabase migration list
LOCAL │ REMOTE │ TIME (UTC)
─────────────────┼────────────────┼──────────────────────
@@ -1372,13 +1376,30 @@ commands:
```bash
$ supabase migration repair 20230103054303 --status reverted
Repaired migration history: 20230103054303 => reverted
Connecting to remote database...
Repaired migration history: [20220810154537] => reverted
Finished supabase migration repair.
$ supabase migration list
LOCAL │ REMOTE │ TIME (UTC)
─────────────────┼────────────────┼──────────────────────
```
Now you can run `db pull` again to dump the remote schema as a local migration file.
```bash
$ supabase db pull
Connecting to remote database...
Schema written to supabase/migrations/20240414044403_remote_schema.sql
Update remote migration history table? [Y/n]
Repaired migration history: [20240414044403] => applied
Finished supabase db pull.
$ supabase migration list
LOCAL │ REMOTE │ TIME (UTC)
─────────────────┼────────────────┼──────────────────────
20240414044403 │ 20240414044403 │ 2024-04-14 04:44:03
```
examples:
- id: basic-usage
name: Mark a migration as reverted
@@ -1541,9 +1562,8 @@ commands:
flags:
- id: name
name: --name <string>
description: |
Name that will be used to store token in your settings, defaults to built-in token name generator
default_value: ''
description: Name that will be used to store token in your settings
default_value: built-in token name generator
- id: no-browser
name: --no-browser
description: Do not open browser automatically
@@ -2320,9 +2340,13 @@ commands:
usage: supabase init [flags]
subcommands: []
flags:
- id: force
name: --force
description: Overwrite existing supabase/config.toml.
default_value: 'false'
- id: use-orioledb
name: --use-orioledb
description: Use OrioleDB storage engine for Postgres
description: Use OrioleDB storage engine for Postgres.
default_value: 'false'
- id: with-intellij-settings
name: --with-intellij-settings
@@ -2828,6 +2852,39 @@ commands:
Optionally, a new row can be inserted into the migration history table to reflect the current state of the remote database.
If no entries exist in the migration history table, `pg_dump` will be used to capture all contents of the remote schemas you have created. Otherwise, this command will only diff schema changes against the remote database, similar to running `db diff --linked`.
examples:
- id: basic-usage
name: Basic usage
code: supabase db pull
response: |
Connecting to remote database...
Schema written to supabase/migrations/20240414044403_remote_schema.sql
Update remote migration history table? [Y/n]
Repaired migration history: [20240414044403] => applied
Finished supabase db pull.
The auth and storage schemas are excluded. Run supabase db pull --schema auth,storage again to diff them.
- id: local-studio
name: Local studio
code: supabase db pull --local
response: |
Connecting to local database...
Setting up initial schema....
Creating custom roles supabase/roles.sql...
Applying migration 20240414044403_remote_schema.sql...
No schema changes found
The auth and storage schemas are excluded. Run supabase db pull --schema auth,storage again to diff them.
exit status 1
- id: custom-schemas
name: Custom schemas
code: supabase db pull --schema auth,storage
response: |
Connecting to remote database...
Setting up initial schema....
Creating custom roles supabase/roles.sql...
Applying migration 20240414044403_remote_schema.sql...
No schema changes found
Try rerunning the command with --debug to troubleshoot the error.
exit status 1
tags: []
links: []
usage: supabase db pull [migration name] [flags]
@@ -3437,3 +3494,16 @@ commands:
description: enable experimental features
required: true
default_value: 'false'
- id: supabase-bootstrap
title: supabase bootstrap
summary: Bootstrap a Supabase project from a starter template
tags:
- quick-start
links: []
usage: supabase bootstrap [template] [flags]
subcommands: []
flags:
- id: password
name: -p, --password <string>
description: Password to your remote Postgres database.
default_value: ''
@@ -33,6 +33,7 @@
"reference_python_v2",
"reference_csharp_v0",
"reference_swift_v1",
"reference_swift_v2",
"reference_kotlin_v1",
"reference_kotlin_v2"
]
@@ -48,6 +49,7 @@
"reference_python_v2",
"reference_csharp_v0",
"reference_swift_v1",
"reference_swift_v2",
"reference_kotlin_v1",
"reference_kotlin_v2"
]
@@ -986,6 +988,7 @@
"reference_python_v2",
"reference_csharp_v0",
"reference_swift_v1",
"reference_swift_v2",
"reference_kotlin_v1",
"reference_kotlin_v2"
],
+32 -2
View File
@@ -568,7 +568,7 @@ functions:
notes: |
- Requires either an email or phone number.
- This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number.
- If the user doesn't exist, `signInWithOtp()` will signup the user instead. To restrict this behaviour, you can set `shouldCreateUser` in `SignInWithPasswordlessCredentials.options` to `false`.
- If the user doesn't exist, `signInWithOtp()` will signup the user instead. To restrict this behavior, you can set `shouldCreateUser` in `SignInWithPasswordlessCredentials.options` to `false`.
- If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP.
- If you're using phone, you can configure whether you want the user to receive a OTP.
- The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls).
@@ -4925,7 +4925,7 @@ functions:
notes: |
- Requires an Authorization header.
- Invoke params generally match the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) spec.
- When you pass in a body to your function, we automatically attach the Content-Type header for `Blob`, `ArrayBuffer`, `File`, `FormData` and `String`. If it doesn't match any of these types we assume the payload is `json`, serialise 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 you pass in a body to your function, we automatically attach the Content-Type header for `Blob`, `ArrayBuffer`, `File`, `FormData` and `String`. 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 behavior by passing in a `Content-Type` header of your own.
- Responses are automatically parsed as `json`, `blob` and `form-data` depending on the `Content-Type` header sent by your function. Responses are parsed as `text` by default.
examples:
- id: basic-invocation
@@ -4991,6 +4991,36 @@ functions:
method: 'DELETE'
})
```
- id: regional-invocation
name: Invoking a Function in the UsEast1 region
description: |
Here are the available regions:
- `FunctionRegion.Any`
- `FunctionRegion.ApNortheast1`
- `FunctionRegion.ApNortheast2`
- `FunctionRegion.ApSouth1`
- `FunctionRegion.ApSoutheast1`
- `FunctionRegion.ApSoutheast2`
- `FunctionRegion.CaCentral1`
- `FunctionRegion.EuCentral1`
- `FunctionRegion.EuWest1`
- `FunctionRegion.EuWest2`
- `FunctionRegion.EuWest3`
- `FunctionRegion.SaEast1`
- `FunctionRegion.UsEast1`
- `FunctionRegion.UsWest1`
- `FunctionRegion.UsWest2`
isSpotlight: true
code: |
```js
import { createClient, FunctionRegion } from '@supabase/supabase-js'
const { data, error } = await supabase.functions.invoke('hello', {
body: { foo: 'bar' },
region: FunctionRegion.UsEast1
})
```
- id: calling-with-get-verb
name: Calling with GET HTTP verb
description: |
+13 -13
View File
@@ -57,7 +57,7 @@ functions:
scheme = "deeplink scheme"
// On Android only, you can set OAuth and SSO logins to open in a custom tab, rather than an external browser:
defaultExternalAuthAction = ExternalAuthAction.CUSTOM_TABS //defaults to EXTERNAL_BROWSER
defaultExternalAuthAction = ExternalAuthAction.CustomTabs() //defaults to ExternalAuthAction.ExternalBrowser
}
```
4. **Call platform specific function on startup** \
@@ -684,10 +684,10 @@ functions:
isOptional: true # Not directly `optional`, as there are two overloads one with and one without `parameters`.
type: T
description: Parameters to pass to the function. T can be any serializable type.
- name: head
- name: method
isOptional: true
type: Boolean
description: If true, delete will delete the selected data.
type: RpcMethod
description: The HTTP method to use. Defaults to `RpcMethod.POST`
- name: request
isOptional: true
type: PostgrestRequestBuilder.() -> Unit
@@ -3317,7 +3317,7 @@ functions:
title: 'reauthenticate()'
$ref: '@supabase/gotrue-js.GoTrueClient.reauthenticate'
notes: |
- This method is used together with `modifyUser()` when a user's password needs to be updated.
- This method is used together with `updateUser()` when a user's password needs to be updated.
- This method will send a nonce to the user's email. If the user doesn't have a confirmed email address, the method will send the nonce to the user's confirmed phone number instead.
examples:
- id: send-reauthentication-nonce
@@ -3422,11 +3422,11 @@ functions:
val user = supabase.auth.retrieveUser("JWT")
```
- id: update-user
title: 'modifyUser()'
title: 'updateUser()'
$ref: '@supabase/gotrue-js.GoTrueClient.updateUser'
notes: |
Modifies the user data.
- In order to use the `modifyUser()` method, the user needs to be signed in first.
- In order to use the `updateUser()` method, the user needs to be signed in first.
- By default, email updates sends a confirmation link to both the user's current and new email.
To only send a confirmation link to the user's new email, disable **Secure email change** in your project's [email auth provider settings](https://supabase.com/dashboard/project/_/auth/providers).
params:
@@ -3469,7 +3469,7 @@ functions:
isSpotlight: false
code: |
```kotlin
val user = supabase.auth.modifyUser {
val user = supabase.auth.updateUser {
email = "newEmail@email.com"
}
```
@@ -3478,7 +3478,7 @@ functions:
isSpotlight: false
code: |
```kotlin
val user = supabase.auth.modifyUser {
val user = supabase.auth.updateUser {
password = "secretPassword"
}
```
@@ -3487,7 +3487,7 @@ functions:
isSpotlight: true
code: |
```kotlin
val user = supabase.auth.modifyUser {
val user = supabase.auth.updateUser {
data {
put("name", "John")
}
@@ -3681,11 +3681,11 @@ functions:
Sends a password reset request to the given email address.
- The password reset flow consist of 2 broad steps: (i) Allow the user to login via the password reset link; (ii) Update the user's password.
- The `resetPasswordForEmail()` only sends a password reset link to the user's email.
To update the user's password, see [`modifyUser()`](/docs/reference/kotlin/auth-updateuser).
To update the user's password, see [`updateUser()`](/docs/reference/kotlin/auth-updateuser).
- The user gets redirected back to your app, assuming you setup [OTP handling](/docs/reference/kotlin/initializing)
- After the user has been redirected successfully, prompt them for a new password and call `modifyUser()`:
- After the user has been redirected successfully, prompt them for a new password and call `updateUser()`:
```kotlin
supabase.auth.modifyUser {
supabase.auth.updateUser {
password = "1234567"
}
```
+398 -235
View File
@@ -92,7 +92,7 @@ functions:
- To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/swift/get-user).
examples:
- id: sign-up
name: Sign up
name: Sign up with email and password
isSpotlight: true
code: |
```swift
@@ -101,6 +101,30 @@ functions:
password: "example-password"
)
```
- id: sign-up-phone
name: Sign up with a phone number and password (SMS)
isSpotlight: true
code: |
```swift
try await supabase.auth.signUp(
phone: "123456789",
password: "example-password",
channel: "sms"
)
```
- id: sign-up-phone-whatsapp
name: Sign up with a phone number and password (whatsapp)
isSpotlight: true
description: |
The user will be sent a WhatsApp message which contains a OTP. By default, a given user can only request a OTP once every 60 seconds. Note that a user will need to have a valid WhatsApp account that is linked to Twilio in order to use this feature.
code: |
```swift
try await supabase.auth.signUp(
phone: "123456789",
password: "example-password",
channel: "whatsapp"
)
```
- id: sign-up-with-additional-user-metadata
name: Sign up with additional user metadata
isSpotlight: false
@@ -120,6 +144,7 @@ functions:
- id: sign-up-with-redirect
name: Sign up with a redirect URL
description: |
- You can provide a default redirect URL when initializing the client.
- See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
code: |
```swift
@@ -129,6 +154,69 @@ functions:
redirectTo: URL(string: "https://example.com/welcome")!
)
```
- id: on-auth-state-change
title: "onAuthStateChange()"
notes: |
- Subscribes to important events occurring on the user's session.
- Emitted events:
- `INITIAL_SESSION`
- Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
- `SIGNED_IN`
- Emitted each time a user session is confirmed or re-established, including on user sign in.
- Avoid making assumptions as to when this event is fired, this may occur even when the user is already signed in. Instead, check the user object attached to the event to see if a new user has signed in and update your application's UI.
- `SIGNED_OUT`
- Emitted when the user signs out. This can be after:
- A call to `supabase.auth.signOut()`.
- After the user's session has expired for any reason:
- User has signed out on another device.
- The session has reached its timebox limit or inactivity timeout.
- User has signed in on another device with single session per user enabled.
- Check the [User Sessions](/docs/guides/auth/sessions) docs for more information.
- Use this to clean up any local storage your application has associated with the user.
- `TOKEN_REFRESHED`
- Emitted each time a new access and refresh token are fetched for the signed in user.
- It's best practice and highly recommended to extract the access token (JWT) and store it in memory for further use in your application.
- Avoid frequent calls to `supabase.auth.session` for the same purpose.
- There is a background process that keeps track of when the session should be refreshed so you will always receive valid tokens by listening to this event.
- The frequency of this event is related to the JWT expiry limit configured on your project.
- `USER_UPDATED`
- Emitted each time the `supabase.auth.update(user:)` method finishes successfully. Listen to it to update your application's UI based on new profile information.
- `PASSWORD_RECOVERY`
- Emitted instead of the `SIGNED_IN` event when the user lands on a page that includes a password recovery link in the URL.
- Use it to show a UI to the user where they can [reset their password](/docs/guides/auth/passwords#resetting-a-users-password-forgot-password).
examples:
- id: listen-to-auth-changes
name: Listen to auth changes
isSpotlight: true
code: |
```swift
// Using AsyncStream
for await (event, session) in await supabase.auth.authStateChanges {
print(event, session)
}
// Using Closure
let subscription = await supabase.auth.onAuthStateChange { event, session in
print(event, session)
}
// call remove() to remove subscription
subscription.remove()
```
description: |
- When using closure based, remember to call `remove()` on the returned subscription.
- When using AsyncStream, it automatically removes the subscription when AsyncStream is canceled, or finishes.
- id: list-to-a-specific-event
name: Listen to a specific event
code: |
```swift
for await (_, session) in await supabase.auth.authStateChanges
.filter({ $0.event == .signedIn }) {
// handle signIn event.
}
```
- id: sign-in-anonymously
title: "signInAnonymously()"
notes: |
@@ -187,14 +275,14 @@ functions:
- id: sign-in-with-otp
title: "signInWithOTP()"
notes: |
- Requires either an email or phone number.
- This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number.
- If the user doesn't exist, `signInWithOTP()` will signup the user instead. To restrict this behavior, you can set `shouldCreateUser` to `false`.
- If the user doesn't exist, `signInWithOTP()` will signup the user instead. To restrict this behavior, you can set `shouldCreateUser` to `false``.
- If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP.
- If you're using phone, you can configure whether you want the user to receive a OTP.
- The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls).
- See [redirect URLs and wildcards](/docs/guides/auth#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
- Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`.
- When using magic links, specify a `redirectTo` that matches a configured url scheme in your iOS app, so Supabase can correctly redirect back to your app.
- See our [Twilio Phone Auth Guide](/docs/guides/auth/phone-login/twilio) for details about configuring WhatsApp sign in.
examples:
- id: sign-in-with-email
@@ -216,13 +304,55 @@ functions:
```swift
try await supabase.auth.signInWithOTP(phone: "+13334445555")
```
- id: sign-in-with-whatsapp-otp
name: Sign in with WhatsApp OTP
isSpotlight: false
description: The user will be sent a WhatsApp message which contains a OTP. By default, a given user can only request a OTP once every 60 seconds. Note that a user will need to have a valid WhatsApp account that is linked to Twilio in order to use this feature.
code: |
```swift
try await supabase.auth.signInWithOTP(
phone: "+13334445555",
channel: "whatsapp"
)
```
- id: sign-in-with-oauth
title: "getOAuthSignInURL()"
title: "signInWithOAuth()"
notes: |
- This method is used for signing in using a third-party provider.
- Supabase supports many different [third-party providers](https://supabase.com/docs/guides/auth#providers).
examples:
- id: sign-in-using-a-third-party-provider
- id: sign-in-using-ASWebAuthenticationSession
name: Sign in with OAuth using ASWebAuthenticationSession
isSpotlight: true
description: |
- Use `configure` parameter to customize the `ASWebAuthenticationSession`
code: |
```swift
let session = try await supabase.auth.signInWithOAuth(
provider: .github
) { (session: ASWebAuthenticationSession) in
// customize session
}
```
- id: sign-in-using-generic-flow
name: Sign in with OAuth and customize flow
isSpotlight: true
description: |
- Use `launchFlow` parameter to customize the flow.
code: |
```swift
let session = try await supabase.auth.signInWithOAuth(
provider: .github
) { url in
// use url to start OAuth flow
// and return a result url that contains the OAuth token.
// ...
return resultURL
}
```
- id: sign-in-using-manual-implementation
name: Sign in using a third-party provider
isSpotlight: true
description: |
@@ -231,7 +361,7 @@ functions:
- When using `ASWebAuthenticationSession` or any other implementation, use the returning URL as input to `session(from:)` method.
code: |
```swift
let url = try await supabase.auth.getOAuthSignInURL(provider: .github)
let url = try await supabase.auth.getOAuthSignInURL(provider: .github, redirectTo: URL(string: "my-app-scheme://"))
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "my-app-scheme") { url, error in
guard let url else { return }
@@ -246,21 +376,6 @@ functions:
session.start()
```
- id: sign-in-using-a-third-party-provider-with-redirect
name: Sign in using a third-party provider with redirect
isSpotlight: false
description: |
- When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectTo` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls). It does not redirect the user immediately after invoking this method.
- See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
- getOAuthSignInURL() provides the URL which needs to be opened in a SFSafariViewController instance.
- The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs.
code: |
```swift
let url = try await supabase.auth.getOAuthSignInURL(
provider: .google,
redirectTo: URL(string: "https://example.com/welcome")!
)
```
- id: sign-in-with-scopes
name: Sign in with scopes
isSpotlight: false
@@ -269,7 +384,7 @@ functions:
You may also need to specify the scopes in the provider's OAuth app settings, depending on the provider. The list of scopes will be documented by the third-party provider you are using and specifying scopes will enable you to use the OAuth provider token to call additional APIs supported by the third-party provider to get more information.
code: |
```swift
let url = try await supabase.auth.getOAuthSignInURL(
let url = try await supabase.auth.signInWithOAuth(
provider: .github,
scopes: "repo gist notifications"
)
@@ -290,6 +405,45 @@ functions:
)
)
```
- id: sign-in-with-sso
title: 'signInWithSSO()'
notes: |
- Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this.
- If you've associated an email domain to the identity provider, you can use the `domain` property to start a sign-in flow.
- In case you need to use a different way to start the authentication flow with an identity provider, you can use the `providerId` property. For example:
- Mapping specific user email addresses with an identity provider.
- Using different hints to identity the identity provider to be used by the user, like a company-specific page, IP address or other tracking information.
examples:
- id: sign-in-with-domain
name: Sign in with email domain
isSpotlight: true
code: |
```swift
// You can extract the user's email domain and use it to trigger the
// authentication flow with the correct identity provider.
let url = try await await supabase.auth.signInWithSSO{
domain: "company.com"
}
// Open the URL using your preferred method to complete sign-in process.
UIApplication.shared.open(url)
```
- id: sign-in-with-provider-uuid
name: Sign in with provider UUID
isSpotlight: true
code: |
```swift
// Useful when you need to map a user's sign in request according
// to different rules that can't use email domains.
let url = try await supabase.auth.signInWithSSO{
providerId: "21648a9d-8d5a-4555-a9d1-d6375dc14e92"
}
// Open the URL using your preferred method to complete sign-in process.
UIApplication.shared.open(url)
```
- id: sign-out
title: "signOut()"
notes: |
@@ -410,6 +564,30 @@ functions:
```swift
let identities = try await supabase.auth.userIdentities()
```
- id: unlink-identity
title: 'unlinkIdentity()'
notes: |
- The **Enable Manual Linking** option must be enabled from your [project's authentication settings](/dashboard/project/_/settings/auth).
- The user needs to be signed in to call `unlinkIdentity()`.
- The user must have at least 2 identities in order to unlink an identity.
- The identity to be unlinked must belong to the user.
examples:
- id: unlink-identity
name: Unlink an identity
isSpotlight: true
code: |
```swift
// retrieve all identites linked to a user
let identities = try await supabase.auth.userIdentities()
// find the google identity
let googleIdentity = identities.first {
$0.provider == .google
}
// unlink the google identity
try await supabase.auth.unlinkIdentity(googleIdentity)
```
- id: send-password-reauthentication
title: "reauthenticate()"
notes: |
@@ -512,29 +690,7 @@ functions:
```swift
let session = try await supabase.auth.refreshSession(refreshToken: "custom-refresh-token")
```
- id: on-auth-state-change
title: "authStateChanges"
notes: |
- Types of auth events: `INITIAL_SESSION`, `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `USER_UPDATED`, `PASSWORD_RECOVERY`, `MFA_CHALLENGE_VERIFIED`
- The `INITIAL_SESSION` can be used to allow you to invoke the callback function when `authStateChanges` is first called.
examples:
- id: listen-to-auth-changes
name: Listen to auth changes
isSpotlight: true
code: |
```swift
for await (event, session) in await supabase.auth.authStateChanges {
print(event, session)
}
```
- id: list-to-a-specific-event
name: Listen to a specific event
code: |
```swift
for await (_, session) in await supabase.auth.authStateChanges.filter({ $0.event == .signedIn }) {
// handle signIn event.
}
```
- id: exchange-code-for-session
title: "exchangeCodeForSession()"
notes: |
@@ -679,6 +835,41 @@ functions:
```swift
let factors = try await supabase.auth.mfa.listFactors()
```
- id: admin-api
title: 'Overview'
notes: |
- Any method under the `supabase.auth.admin` namespace requires a `service_role` key.
- These methods are considered admin methods and should be called on a trusted server. Never expose your `service_role` key in the browser.
examples:
- id: create-auth-admin-client
name: Create server-side auth client
isSpotlight: true
code: |
```swift
import Supabase
let supabase = SupabaseClient(
supabaseURL: supabaseURL,
supabaseKey: serviceRoleKey
)
// Access auth admin api
let adminAuthClient = supabase.auth.admin
```
- id: delete-user
title: 'deleteUser()'
notes: |
- The `deleteUser()` method requires the user's ID, which maps to the `auth.users.id` column.
examples:
- id: removes-a-user
name: Removes a user
isSpotlight: true
code: |
```swift
try await supabase.auth.admin.deleteUser(
id: "715ed5db-f090-4b8c-a067-640ecee36aa0"
)
```
- id: select
title: "Fetch data: select()"
notes: |
@@ -2899,133 +3090,140 @@ functions:
isSpotlight: true
code: |
```swift
let channel = supabase
.realtime
.channel("room1")
let channel = await supabase.channel("room1")
channel
.on("broadcast", filter: ChannelFilter(event: "cursor-pos")) { message in
print("Cursor position received!", message.payload)
}
.subscribe { status, error in
if status == .subscribed {
Task {
await channel.send(
type: .broadcast,
event: "cursor-pos",
payload: ["x": Double.random(in: 0...1), "y": Double.random(in: 0...1)]
)
}
}
let broadcastStream = await channel.broadcastStream(event: "cursor-pos")
await channel.subscribe()
Task {
for await message in broadcastMessage {
print("Cursor position received", message.payload)
}
}
Task {
await channel.broadcast(
event: "cursor-pos",
message: [
"x": .double(.random(in: 0...1)),
"y": .double(.random(in: 0...1))
]
)
}
```
- id: listen-to-presence-sync
name: Listen to presence sync
isSpotlight: true
- id: listen-to-presence-updates
name: Listen to presence updates
code: |
```swift
let channel = supabase.realtime.channel("room1")
channel
.on("presence", filter: ChannelFilter(event: "sync")) { _ in
print("Synced presence state: ", channel.presenceState())
struct PresenceState: Codable {
let username: String
}
let channel = await supabase.channel("channelId")
let presenceChange = await channel.presenceChange()
await channel.subscribe()
Task {
for await presence in presenceChange {
let joins = try presence.decodeJoins(as: PresenceState.self)
let leaves = try presence.decodeLeaves(as: PresenceState.self)
}
.subscribe { status, error in
if status == .subscribed {
Task {
await channel.track(["online_at": Date().ISO8601Format()])
}
}
}
```
- id: listen-to-presence-join
name: Listen to presence join
isSpotlight: true
code: |
```swift
let channel = supabase.realtime.channel("room1")
channel
.on("presence", filter: ChannelFilter(event: "join")) { message in
print("Newly joined presences: ", message.payload)
}
.subscribe { status, error in
if status == .subscribed {
Task {
await channel.track(["online_at": Date().ISO8601Format()])
}
}
}
```
- id: listen-to-presence-leave
name: Listen to presence leave
isSpotlight: true
code: |
```swift
let channel = supabase.realtime.channel("room1")
channel
.on("presence", filter: ChannelFilter(event: "leave")) { message in
print("Newly left presences: ", message.payload)
}
.subscribe { status, error in
if status == .subscribed {
Task {
await channel.track(["online_at": Date().ISO8601Format()])
await channel.untrack()
}
}
}
```
}
// Send your own state
Task {
try await channel.track(PresenceState(username: "John"))
}
- id: listen-to-all-database-changes
name: Listen to all database changes
isSpotlight: true
code: |
```swift
supabase.realtime
.channel("room1")
.on("postgres_changes", filter: ChannelFilter(event: "*", schema: "*")) { message in
print("Change received!", message.payload)
let channel = await supabase.channel("channelId")
let changeStream = await channel.postgresChange(AnyAction.self, schema: "public")
await channel.subscribe()
for await change in changeStream {
switch change {
case .delete(let action): print("Deleted: \(action.oldRecord)")
case .insert(let action): print("Inserted: \(action.record)")
case .select(let action): print("Selected: \(action.record)")
case .update(let action): print("Updated": \(action.oldRecord) with \(action.record)")
}
.subscribe()
}
```
- id: listen-to-a-specific-table
name: Listen to a specific table
code: |
```swift
supabase.realtime
.channel("room1")
.on("postgres_changes", filter: ChannelFilter(event: "*", schema: "public", table: "countries")) { message in
print("Change received!", message.payload)
let channel = await supabase.channel("channelId")
let changeStream = await channel.postgresChange(
AnyAction.self,
schema: "public",
table: "users"
)
await channel.subscribe()
for await change in changeStream {
switch change {
case .delete(let action): print("Deleted: \(action.oldRecord)")
case .insert(let action): print("Inserted: \(action.record)")
case .select(let action): print("Selected: \(action.record)")
case .update(let action): print("Updated": \(action.oldRecord) with \(action.record)")
}
.subscribe()
}
```
- id: listen-to-inserts
name: Listen to inserts
code: |
```swift
supabase.realtime
.channel("room1")
.on("postgres_changes", filter: ChannelFilter(event: "INSERT", schema: "public", table: "countries")) { message in
print("Change received!", message.payload)
}
.subscribe()
let channel = await supabase.channel("channelId")
let insertions = await channel.postgresChange(
InsertAction.self,
schema: "public",
table: "users"
)
await channel.subscribe()
for await insert in insertions {
print("Inserted: \(insert.record)")
}
```
- id: listen-to-updates
name: Listen to updates
description: |
By default, Supabase will send only the updated record. If you want to receive the previous values as well you can
enable full replication for the table you are listening to:
enable full replication for the table you are listening too:
```sql
alter table "your_table" replica identity full;
```
code: |
```swift
supabase.realtime
.channel("room1")
.on("postgres_changes", filter: ChannelFilter(event: "UPDATE", schema: "public", table: "countries")) { message in
print("Change received!", message.payload)
}
.subscribe()
```
let channel = await supabase.channel("channelId")
let updates = await channel.postgresChange(
UpdateAction.self,
schema: "public",
table: "users"
)
await channel.subscribe()
for await update in updates {
print("Updated: \(update.oldRecord) with \(update.record)")
}
```
- id: listen-to-deletes
name: Listen to deletes
description: |
@@ -3037,131 +3235,96 @@ functions:
```
code: |
```swift
supabase.realtime
.channel("room1")
.on("postgres_changes", filter: ChannelFilter(event: "DELETE", schema: "public", table: "countries")) { message in
print("Change received!", message.payload)
}
.subscribe()
```
- id: listen-to-multiple-events
name: Listen to multiple events
description: You can chain listeners if you want to listen to multiple events for each table.
code: |
```swift
supabase.realtime
.channel("room1")
.on("postgres_changes", filter: ChannelFilter(event: "INSERT", schema: "public", table: "countries"), handler: handleRecordInserted)
.on("postgres_changes", filter: ChannelFilter(event: "DELETE", schema: "public", table: "countries"), handler: handleRecordDeleted)
.subscribe()
let channel = await supabase.channel("channelId")
func handleRecordInserted(_ message: Message) {
// handle message
}
let deletions = await channel.postgresChange(
DeleteAction.self,
schema: "public",
table: "users"
)
func handleRecordDeleted(_ message: Message) {
// handle message
await channel.subscribe()
for await deletion in deletions {
print("Deleted: \(deletion.oldRecord)")
}
```
- id: listening-to-row-level-changes
name: Listen to row level changes
description: You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
notes: |
- ``eq`` filter works with all database types as under the hood, it's casting both the filter value and the database value to the correct type and then comparing them.
code: |
```swift
supabase.realtime
.channel("room1")
.on(
"postgres_changes",
filter: ChannelFilter(
event: "INSERT",
schema: "public",
table: "countries",
filter: "id=eq.200"
),
handler: handleRecordInserted
)
.subscribe()
let channel = await supabase.channel("channelId")
func handleRecordInserted(_ message: Message) {
// handle message
}
```
- id: broadcast-message
title: broadcastMessage()
description: |
Broadcast a message to all connected clients to a channel.
notes: |
- When using REST you don't need to subscribe to the channel
examples:
- id: send-a-message
name: Send a message via websocket
isSpotlight: true
code: |
```swift
supabase.realtime
.channel("room1")
.subscribe { status, error in
if status == .subscribed {
Task {
await channel.send(
type: "broadcast",
event: "cursor-pos",
payload: ["x": Double.random(in: 0...1), "y": Double.random(in: 0...1)]
)
}
}
}
```
- id: send-a-message-via-rest
name: Send a message via REST
isSpotlight: true
code: |
```swift
await supabase.realtime
.channel("room1")
.send(
type: "broadcast",
event: "cursor-pos",
payload: ["x": Double.random(in: 0...1), "y": Double.random(in: 0...1)]
)
```
- id: get-channels
title: channels
examples:
- id: get-all-channels
name: Get all channels
isSpotlight: true
code: |
```swift
let channels = supabase.realtime.channels
let deletions = await channel.postgresChange(
DeleteAction.self,
schema: "public",
table: "users",
filter: "id=eq.1"
)
await channel.subscribe()
for await deletion in deletions {
print("Deleted: \(deletion.oldRecord)")
}
```
- id: remove-channel
title: removeChannel()
description: |
Unsubscribes and removes Realtime channel from Realtime client.
title: "removeChannel()"
notes: |
- Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
- Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes.
- Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
- If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false.
examples:
- id: removes-a-channel
name: Removes a channel
name: Remove a channel
isSpotlight: true
code: |
```swift
supabase.realtime.remove(myChannel)
```
let channel = await supabase.channel("channelId")
//...
await supabase.removeChannel(channel)
```
- id: unsubscribe-channel
name: Unsubscribe from a channel
isSpotlight: true
code: |
```swift
let channel = await supabase.channel("channelId")
//...
await channel.unsubscribe()
```
- id: remove-all-channels
title: removeAllChannels()
$ref: "@supabase/supabase-js.index.SupabaseClient.removeAllChannels"
notes: |
Unsubscribes and removes all Realtime channels from Realtime client.
- Removing channels is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
- If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false.
examples:
- id: remove-all-channels
name: Remove all channels
isSpotlight: true
code: |
```swift
supabase.realtime.removeAllChannels()
await supabase.removeAllChannels()
```
- id: get-channels
title: getChannels()
$ref: "@supabase/supabase-js.index.SupabaseClient.getChannels"
notes: |
Returns all Realtime channels.
examples:
- id: get-all-channels
name: Get all channels
isSpotlight: true
code: |
```swift
let channels = await supabase.channels
```
- id: list-buckets
@@ -0,0 +1,27 @@
import Panel from 'components/ui/Panel'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_ } from 'ui'
import { CriticalIcon } from 'ui-patterns/Icons/StatusIcons'
import { DeleteAccountButton } from './DeleteAccountButton'
export const AccountDeletion = () => {
return (
<>
<Panel className="[&>div:first-child]:uppercase" title="Danger Zone">
<Panel.Content>
<Alert_Shadcn_ variant="destructive">
<CriticalIcon />
<AlertTitle_Shadcn_>Request for account deletion</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Deleting your account is permanent and cannot be undone. Your data will be deleted
within 30 days, except we may retain some metadata and logs for longer where required
or permitted by law.
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-3">
<DeleteAccountButton />
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</Panel.Content>
</Panel>
</>
)
}
@@ -0,0 +1,173 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import * as z from 'zod'
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProfile } from 'lib/profile'
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormLabel_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
} from 'ui'
export const DeleteAccountButton = () => {
const { profile } = useProfile()
const [isOpen, setIsOpen] = useState(false)
const { data: organizations, isSuccess } = useOrganizationsQuery()
const accountEmail = profile?.primary_email
const FormSchema = z.object({ account: z.string() })
const form = useForm<z.infer<typeof FormSchema>>({
mode: 'onBlur',
reValidateMode: 'onBlur',
resolver: zodResolver(FormSchema),
defaultValues: { account: '' },
})
const { account } = form.watch()
const { mutate: submitSupportTicket, isLoading } = useSendSupportTicketMutation({
onSuccess: () => {
setIsOpen(false)
toast.success(
'Successfully submitted account deletion request - we will reach out to you via email once the request is completed!',
{ duration: 8000 }
)
},
onError: (error) => {
toast.error(`Failed to submit account deletion request: ${error}`)
},
})
const onConfirmDelete = async () => {
if (!accountEmail) return console.error('Account information is required')
const payload = {
subject: 'Account Deletion Request',
message: 'I want to delete my account.',
category: SupportCategories.PROBLEM,
severity: 'Low',
allowSupportAccess: false,
verified: true,
projectRef: 'no-project',
}
submitSupportTicket(payload)
}
useEffect(() => {
if (isOpen && form !== undefined) form.reset({ account: '' })
}, [form, isOpen])
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button type="danger" loading={!accountEmail}>
Request to delete account
</Button>
</DialogTrigger>
<DialogContent className="!w-[500px]">
<DialogHeader className="pb-0">
{(organizations ?? []).length > 0 ? (
<>
<DialogTitle>Leave all organizations before requesting account deletion</DialogTitle>
<DialogDescription>
This will allow us to process your account deletion request faster
</DialogDescription>
</>
) : (
<>
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription>
Deleting your account is permanent and{' '}
<span className="text-foreground">cannot</span> be undone
</DialogDescription>
</>
)}
</DialogHeader>
{isSuccess && (
<>
{organizations.length > 0 ? (
<DialogSection className="!pt-0">
<span className="text-sm text-foreground flex flex-col gap-y-2">
Before submitting an account deletion request, please ensure that your account is
not part of any organization. This can be done by leaving leaving or deleting the
organizations that you are a part of.
</span>
<Button
block
type="primary"
size="medium"
className="mt-6"
onClick={() => setIsOpen(false)}
>
Understood
</Button>
</DialogSection>
) : (
<Form_Shadcn_ {...form}>
<form
id="account-deletion-request"
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(() => onConfirmDelete())}
>
<FormField_Shadcn_
name="account"
control={form.control}
render={({ field }) => (
<FormItem_Shadcn_ className="px-7">
<FormLabel_Shadcn_>
Please type{' '}
<span className="font-bold">{profile?.primary_email ?? ''}</span> to
confirm
</FormLabel_Shadcn_>
<FormControl_Shadcn_>
<Input_Shadcn_
autoFocus
{...field}
autoComplete="off"
disabled={isLoading}
placeholder="Enter the account above"
/>
</FormControl_Shadcn_>
</FormItem_Shadcn_>
)}
/>
<DialogSectionSeparator />
<div className="px-7 pb-4">
<Button
block
size="small"
type="danger"
htmlType="submit"
loading={isLoading}
disabled={account !== accountEmail || isLoading}
>
Submit request for account deletion
</Button>
</div>
</form>
</Form_Shadcn_>
)}
</>
)}
</DialogContent>
</Dialog>
)
}
@@ -3,11 +3,11 @@ import { Lock } from 'lucide-react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'
import { generateAlterPolicyQuery, generateCreatePolicyQuery } from './AIPolicyEditorPanel.utils'
interface LockedCreateQuerySection {
schema: string
selectedPolicy?: PostgresPolicy
formFields: { name: string; table: string; behavior: string; command: string; roles: string }
editorOneRef: any
@@ -15,6 +15,7 @@ interface LockedCreateQuerySection {
}
export const LockedCreateQuerySection = ({
schema,
selectedPolicy,
formFields,
editorOneRef,
@@ -22,7 +23,6 @@ export const LockedCreateQuerySection = ({
}: LockedCreateQuerySection) => {
const router = useRouter()
const { ref } = useParams()
const state = useTableEditorStateSnapshot()
const isEditing = selectedPolicy !== undefined
const { name, table, behavior, command, roles } = formFields
@@ -44,7 +44,7 @@ export const LockedCreateQuerySection = ({
const query = isEditing
? generateCreatePolicyQuery({
name,
schema: state.selectedSchemaName,
schema,
table,
behavior,
command,
@@ -58,7 +58,7 @@ export const LockedCreateQuerySection = ({
: generateAlterPolicyQuery({
name: '',
newName: name,
schema: state.selectedSchemaName,
schema,
table,
command,
roles: roles.length === 0 ? 'public' : roles,
@@ -84,7 +84,7 @@ export const LockedCreateQuerySection = ({
<div className="flex items-start" style={{ fontSize: '14px' }}>
<p className="px-6 font-mono text-sm text-foreground-light select-none">2</p>
<p className="font-mono tracking-tighter">
<span className="text-[#569cd6]">on</span> "{state.selectedSchemaName}"."
<span className="text-[#569cd6]">on</span> "{schema}"."
{table}"
</p>
</div>
@@ -124,16 +124,16 @@ export const LockedCreateQuerySection = ({
export const LockedRenameQuerySection = ({
oldName,
newName,
schema,
table,
lineNumber,
}: {
oldName: string
newName: string
schema: string
table: string
lineNumber: number
}) => {
const state = useTableEditorStateSnapshot()
return (
<div className="bg-surface-300 py-1">
<div className="flex items-center" style={{ fontSize: '14px' }}>
@@ -153,7 +153,7 @@ export const LockedRenameQuerySection = ({
</p>
</div>
<p className="font-mono tracking-tighter">
<span className="text-[#569cd6]">on</span> "{state.selectedSchemaName}"."{table}"
<span className="text-[#569cd6]">on</span> "{schema}"."{table}"
</p>
</div>
<div className="flex items-center" style={{ fontSize: '14px' }}>
@@ -1,10 +1,8 @@
import { useEffect } from 'react'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
FormControl_Shadcn_,
FormField_Shadcn_,
@@ -24,6 +22,7 @@ import {
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
interface PolicyDetailsV2Props {
schema: string
searchString?: string
isEditing: boolean
form: any
@@ -31,18 +30,18 @@ interface PolicyDetailsV2Props {
}
export const PolicyDetailsV2 = ({
schema,
searchString,
isEditing,
form,
onUpdateCommand,
}: PolicyDetailsV2Props) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { data: tables, isSuccess: isSuccessTables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: snap.selectedSchemaName,
schema: schema,
sortByProperty: 'name',
includeColumns: true,
})
@@ -66,7 +65,7 @@ export const PolicyDetailsV2 = ({
if (!isEditing) {
const table = tables?.find(
(table) =>
table.schema === snap.selectedSchemaName &&
table.schema === schema &&
(table.id.toString() === searchString || table.name === searchString)
)
if (table) {
@@ -117,7 +116,7 @@ export const PolicyDetailsV2 = ({
onValueChange={(value) => form.setValue('table', value)}
>
<SelectTrigger_Shadcn_ className="text-sm h-10">
{snap.selectedSchemaName}.{field.value}
{schema}.{field.value}
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
@@ -9,9 +9,9 @@ import CardButton from 'components/ui/CardButton'
import CopyButton from 'components/ui/CopyButton'
import NoSearchResults from 'components/ui/NoSearchResults'
import { getGeneralPolicyTemplates } from '../PolicyEditorModal/PolicyEditorModal.constants'
import { useTableEditorStateSnapshot } from 'state/table-editor'
interface PolicyTemplatesProps {
schema: string
table: string
selectedPolicy?: PostgresPolicy
selectedTemplate?: string
@@ -19,17 +19,14 @@ interface PolicyTemplatesProps {
}
export const PolicyTemplates = ({
schema,
table,
selectedPolicy,
selectedTemplate,
onSelectTemplate,
}: PolicyTemplatesProps) => {
const [search, setSearch] = useState('')
const state = useTableEditorStateSnapshot()
const templates = getGeneralPolicyTemplates(
state.selectedSchemaName,
table.length > 0 ? table : 'table_name'
)
const templates = getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name')
const baseTemplates =
selectedPolicy !== undefined
@@ -15,7 +15,6 @@ import {
Checkbox_Shadcn_,
Form_Shadcn_,
Label_Shadcn_,
Modal,
ScrollArea,
Sheet,
SheetContent,
@@ -45,7 +44,6 @@ import { useSelectedOrganization, useSelectedProject } from 'hooks'
import { BASE_PATH, OPT_IN_TAGS } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import Telemetry from 'lib/telemetry'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { AIPolicyChat } from './AIPolicyChat'
import {
@@ -70,6 +68,7 @@ const DiffEditor = dynamic(
interface AIPolicyEditorPanelProps {
visible: boolean
schema: string
searchString?: string
selectedPolicy?: PostgresPolicy
onSelectCancel: () => void
@@ -80,6 +79,7 @@ interface AIPolicyEditorPanelProps {
*/
export const AIPolicyEditorPanel = memo(function ({
visible,
schema,
searchString,
selectedPolicy,
onSelectCancel,
@@ -91,7 +91,6 @@ export const AIPolicyEditorPanel = memo(function ({
const selectedOrganization = useSelectedOrganization()
const telemetryProps = useTelemetryProps()
const state = useTableEditorStateSnapshot()
const isAiAssistantEnabled = useIsRLSAIAssistantEnabled()
// [Joshen] Hyrid form fields, just spit balling to get a decent POC out
@@ -330,7 +329,7 @@ export const AIPolicyEditorPanel = memo(function ({
if (selectedPolicy === undefined) {
const sql = generateCreatePolicyQuery({
name: name,
schema: state.selectedSchemaName,
schema,
table,
behavior,
command,
@@ -534,6 +533,7 @@ export const AIPolicyEditorPanel = memo(function ({
) : (
<>
<PolicyDetailsV2
schema={schema}
searchString={searchString}
isEditing={selectedPolicy !== undefined}
form={form}
@@ -544,6 +544,7 @@ export const AIPolicyEditorPanel = memo(function ({
/>
<div className="h-full">
<LockedCreateQuerySection
schema={schema}
selectedPolicy={selectedPolicy}
editorOneRef={editorOneRef}
editorTwoRef={editorTwoRef}
@@ -670,6 +671,7 @@ export const AIPolicyEditorPanel = memo(function ({
<LockedRenameQuerySection
oldName={selectedPolicy.name}
newName={name}
schema={schema}
table={table}
lineNumber={
8 + expOneLineCount + (showCheckBlock ? expTwoLineCount : 0)
@@ -789,6 +791,7 @@ export const AIPolicyEditorPanel = memo(function ({
>
<ScrollArea className="h-full w-full">
<PolicyTemplates
schema={schema}
table={table}
selectedPolicy={selectedPolicy}
selectedTemplate={selectedDiff}
@@ -14,10 +14,10 @@ import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import InformationBox from 'components/ui/InformationBox'
import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation'
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
interface PoliciesProps {
schema: string
tables: PostgresTable[]
hasTables: boolean
isLocked: boolean
@@ -25,6 +25,7 @@ interface PoliciesProps {
}
const Policies = ({
schema,
tables,
hasTables,
isLocked,
@@ -33,7 +34,6 @@ const Policies = ({
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const [selectedTableToToggleRLS, setSelectedTableToToggleRLS] = useState<any>({})
const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<any>({})
@@ -137,7 +137,7 @@ const Policies = ({
return (
<>
<div className="flex flex-col gap-y-4 pb-4">
{isLocked && <ProtectedSchemaWarning schema={snap.selectedSchemaName} entity="policies" />}
{isLocked && <ProtectedSchemaWarning schema={schema} entity="policies" />}
{tables.length > 0 ? (
tables.map((table: any) => (
<section key={table.id}>
@@ -43,7 +43,7 @@ const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps)
</div>
<div className="relative w-2/3">
{isLoading && <ShimmeringLoader className="py-4" />}
{isError && <AlertError error={error} subject="Failed to retrieve database roles" />}
{isError && <AlertError error={error as any} subject="Failed to retrieve database roles" />}
{isSuccess && (
<MultiSelect
options={formattedRoles}
@@ -48,10 +48,10 @@ const UserListItem = ({
<span className="text-foreground">{createdAt?.format('DD MMM, YYYY HH:mm')}</span>
</Table.td>
<Table.td className="table-cell">
{!isUserConfirmed ? (
<Badge variant="warning">Waiting for verification..</Badge>
) : user.last_sign_in_at ? (
{user?.last_sign_in_at ? (
lastSignedIn?.format('DD MMM, YYYY HH:mm')
) : !isUserConfirmed ? (
<Badge variant="warning">Waiting for verification..</Badge>
) : (
'Never'
)}
@@ -17,3 +17,16 @@ export const subscriptionHasHipaaAddon = (subscription?: OrgSubscription): boole
(addon) => addon.supabase_prod_id === 'addon_security_hipaa'
)
}
export const billingPartnerLabel = (billingPartner?: string) => {
if (!billingPartner) return billingPartner
switch (billingPartner) {
case 'fly':
return 'Fly.io'
case 'aws':
return 'AWS'
default:
return billingPartner
}
}
@@ -0,0 +1,43 @@
import React from 'react'
import { Button, Input, Modal } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
type Props = {}
const CreateWarehouseAccessToken = (props: Props) => {
const [open, setOpen] = React.useState(false)
return (
<>
<Button type="outline" onClick={() => setOpen(true)}>
Create access token
</Button>
<Modal
size="medium"
onCancel={() => {
setOpen(false)
}}
header="Create access token"
visible={open}
alignFooter="right"
onConfirm={() => {
setOpen(false)
}}
>
<Modal.Content>
<form>
<FormItemLayout
label="Description"
description="A short description for identifying what this access token is to be used for."
isReactForm={false}
>
<Input placeholder="Description" />
</FormItemLayout>
</form>
</Modal.Content>
</Modal>
</>
)
}
export default CreateWarehouseAccessToken
@@ -0,0 +1,14 @@
import React from 'react'
import CreateWarehouseAccessToken from './CreateWarehouseAccessToken'
type Props = {}
const WarehouseAccessTokens = (props: Props) => {
return (
<div>
<CreateWarehouseAccessToken />
</div>
)
}
export default WarehouseAccessTokens
@@ -26,22 +26,22 @@ interface CreateRolePanelProps {
const FormSchema = z.object({
name: z.string().trim().min(1, 'You must provide a name').default(''),
is_superuser: z.boolean().default(false),
can_login: z.boolean().default(false),
can_create_role: z.boolean().default(false),
can_create_db: z.boolean().default(false),
is_replication_role: z.boolean().default(false),
can_bypass_rls: z.boolean().default(false),
isSuperuser: z.boolean().default(false),
canLogin: z.boolean().default(false),
canCreateRole: z.boolean().default(false),
canCreateDb: z.boolean().default(false),
isReplicationRole: z.boolean().default(false),
canBypassRls: z.boolean().default(false),
})
const initialValues = {
name: '',
is_superuser: false,
can_login: false,
can_create_role: false,
can_create_db: false,
is_replication_role: false,
can_bypass_rls: false,
isSuperuser: false,
canLogin: false,
canCreateRole: false,
canCreateDb: false,
isReplicationRole: false,
canBypassRls: false,
}
const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
@@ -54,8 +54,8 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
})
const { mutate: createDatabaseRole, isLoading: isCreating } = useDatabaseRoleCreateMutation({
onSuccess: (res) => {
toast.success(`Successfully created new role: ${res.name}`)
onSuccess: (_, vars) => {
toast.success(`Successfully created new role: ${vars.payload.name}`)
handleClose()
},
})
@@ -27,7 +27,7 @@ const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => {
deleteDatabaseRole({
projectRef: project.ref,
connectionString: project.connectionString,
id: role.id.toString(),
id: role.id,
})
}
@@ -1,5 +1,4 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import type { PostgresRole } from '@supabase/postgres-meta'
import { useState } from 'react'
import toast from 'react-hot-toast'
import {
@@ -18,13 +17,14 @@ import {
} from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { PgRole } from 'data/database-roles/database-roles-query'
import { useDatabaseRoleUpdateMutation } from 'data/database-roles/database-role-update-mutation'
import { ROLE_PERMISSIONS } from './Roles.constants'
interface RoleRowProps {
role: PostgresRole
role: PgRole
disabled?: boolean
onSelectDelete: (role: PostgresRole) => void
onSelectDelete: (role: PgRole) => void
}
const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
@@ -33,25 +33,22 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
const { mutate: updateDatabaseRole, isLoading: isUpdating } = useDatabaseRoleUpdateMutation()
const {
is_superuser,
can_login,
can_create_role,
can_create_db,
is_replication_role,
can_bypass_rls,
} = role
const { isSuperuser, canLogin, canCreateRole, canCreateDb, isReplicationRole, canBypassRls } =
role
const onSaveChanges = async (values: any, { resetForm }: any) => {
const onSaveChanges = async (values: Partial<PgRole>, { resetForm }: any) => {
if (!project) return console.error('Project is required')
const { is_superuser, is_replication_role, ...payload } = values
const changed = Object.fromEntries(
Object.entries(values).filter(([k, v]) => v !== (role as any)[k])
)
updateDatabaseRole(
{
projectRef: project.ref,
connectionString: project.connectionString,
id: role.id,
payload,
payload: changed,
},
{
onSuccess: () => {
@@ -66,12 +63,12 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
<Form
name="role-update-form"
initialValues={{
is_superuser,
can_login,
can_create_role,
can_create_db,
is_replication_role,
can_bypass_rls,
isSuperuser,
canLogin,
canCreateRole,
canCreateDb,
isReplicationRole,
canBypassRls,
}}
onSubmit={onSaveChanges}
className={[
@@ -118,7 +115,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
</div>
</div>
<div className="flex items-center space-x-4">
{role.active_connections > 0 && (
{role.activeConnections > 0 && (
<div className="relative h-2 w-2">
<span className="flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-brand opacity-75"></span>
@@ -129,10 +126,10 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
<p
id="collapsible-trigger"
className={`text-sm ${
role.active_connections > 0 ? 'text-foreground' : 'text-foreground-light'
role.activeConnections > 0 ? 'text-foreground' : 'text-foreground-light'
}`}
>
{role.active_connections} connections
{role.activeConnections} connections
</p>
{!disabled && (
<DropdownMenu>
@@ -33,35 +33,35 @@ export const SYSTEM_ROLES = [
] as const
export const ROLE_PERMISSIONS = {
can_login: {
canLogin: {
disabled: false,
description: 'User can login',
grant_by_dashboard: true,
},
can_create_role: {
canCreateRole: {
disabled: false,
description: 'User can create roles',
grant_by_dashboard: true,
},
can_create_db: {
canCreateDb: {
disabled: false,
description: 'User can create databases',
grant_by_dashboard: true,
},
can_bypass_rls: {
disabled: true,
canBypassRls: {
disabled: false,
description: 'User bypasses every row level security policy',
grant_by_dashboard: false,
grant_by_dashboard: true,
},
is_superuser: {
isSuperuser: {
disabled: true,
description: 'User is a Superuser',
grant_by_dashboard: false,
},
is_replication_role: {
disabled: true,
isReplicationRole: {
disabled: false,
description:
'User can initiate streaming replication and put the system in and out of backup mode',
grant_by_dashboard: false,
grant_by_dashboard: true,
},
} as const
@@ -42,19 +42,19 @@ const RolesList = () => {
const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase())
const filteredRoles = (
filterType === 'active' ? roles.filter((role) => role.active_connections > 0) : roles
filterType === 'active' ? roles.filter((role) => role.activeConnections > 0) : roles
).filter((role) => role.name.includes(filterString))
const [supabaseRoles, otherRoles] = partition(filteredRoles, (role) =>
SUPABASE_ROLES.includes(role.name as SUPABASE_ROLE)
)
const totalActiveConnections = roles
.map((role) => role.active_connections)
.map((role) => role.activeConnections)
.reduce((a, b) => a + b, 0)
// order the roles with active connections by number of connections, most connections first
const rolesWithActiveConnections = sortBy(
roles.filter((role) => role.active_connections > 0),
(r) => -r.active_connections
roles.filter((role) => role.activeConnections > 0),
(r) => -r.activeConnections
)
return (
@@ -145,7 +145,7 @@ const RolesList = () => {
<p className="text-xs text-foreground-light pr-2">Connections by roles:</p>
{rolesWithActiveConnections.map((role) => (
<div key={role.id} className="text-xs text-foreground">
{role.name}: {role.active_connections}
{role.name}: {role.activeConnections}
</div>
))}
</div>
@@ -16,6 +16,7 @@ import {
FormSectionLabel,
FormsContainer,
} from 'components/ui/Forms'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { invalidateSchemasQuery } from 'data/database/schemas-query'
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
import { useCheckPermissions } from 'hooks'
@@ -62,6 +63,16 @@ const CreateWrapper = () => {
}
: {}
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const wrappersExtension = data?.find((ext) => ext.name === 'wrappers')
const hasRequiredVersion =
(wrappersExtension?.installed_version ?? '') >= (wrapperMeta?.minimumExtensionVersion ?? '')
if (wrapperMeta === undefined) {
return (
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
@@ -79,6 +90,37 @@ const CreateWrapper = () => {
)
}
if (!hasRequiredVersion) {
const databaseNeedsUpgrading =
wrappersExtension?.installed_version !== wrappersExtension?.default_version
return (
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
<div className="space-y-2 flex flex-col items-center w-[400px]">
<p>Your extension version is outdated for this wrapper.</p>
<p className="text-sm text-center text-foreground-light">
The wrapper type {type} requires a minimum extension version of{' '}
{wrapperMeta.minimumExtensionVersion}. You have version{' '}
{wrappersExtension?.installed_version} installed. Please{' '}
{databaseNeedsUpgrading && 'upgrade your database then '}reinstall the extension to
create this wrapper.
</p>
</div>
<Button asChild type="default">
<Link
href={
databaseNeedsUpgrading
? `/project/${ref}/settings/infrastructure`
: `/project/${ref}/database/extensions?filter=wrappers`
}
>
{databaseNeedsUpgrading ? 'Upgrade Database' : 'Reinstall Extension'}
</Link>
</Button>
</div>
)
}
const onUpdateTable = (values: any) => {
setNewTables((prev) => {
// if the new values have tableIndex, we are editing an existing table
@@ -9,6 +9,10 @@ export const WRAPPER_HANDLERS = {
BIG_QUERY: 'big_query_fdw_handler',
AIRTABLE: 'airtable_fdw_handler',
LOGFLARE: 'logflare_fdw_handler',
AUTH0: 'auth0_fdw_handler',
COGNITO: 'cognito_fdw_handler',
MSSQL: 'mssql_fdw_handler',
REDIS: 'redis_fdw_handler',
}
export const WRAPPERS: WrapperMeta[] = [
@@ -831,4 +835,335 @@ export const WRAPPERS: WrapperMeta[] = [
},
],
},
{
name: 'auth0_wrapper',
handlerName: WRAPPER_HANDLERS.AUTH0,
validatorName: 'auth0_fdw_validator',
icon: `${BASE_PATH}/img/icons/auth0-icon.svg`,
extensionName: 'Auth0Fdw',
label: 'Auth0',
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/auth0',
minimumExtensionVersion: '0.3.0',
server: {
options: [
{
name: 'api_key_id',
label: 'Auth0 API key or PAT',
required: true,
encrypted: true,
hidden: true,
},
{
name: 'url',
label: 'Auth0 API URL',
defaultValue: 'https://dev-<tenant-id>.us.auth0.com/api/v2/users',
required: false,
encrypted: false,
hidden: false,
},
],
},
tables: [
{
label: 'Users',
description: 'Auth0 Users',
availableColumns: [
{
name: 'user_id',
type: 'text',
},
{
name: 'email',
type: 'text',
},
{
name: 'email_verified',
type: 'boolean',
},
{
name: 'username',
type: 'text',
},
{
name: 'phone_number',
type: 'text',
},
{
name: 'phone_verified',
type: 'boolean',
},
{
name: 'created_at',
type: 'jsonb',
},
{
name: 'updated_at',
type: 'jsonb',
},
{
name: 'identities',
type: 'jsonb',
},
{
name: 'app_metadata',
type: 'jsonb',
},
{
name: 'user_metadata',
type: 'jsonb',
},
{
name: 'picture',
type: 'text',
},
{
name: 'name',
type: 'text',
},
{
name: 'nickname',
type: 'text',
},
{
name: 'multifactor',
type: 'jsonb',
},
{
name: 'last_ip',
type: 'text',
},
{
name: 'last_login',
type: 'jsonb',
},
{
name: 'logins_count',
type: 'integer',
},
{
name: 'blocked',
type: 'boolean',
},
{
name: 'given_name',
type: 'text',
},
{
name: 'family_name',
type: 'text',
},
],
options: [
{
name: 'object',
defaultValue: 'users',
editable: false,
required: true,
type: 'text',
},
],
},
],
},
{
name: 'cognito_wrapper',
handlerName: WRAPPER_HANDLERS.COGNITO,
validatorName: 'cognito_fdw_validator',
icon: `${BASE_PATH}/img/icons/cognito-icon.svg`,
extensionName: 'CognitoFdw',
label: 'Cognito',
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/cognito',
minimumExtensionVersion: '0.3.0',
server: {
options: [
{
name: 'aws_access_key_id',
label: 'AWS Access Key ID',
required: true,
encrypted: false,
hidden: false,
},
{
name: 'api_key_id',
label: 'AWS Secret Key',
required: true,
encrypted: true,
hidden: true,
},
{
name: 'region',
label: 'Region',
required: true,
encrypted: false,
hidden: false,
},
{
name: 'user_pool_id',
label: 'User Pool ID',
required: true,
encrypted: false,
hidden: false,
},
],
},
tables: [
{
label: 'Users',
description: 'Cognito Users',
availableColumns: [
{
name: 'email',
type: 'text',
},
{
name: 'created_at',
type: 'text',
},
{
name: 'email_verified',
type: 'boolean',
},
{
name: 'identities',
type: 'jsonb',
},
{
name: 'username',
type: 'text',
},
{
name: 'status',
type: 'text',
},
],
options: [
{
name: 'object',
defaultValue: 'users',
editable: false,
required: true,
type: 'text',
},
],
},
],
},
{
name: 'mssql_wrapper',
handlerName: WRAPPER_HANDLERS.MSSQL,
validatorName: 'mssql_fdw_validator',
icon: `${BASE_PATH}/img/icons/mssql-icon.svg`,
extensionName: 'mssqlFdw',
label: 'Microsoft SQL Server',
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/mssql',
minimumExtensionVersion: '0.3.0',
server: {
options: [
{
name: 'conn_string_id',
label: 'Connection String',
required: true,
encrypted: true,
hidden: true,
},
],
},
tables: [
{
label: 'Microsoft SQL Server Table',
description: 'Map to an Microsoft SQL Server Table',
options: [
{
name: 'table',
label: 'MSSQL Table',
editable: true,
required: true,
type: 'text',
},
],
},
],
},
{
name: 'redis_wrapper',
handlerName: WRAPPER_HANDLERS.REDIS,
validatorName: 'redis_fdw_validator',
icon: `${BASE_PATH}/img/icons/redis-icon.svg`,
extensionName: 'redisFdw',
label: 'Redis',
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/redis',
minimumExtensionVersion: '0.3.0',
server: {
options: [
{
name: 'conn_url_id',
label: 'Connection URL',
required: true,
encrypted: true,
hidden: true,
},
],
},
tables: [
{
label: 'Redis Table',
description: 'Map to an Redis Table',
options: [
{
name: 'src_type',
label: 'Source Type',
editable: true,
required: true,
type: 'select',
defaultValue: 'list',
options: [
{
label: 'list',
value: 'list',
},
{
label: 'set',
value: 'set',
},
{
label: 'hash',
value: 'hash',
},
{
label: 'zset',
value: 'zset',
},
{
label: 'stream',
value: 'stream',
},
{
label: 'multi_list',
value: 'multi_list',
},
{
label: 'multi_set',
value: 'multi_set',
},
{
label: 'multi_hash',
value: 'multi_hash',
},
{
label: 'multi_zset',
value: 'multi_zset',
},
],
},
{
name: 'src_key',
label: 'Source Key',
editable: true,
required: false,
type: 'text',
},
],
},
],
},
]
@@ -8,6 +8,7 @@ export type WrapperMeta = {
server: Server
tables: Table[]
docsUrl: string
minimumExtensionVersion?: string
}
export type ServerOption = {
@@ -67,7 +67,7 @@ const WrappersDisabledState = () => {
}
} finally {
setIsEnabling(false)
if (!hasError) toast.success('Wrappers is now enabled!')
if (!hasError) toast.success('Wrappers are now enabled!')
}
}
@@ -169,6 +169,20 @@ export const FRAMEWORKS: ConnectionType[] = [
},
],
},
{
key: 'flutter',
label: 'Flutter',
icon: 'flutter',
guideLink: 'https://supabase.com/docs/guides/getting-started/tutorials/with-flutter',
children: [
{
key: 'supabaseflutter',
label: 'supabase-flutter',
children: [],
icon: 'supabase',
},
],
},
]
export const ORMS: ConnectionType[] = [
@@ -0,0 +1,93 @@
import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types'
import {
ConnectTabs,
ConnectTabTriggers,
ConnectTabTrigger,
ConnectTabContent,
} from 'components/interfaces/Home/Connect/ConnectTabs'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
return (
<ConnectTabs>
<ConnectTabTriggers>
<ConnectTabTrigger value="lib/main.dart" />
<ConnectTabTrigger value="lib/main.dart (app)" />
</ConnectTabTriggers>
<ConnectTabContent value="lib/main.dart">
<SimpleCodeBlock className="dart" parentClassName="min-h-72">
{`
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
await Supabase.initialize(
url: ${projectKeys.apiUrl ?? 'your-project-url'},
anonKey: ${projectKeys.anonKey ?? 'your-anon-key'},
);
runApp(MyApp());
}
`}
</SimpleCodeBlock>
</ConnectTabContent>
<ConnectTabContent value="lib/main.dart (app)">
<SimpleCodeBlock className="dart" parentClassName="min-h-72">
{`
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Todos',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _future = Supabase.instance.client
.from('todos')
.select();
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: ((context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo['name']),
);
}),
);
},
),
);
}
}
`}
</SimpleCodeBlock>
</ConnectTabContent>
</ConnectTabs>
)
}
export default ContentFile
@@ -30,10 +30,10 @@ export const CLIENT_LIBRARIES = [
},
{
language: 'Swift',
officialSupport: false,
officialSupport: true,
releaseState: undefined,
docsUrl: 'https://supabase.com/docs/reference/swift/initializing',
gitUrl: 'https://github.com/supabase-community/supabase-swift',
gitUrl: 'https://github.com/supabase/supabase-swift',
},
{
language: 'Kotlin',
@@ -8,14 +8,25 @@ import CreditBalance from './CreditBalance'
import PaymentMethods from './PaymentMethods/PaymentMethods'
import Subscription from './Subscription/Subscription'
import TaxID from './TaxID/TaxID'
import { useParams } from 'common'
import { useOrgSubscriptionQuery } from '../../../../data/subscriptions/org-subscription-query'
const BillingSettings = () => {
const {
billingAccountData: billingAccountDataEnabled,
billingPaymentMethods: billingPaymentMethodsEnabled,
billingCredits: billingCreditsEnabled,
billingAccountData: isBillingAccountDataEnabledOnProfileLevel,
billingPaymentMethods: isBillingPaymentMethodsEnabledOnProfileLevel,
billingCredits: isBillingCreditsEnabledOnProfileLevel,
} = useIsFeatureEnabled(['billing:account_data', 'billing:payment_methods', 'billing:credits'])
const { slug: orgSlug } = useParams()
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug })
const isNotOrgWithPartnerBilling = !subscription?.billing_via_partner ?? true
const billingAccountDataEnabled =
isBillingAccountDataEnabledOnProfileLevel && isNotOrgWithPartnerBilling
const billingPaymentMethodsEnabled =
isBillingPaymentMethodsEnabledOnProfileLevel && isNotOrgWithPartnerBilling
return (
<>
<ScaffoldContainer id="subscription">
@@ -34,7 +45,7 @@ const BillingSettings = () => {
<BillingBreakdown />
</ScaffoldContainer>
{billingCreditsEnabled && (
{isBillingCreditsEnabledOnProfileLevel && (
<>
<ScaffoldDivider />
@@ -39,6 +39,7 @@ import ExitSurveyModal from './ExitSurveyModal'
import UpgradeSurveyModal from './UpgradeModal'
import MembersExceedLimitModal from './MembersExceedLimitModal'
import PaymentMethodSelection from './PaymentMethodSelection'
import { billingPartnerLabel } from 'components/interfaces/Billing/Subscription/Subscription.utils'
const PlanUpdateSidePanel = () => {
const router = useRouter()
@@ -545,8 +546,9 @@ const PlanUpdateSidePanel = () => {
) : (
<div className="py-4 space-y-2">
<p className="text-sm">
This organization is billed through our partner Fly.io and you will be charged by
them directly.
This organization is billed through our partner{' '}
{billingPartnerLabel(subscription?.billing_partner)} and you will be charged by them
directly.
</p>
{subscriptionPreview?.billed_via_partner &&
subscriptionPreview?.plan_change_type === 'downgrade' && (
@@ -3,23 +3,23 @@ import { ExternalLink } from 'lucide-react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useIndexAdvisorEnableMutation } from 'data/database/index-advisor-enable-mutation'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import { Markdown } from '../Markdown'
import Link from 'next/link'
import { useParams } from 'common'
export const IndexAdvisorDisabledState = () => {
const { ref } = useParams()
const { project } = useProjectContext()
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const hypopgExtension = (extensions ?? []).find((ext) => ext.name === 'hypopg')
const indexAdvisorExtension = (extensions ?? []).find((ext) => ext.name === 'index_advisor')
const { mutateAsync: enableExtension, isLoading: isEnablingExtension } =
useDatabaseExtensionEnableMutation()
const { mutateAsync: enableIndexAdvisor, isLoading: isEnablingIndexAdvisor } =
useIndexAdvisorEnableMutation()
const isEnabling = isEnablingExtension || isEnablingIndexAdvisor
const onEnableIndexAdvisor = async () => {
if (project === undefined) return console.error('Project is required')
@@ -32,33 +32,56 @@ export const IndexAdvisorDisabledState = () => {
version: hypopgExtension.default_version,
})
}
// [Joshen] Once index_advisor ext is ready, it'll just be enabling an extension
await enableIndexAdvisor({
projectRef: project.ref,
connectionString: project.connectionString,
})
if (indexAdvisorExtension?.installed_version === null) {
await enableExtension({
projectRef: project?.ref,
connectionString: project?.connectionString,
name: indexAdvisorExtension.name,
schema: indexAdvisorExtension?.schema ?? 'extensions',
version: indexAdvisorExtension.default_version,
})
}
}
return (
<Alert_Shadcn_>
<Alert_Shadcn_ className="mb-6">
<AlertTitle_Shadcn_>
Get index suggestions to improve your query performance
<Markdown
className="text-foreground"
content={
indexAdvisorExtension === undefined
? 'Newer version of Postgres required'
: 'Postgres extensions `index_advisor` and `hypopg` required'
}
/>
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<Markdown content="The `index_advisor` extension can help in recommending database indexes to reduce the costs of your query." />
<Markdown
content={
indexAdvisorExtension === undefined
? 'Upgrade to the latest version of Postgres to get recommendations on indexes for your queries'
: 'These extensions can help in recommending database indexes to reduce the costs of your query.'
}
/>
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-3">
<div className="flex items-center gap-x-2">
<Button
type="default"
disabled={isEnabling}
loading={isEnabling}
onClick={() => onEnableIndexAdvisor()}
>
Enable index advisor
</Button>
<Button asChild type="default" icon={<ExternalLink />}>
{indexAdvisorExtension === undefined ? (
<Button asChild type="default">
<Link href={`/project/${ref}/settings/infrastructure`}>Upgrade Postgres version</Link>
</Button>
) : (
<Button
type="default"
disabled={isEnablingExtension}
loading={isEnablingExtension}
onClick={() => onEnableIndexAdvisor()}
>
Enable extensions
</Button>
)}
<Button asChild type="outline" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
@@ -9,7 +9,6 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
import { useGetIndexAdvisorResult } from 'data/database/retrieve-index-advisor-result-query'
import { useGetIndexesFromSelectQuery } from 'data/database/retrieve-index-from-select-query'
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import {
AccordionContent_Shadcn_,
AccordionItem_Shadcn_,
@@ -51,21 +50,16 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
query: selectedRow?.['query'],
})
const { data: indexAdvisorExt, isLoading: isLoadingIndexAdvisorExt } = useExecuteSqlQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
sql: `select * from pg_proc where proname = 'index_advisor';`,
queryKey: ['index-advisor-function-check'],
})
const { data: extensions, isLoading: isLoadingExtensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isLoadingChecks = isLoadingIndexAdvisorExt || isLoadingExtensions
const hypopgExtension = (extensions ?? []).find((ext) => ext.name === 'hypopg')
const indexAdvisorExtension = (extensions ?? []).find((ext) => ext.name === 'index_advisor')
const isIndexAdvisorAvailable =
(indexAdvisorExt?.result ?? []).length > 0 &&
indexAdvisorExtension !== undefined &&
indexAdvisorExtension.installed_version !== null &&
hypopgExtension !== undefined &&
hypopgExtension.installed_version !== null
@@ -165,7 +159,7 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
<QueryPanelSection className="flex flex-col gap-y-6">
<div className="flex flex-col gap-y-2">
<p className="text-sm">New index recommendations</p>
{isLoadingChecks ? (
{isLoadingExtensions ? (
<GenericSkeletonLoader />
) : !isIndexAdvisorAvailable ? (
<IndexAdvisorDisabledState />
@@ -28,9 +28,8 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens
: undefined
const anonRoleKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
? apiService.serviceApiKey
? apiService.defaultApiKey
: undefined
const { data: postgrestConfig } = useProjectPostgrestConfigQuery(
{
projectRef: config.projectRef,
@@ -59,27 +58,30 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens
}, [snap.role])
useEffect(() => {
let token: string | undefined
let bearer: string | null = null
const triggerUpdateTokenBearer = async () => {
let token: string | undefined
let bearer: string | null = null
if (
config.projectRef !== undefined &&
jwtSecret !== undefined &&
snap.role !== undefined &&
snap.role.type === 'postgrest'
) {
token = anonRoleKey
getRoleImpersonationJWT(config.projectRef, jwtSecret, snap.role)
.then((token) => (bearer = token))
.catch((err) => toast.error(`Failed to get JWT for role: ${err.message}`))
} else {
token = serviceRoleKey
if (
config.projectRef !== undefined &&
jwtSecret !== undefined &&
snap.role !== undefined &&
snap.role.type === 'postgrest'
) {
token = anonRoleKey
await getRoleImpersonationJWT(config.projectRef, jwtSecret, snap.role)
.then((b) => (bearer = b))
.catch((err) => toast.error(`Failed to get JWT for role: ${err.message}`))
} else {
token = serviceRoleKey
}
if (token) {
onChangeConfig({ ...config, token, bearer })
}
}
if (token) {
onChangeConfig({ ...config, token, bearer })
}
}, [config.projectRef, jwtSecret, serviceRoleKey, snap.role])
triggerUpdateTokenBearer()
}, [snap.role, anonRoleKey, serviceRoleKey])
return <RoleImpersonationPopover align="start" variant="connected-on-both" />
}
@@ -32,7 +32,7 @@ export const RealtimeInspector = () => {
enableBroadcast: true,
})
const { logData, sendMessage } = useRealtimeMessages(realtimeConfig)
const { logData, sendMessage } = useRealtimeMessages(realtimeConfig, setRealtimeConfig)
return (
<div className="flex flex-col grow h-full">
@@ -1,6 +1,6 @@
import { RealtimeChannel, SupabaseClient } from '@supabase/supabase-js'
import { sortBy, take } from 'lodash'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'
import toast from 'react-hot-toast'
import { useProjectApiQuery } from 'data/config/project-api-query'
@@ -50,20 +50,25 @@ export interface RealtimeConfig {
enableDbChanges: boolean
}
export const useRealtimeMessages = ({
enabled,
channelName,
projectRef,
logLevel,
token,
schema,
table,
filter,
bearer,
enablePresence,
enableDbChanges,
enableBroadcast,
}: RealtimeConfig) => {
export const useRealtimeMessages = (
config: RealtimeConfig,
setRealtimeConfig: Dispatch<SetStateAction<RealtimeConfig>>
) => {
const {
enabled,
channelName,
projectRef,
logLevel,
token,
schema,
table,
filter,
bearer,
enablePresence,
enableDbChanges,
enableBroadcast,
} = config
const { data } = useProjectApiQuery({ projectRef: projectRef })
// the default host is prod until the correct one comes through an API call.
@@ -97,7 +102,8 @@ export const useRealtimeMessages = ({
},
}
const newClient = new SupabaseClient(host, token, opts)
if (bearer != '') {
if (bearer) {
newClient.realtime.setAuth(bearer)
}
@@ -170,6 +176,14 @@ export const useRealtimeMessages = ({
}
} else if (status === 'CLOSED') {
// console.log(`Realtime Channel status: ${status}`)
} else if (status === 'CHANNEL_ERROR') {
toast.error(
'Failed to connect to the channel: This may be due to restrictive RLS policies. Check your role and try again.'
)
newChannel.unsubscribe()
setChannel(undefined)
setRealtimeConfig({ ...config, enabled: false })
} else {
// console.error(`Realtime Channel error status: ${status}`)
// console.error(`Realtime Channel error: ${error}`)
@@ -32,6 +32,8 @@ export function getHumanReadableTitle(title: LINT_TYPES) {
return 'Security Definer View'
case 'duplicate_index':
return 'Duplicate Index'
case 'extension_in_public':
return 'Extension in Public'
default:
assertUnreachable(title)
throw new Error('This case should never be reached')
@@ -172,7 +174,18 @@ export const LintCTA = ({
case 'security_definer_view':
// we don't have a good place to send the user to check out a view
return <></>
case 'extension_in_public':
return (
<Button asChild type="default">
<Link
href={`/project/${projectRef}/database/extensions?filter=${metadata?.name}`}
target="_blank"
rel="noreferrer"
>
View extension
</Link>
</Button>
)
default:
assertUnreachable(title)
return <></>
@@ -162,7 +162,7 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
>
<Link
passHref
href={`/project/${projectRef}/auth/policies?search=${table.id}`}
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
>
Add RLS policy
</Link>
@@ -202,7 +202,10 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
)
}
>
<Link passHref href={`/project/${projectRef}/auth/policies?search=${table.id}`}>
<Link
passHref
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
>
Auth {policies.length > 1 ? 'policies' : 'policy'}
</Link>
</Button>
@@ -8,13 +8,17 @@ import { AccountLayout } from './'
import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle } from './Scaffold'
import SettingsLayout from './SettingsLayout/SettingsLayout'
import Link from 'next/link'
import { useOrgSubscriptionQuery } from '../../data/subscriptions/org-subscription-query'
const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
const selectedOrganization = useSelectedOrganization()
const router = useRouter()
const { slug } = useParams()
const invoicesEnabled = useIsFeatureEnabled('billing:invoices')
const invoicesEnabledOnProfileLevel = useIsFeatureEnabled('billing:invoices')
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug })
const isNotOrgWithPartnerBilling = !subscription?.billing_via_partner ?? true
const invoicesEnabled = invoicesEnabledOnProfileLevel && isNotOrgWithPartnerBilling
const navLayoutV2 = useFlag('navigationLayoutV2')
@@ -0,0 +1,343 @@
import { differenceInDays } from 'date-fns'
import React from 'react'
import toast from 'react-hot-toast'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Table from 'components/to-be-cleaned/Table'
import CopyButton from 'components/ui/CopyButton'
import { FormHeader } from 'components/ui/Forms'
import Panel from 'components/ui/Panel'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation'
import { useS3AccessKeyDeleteMutation } from 'data/storage/s3-access-key-delete-mutation'
import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query'
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconMoreVertical,
IconTrash,
cn,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
export const S3Connection = () => {
const [openCreateCred, setOpenCreateCred] = React.useState(false)
const [showSuccess, setShowSuccess] = React.useState(false)
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false)
const [deleteCredId, setDeleteCredId] = React.useState<string | null>(null)
const { ref: projectRef } = useParams()
const { project, isLoading: projectIsLoading } = useProjectContext()
const { data: storageCreds, ...storageCredsQuery } = useStorageCredentialsQuery({
projectRef,
})
const hasStorageCreds = storageCreds?.data && storageCreds.data.length > 0
const createS3AccessKey = useS3AccessKeyCreateMutation({
projectRef,
})
const deleteS3AccessKey = useS3AccessKeyDeleteMutation({
projectRef,
})
const { data: projectAPI } = useProjectApiQuery({ projectRef: projectRef })
function getConnectionURL() {
const projUrl = projectAPI
? `${projectAPI.autoApiService.protocol}://${projectAPI.autoApiService.endpoint}`
: `https://${projectRef}.supabase.co`
const url = new URL(projUrl)
url.pathname = '/storage/v1/s3'
return url.toString()
}
const s3connectionUrl = getConnectionURL()
return (
<>
<div>
<FormHeader
title="S3 Connection"
description="Connect to your bucket via the S3 protocol."
docsUrl="https://supabase.com/docs/guides/storage/s3/authentication"
/>
<Panel className="grid gap-4 p-4 !mb-0">
<FormItemLayout layout="horizontal" label="Endpoint" isReactForm={false}>
<Input readOnly copy disabled value={s3connectionUrl} />
</FormItemLayout>
{projectIsLoading ? (
<></>
) : (
<FormItemLayout layout="horizontal" label="Region" isReactForm={false}>
<Input className="input-mono" copy disabled value={project?.region} />
</FormItemLayout>
)}
</Panel>
</div>
<div>
<FormHeader
title="S3 Credentials"
description="Manage your S3 credentials for this project."
actions={
<Dialog
open={openCreateCred}
onOpenChange={(open) => {
setOpenCreateCred(open)
if (!open) setShowSuccess(false)
}}
>
<DialogTrigger asChild>
<Button type="outline">New credential</Button>
</DialogTrigger>
<DialogContent
className="p-4"
onInteractOutside={(e) => {
if (showSuccess) {
e.preventDefault()
}
}}
>
{showSuccess ? (
<>
<DialogTitle>Save your new S3 access keys</DialogTitle>
<DialogDescription>
You won't be able to see them again. If you lose these credentials, you'll
need to create a new ones.
</DialogDescription>
<FormItemLayout label="Access key id" isReactForm={false}>
<Input
className="input-mono"
readOnly
copy
disabled
value={createS3AccessKey.data?.data?.access_key}
/>
</FormItemLayout>
<FormItemLayout label={'Secret access key'} isReactForm={false}>
<Input
className="input-mono"
readOnly
copy
disabled
value={createS3AccessKey.data?.data?.secret_key}
/>
</FormItemLayout>
<div className="flex justify-end">
<Button
className="mt-4"
onClick={() => {
setOpenCreateCred(false)
setShowSuccess(false)
}}
>
Done
</Button>
</div>
</>
) : (
<>
<DialogTitle>Create new S3 access keys</DialogTitle>
<form
onSubmit={async (e) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const description = formData.get('description') as string
await createS3AccessKey.mutateAsync({ description })
setShowSuccess(true)
}}
>
<FormItemLayout label="Description" isReactForm={false}>
<Input
autoComplete="off"
placeholder="My test key"
type="text"
name="description"
required
/>
</FormItemLayout>
<div className="flex justify-end">
<Button
className="mt-4"
htmlType="submit"
loading={createS3AccessKey.isLoading}
>
Create credential
</Button>
</div>
</form>
</>
)}
</DialogContent>
</Dialog>
}
/>
<div
className={cn([
'bg-surface-100',
'overflow-hidden border-muted',
'rounded-md border shadow',
])}
>
{storageCredsQuery.isLoading ? (
<div className="p-4">
<GenericSkeletonLoader />
</div>
) : (
<div className="overflow-x-auto">
<Table
head={[
<Table.th key="">Description</Table.th>,
<Table.th key="">Access key id</Table.th>,
<Table.th key="">Created at</Table.th>,
<Table.th key="actions" />,
]}
body={
hasStorageCreds ? (
storageCreds.data?.map((cred: any) => (
<StorageCredItem
key={cred.id}
created_at={cred.created_at}
access_key={cred.access_key}
description={cred.description}
id={cred.id}
onDeleteClick={() => {
setDeleteCredId(cred.id)
setOpenDeleteDialog(true)
}}
/>
))
) : (
<Table.tr>
<Table.td colSpan={4}>
<p className="text-sm text-foreground">No credentials created</p>
<p className="text-sm text-foreground-light">
There are no credentials associated with your project yet
</p>
</Table.td>
</Table.tr>
)
}
/>
</div>
)}
</div>
</div>
<Dialog open={openDeleteDialog} onOpenChange={setOpenDeleteDialog}>
<DialogContent className="p-4">
<DialogTitle>Revoke S3 access keys</DialogTitle>
<DialogDescription>
This action is irreversible and requests made with these credentials will stop working.
</DialogDescription>
<div className="flex justify-end gap-2">
<Button
type="outline"
onClick={() => {
setOpenDeleteDialog(false)
setDeleteCredId(null)
}}
>
Cancel
</Button>
<Button
type="danger"
loading={deleteS3AccessKey.isLoading}
onClick={async () => {
if (!deleteCredId) return
await deleteS3AccessKey.mutateAsync({ id: deleteCredId })
setOpenDeleteDialog(false)
toast.success('S3 access keys revoked')
}}
>
Yes, revoke credentials
</Button>
</div>
</DialogContent>
</Dialog>
</>
)
}
function StorageCredItem({
description,
id,
created_at,
access_key,
onDeleteClick,
}: {
description: string
id: string
created_at: string
access_key: string
onDeleteClick: (id: string) => void
}) {
function daysSince(date: string) {
const now = new Date()
const created = new Date(date)
const diffInDays = differenceInDays(now, created)
if (diffInDays === 0) {
return 'Today'
} else if (diffInDays === 1) {
return `${diffInDays} day ago`
} else {
return `${diffInDays} days ago`
}
}
return (
<tr className="h-8 text-ellipsis group">
<td>{description}</td>
<td>
<div className="flex items-center justify-between">
<span className="text-ellipsis font-mono cursor-default">{access_key}</span>
<span className="w-24 text-right opacity-0 group-hover:opacity-100 transition-opacity">
<CopyButton text={access_key} type="outline" />
</span>
</div>
</td>
<td>{daysSince(created_at)}</td>
<td>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center justify-end w-full">
<IconMoreVertical />
</DropdownMenuTrigger>
<DropdownMenuContent className="max-w-40">
<DropdownMenuItem
className="flex gap-1.5 "
onClick={(e) => {
e.preventDefault()
onDeleteClick(id)
}}
>
<IconTrash />
Revoke credentials
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
)
}
+13 -7
View File
@@ -4,16 +4,19 @@ import ReactMarkdown from 'react-markdown'
import { Markdown } from 'components/interfaces/Markdown'
import { Button } from 'ui'
import { ReactNode } from 'react'
const FormHeader = ({
title,
description,
docsUrl,
actions,
className,
}: {
title: string
description?: string
docsUrl?: string
actions?: ReactNode
className?: string
}) => {
return (
@@ -26,13 +29,16 @@ const FormHeader = ({
</h3>
{description && <Markdown content={description} className="max-w-full" />}
</div>
{docsUrl !== undefined && (
<Button asChild type="default" icon={<ExternalLink size={14} />}>
<Link href={docsUrl} target="_blank" rel="noreferrer">
Documentation
</Link>
</Button>
)}
<div className="flex items-center gap-x-2">
{docsUrl !== undefined && (
<Button asChild type="default" icon={<ExternalLink size={14} />}>
<Link href={docsUrl} target="_blank" rel="noreferrer">
Documentation
</Link>
</Button>
)}
{actions}
</div>
</div>
)
}
+7 -2
View File
@@ -25,8 +25,13 @@ function Panel(props: PropsWithChildren<PanelProps>) {
)}
>
{props.title && (
<div className={cn('bg-surface-100 border-b border-muted', props.titleClasses)}>
<div className="flex items-center px-6 py-4">{props.title}</div>
<div
className={cn(
'bg-surface-100 border-b border-muted flex items-center px-6 py-4',
props.titleClasses
)}
>
{props.title}
</div>
)}
{props.children}
+3925 -10
View File
File diff suppressed because it is too large. Load diff
@@ -1,12 +1,12 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import pgMeta from '@supabase/pg-meta'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseRolesKeys } from './keys'
import type { components } from 'data/api'
import { executeSql } from 'data/sql/execute-sql-query'
import { invalidateRolesQuery } from './database-roles-query'
type CreateRoleBody = components['schemas']['CreateRoleBody']
type CreateRoleBody = Parameters<typeof pgMeta.roles.create>[0]
export type DatabaseRoleCreateVariables = {
projectRef: string
@@ -19,20 +19,14 @@ export async function createDatabaseRole({
connectionString,
payload,
}: DatabaseRoleCreateVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await post('/platform/pg-meta/{ref}/roles', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
},
body: payload,
headers,
const sql = pgMeta.roles.create(payload).sql
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['roles', 'create'],
})
if (error) throw error
return data
return result
}
type DatabaseRoleCreateData = Awaited<ReturnType<typeof createDatabaseRole>>
@@ -52,7 +46,7 @@ export const useDatabaseRoleCreateMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef))
await invalidateRolesQuery(queryClient, projectRef)
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,35 +1,34 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import pgMeta from '@supabase/pg-meta'
import { toast } from 'react-hot-toast'
import { del } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseRolesKeys } from './keys'
import { executeSql } from 'data/sql/execute-sql-query'
import { invalidateRolesQuery } from './database-roles-query'
type DropRoleBody = Parameters<typeof pgMeta.roles.remove>[1]
export type DatabaseRoleDeleteVariables = {
projectRef: string
connectionString?: string
id: string
id: number
payload?: DropRoleBody
}
export async function deleteDatabaseRole({
projectRef,
connectionString,
id,
payload,
}: DatabaseRoleDeleteVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await del('/platform/pg-meta/{ref}/roles', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
query: { id },
},
headers,
const sql = pgMeta.roles.remove({ id }, payload).sql
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['roles', 'delete'],
})
if (error) throw error
return data
return result
}
type DatabaseRoleDeleteData = Awaited<ReturnType<typeof deleteDatabaseRole>>
@@ -49,7 +48,7 @@ export const useDatabaseRoleDeleteMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef))
await invalidateRolesQuery(queryClient, projectRef)
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,12 +1,12 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import pgMeta from '@supabase/pg-meta'
import { toast } from 'react-hot-toast'
import { patch } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseRolesKeys } from './keys'
import type { components } from 'data/api'
import { executeSql } from 'data/sql/execute-sql-query'
import { invalidateRolesQuery } from './database-roles-query'
type UpdateRoleBody = components['schemas']['UpdateRoleBody']
type UpdateRoleBody = Parameters<typeof pgMeta.roles.update>[1]
export type DatabaseRoleUpdateVariables = {
projectRef: string
@@ -21,21 +21,14 @@ export async function updateDatabaseRole({
id,
payload,
}: DatabaseRoleUpdateVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await patch('/platform/pg-meta/{ref}/roles', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
query: { id },
},
body: payload,
headers,
const sql = pgMeta.roles.update({ id }, payload).sql
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['roles', 'update'],
})
if (error) throw error
return data
return result
}
type DatabaseRoleUpdateData = Awaited<ReturnType<typeof updateDatabaseRole>>
@@ -55,7 +48,7 @@ export const useDatabaseRoleUpdateMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef))
await invalidateRolesQuery(queryClient, projectRef)
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,49 +1,41 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
import type { PostgresRole } from '@supabase/postgres-meta'
import pgMeta from '@supabase/pg-meta'
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
import { z } from 'zod'
import { get } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseRolesKeys } from './keys'
import { ExecuteSqlData, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import { sqlKeys } from 'data/sql/keys'
export type DatabaseRolesVariables = {
projectRef?: string
connectionString?: string
}
export async function getDatabaseRoles(
{ projectRef, connectionString }: DatabaseRolesVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
export type PgRole = z.infer<typeof pgMeta.roles.zod>
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const pgMetaRolesList = pgMeta.roles.list()
const { data, error } = await get('/platform/pg-meta/{ref}/roles', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
},
headers,
signal,
})
if (error) throw error
return data as PostgresRole[]
}
export type DatabaseRolesData = Awaited<ReturnType<typeof getDatabaseRoles>>
export type DatabaseRolesError = ResponseError
export type DatabaseRolesData = z.infer<typeof pgMetaRolesList.zod>
export type DatabaseRolesError = unknown
export const useDatabaseRolesQuery = <TData = DatabaseRolesData>(
{ projectRef, connectionString }: DatabaseRolesVariables,
{ enabled = true, ...options }: UseQueryOptions<DatabaseRolesData, DatabaseRolesError, TData> = {}
options: UseQueryOptions<ExecuteSqlData, DatabaseRolesError, TData> = {}
) =>
useQuery<DatabaseRolesData, DatabaseRolesError, TData>(
databaseRolesKeys.list(projectRef),
({ signal }) => getDatabaseRoles({ projectRef, connectionString }, signal),
useExecuteSqlQuery(
{
enabled: enabled && typeof projectRef !== 'undefined',
projectRef,
connectionString,
sql: pgMetaRolesList.sql,
queryKey: ['roles', 'list'],
},
{
select(data) {
return data.result
},
...options,
}
)
export function invalidateRolesQuery(client: QueryClient, projectRef: string | undefined) {
return client.invalidateQueries(sqlKeys.query(projectRef, ['roles', 'list']))
}
-3
View File
@@ -1,3 +0,0 @@
export const databaseRolesKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'database-roles'] as const,
}
@@ -3,7 +3,7 @@ import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseKeys } from './keys'
import { invalidateSchemasQuery } from './schemas-query'
export type HooksEnableVariables = {
ref: string
@@ -34,7 +34,7 @@ export const useHooksEnableMutation = ({
async onSuccess(data, variables, context) {
const { ref } = variables
await onSuccess?.(data, variables, context)
await queryClient.invalidateQueries(databaseKeys.schemaList(ref))
await invalidateSchemasQuery(queryClient, ref)
},
async onError(data, variables, context) {
if (onError === undefined) {
@@ -1,250 +0,0 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError } from 'types'
import { sqlKeys } from 'data/sql/keys'
// [Joshen] This is just temporary while we wait for Index Advisor to be baked into the image itself
export type IndexAdvisorEnableVariables = {
projectRef: string
connectionString?: string
}
export async function enableIndexAdvisor({
projectRef,
connectionString,
}: IndexAdvisorEnableVariables) {
const sql = `
create or replace function index_advisor(
query text
)
returns table (
startup_cost_before jsonb,
startup_cost_after jsonb,
total_cost_before jsonb,
total_cost_after jsonb,
index_statements text[],
errors text[]
)
volatile
language plpgsql
as $$
declare
n_args int;
prepared_statement_name text = 'index_advisor_working_statement';
hypopg_schema_name text = (select extnamespace::regnamespace::text from pg_extension where extname = 'hypopg');
explain_plan_statement text;
error_message text;
rec record;
plan_initial jsonb;
plan_final jsonb;
statements text[] = '{}';
begin
-- Remove comment lines (its common that they contain semicolons)
query := trim(
regexp_replace(
regexp_replace(
regexp_replace(query,'\\/\\*.+\\*\\/', '', 'g'),
'--[^\\r\\n]*', ' ', 'g'),
'\\s+', ' ', 'g')
);
-- Remove trailing semicolon
query := regexp_replace(query, ';\\s*$', '');
begin
-- Disallow multiple statements
if query ilike '%;%' then
raise exception 'Query must not contain a semicolon';
end if;
-- Hack to support PostgREST because the prepared statement for args incorrectly defaults to text
query := replace(query, 'WITH pgrst_payload AS (SELECT $1 AS json_data)', 'WITH pgrst_payload AS (SELECT $1::json AS json_data)');
-- Create a prepared statement for the given query
deallocate all;
execute format('prepare %I as %s', prepared_statement_name, query);
-- Detect how many arguments are present in the prepared statement
n_args = (
select
coalesce(array_length(parameter_types, 1), 0)
from
pg_prepared_statements
where
name = prepared_statement_name
limit
1
);
-- Create a SQL statement that can be executed to collect the explain plan
explain_plan_statement = format(
'set local plan_cache_mode = force_generic_plan; explain (format json) execute %I%s',
--'explain (format json) execute %I%s',
prepared_statement_name,
case
when n_args = 0 then ''
else format(
'(%s)', array_to_string(array_fill('null'::text, array[n_args]), ',')
)
end
);
-- Store the query plan before any new indexes
execute explain_plan_statement into plan_initial;
-- Create possible indexes
for rec in (
with extension_regclass as (
select
distinct objid as oid
from
pg_catalog.pg_depend
where
deptype = 'e'
)
select
pc.relnamespace::regnamespace::text as schema_name,
pc.relname as table_name,
pa.attname as column_name,
format(
'select %I.hypopg_create_index($i$create index on %I.%I(%I)$i$)',
hypopg_schema_name,
pc.relnamespace::regnamespace::text,
pc.relname,
pa.attname
) hypopg_statement
from
pg_catalog.pg_class pc
join pg_catalog.pg_attribute pa
on pc.oid = pa.attrelid
left join extension_regclass er
on pc.oid = er.oid
left join pg_catalog.pg_index pi
on pc.oid = pi.indrelid
and (select array_agg(x) from unnest(pi.indkey) v(x)) = array[pa.attnum]
and pi.indexprs is null -- ignore expression indexes
and pi.indpred is null -- ignore partial indexes
where
pc.relnamespace::regnamespace::text not in ( -- ignore schema list
'pg_catalog', 'pg_toast', 'information_schema'
)
and er.oid is null -- ignore entities owned by extensions
and pc.relkind in ('r', 'm') -- regular tables, and materialized views
and pc.relpersistence = 'p' -- permanent tables (not unlogged or temporary)
and pa.attnum > 0
and not pa.attisdropped
and pi.indrelid is null
and pa.atttypid in (20,16,1082,1184,1114,701,23,21,700,1083,2950,1700,25,18,1042,1043)
)
loop
-- Create the hypothetical index
execute rec.hypopg_statement;
end loop;
/*
for rec in select * from hypopg()
loop
raise notice '%', rec;
end loop;
*/
-- Create a prepared statement for the given query
-- The original prepared statement MUST be dropped because its plan is cached
execute format('deallocate %I', prepared_statement_name);
execute format('prepare %I as %s', prepared_statement_name, query);
-- Store the query plan after new indexes
execute explain_plan_statement into plan_final;
--raise notice '%', plan_final;
-- Idenfity referenced indexes in new plan
execute format(
'select
coalesce(array_agg(hypopg_get_indexdef(indexrelid) order by indrelid, indkey::text), $i\${}$i$::text[])
from
%I.hypopg()
where
%s ilike ($i$%%$i$ || indexname || $i$%%$i$)
',
hypopg_schema_name,
quote_literal(plan_final)::text
) into statements;
-- Reset all hypothetical indexes
perform hypopg_reset();
-- Reset prepared statements
deallocate all;
return query values (
(plan_initial -> 0 -> 'Plan' -> 'Startup Cost'),
(plan_final -> 0 -> 'Plan' -> 'Startup Cost'),
(plan_initial -> 0 -> 'Plan' -> 'Total Cost'),
(plan_final -> 0 -> 'Plan' -> 'Total Cost'),
statements::text[],
array[]::text[]
);
return;
exception when others then
get stacked diagnostics error_message = MESSAGE_TEXT;
return query values (
null::jsonb,
null::jsonb,
null::jsonb,
null::jsonb,
array[]::text[],
array[error_message]::text[]
);
return;
end;
end;
$$;
`
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['enable-index-advisor'],
})
return result
}
type IndexAdvisorEnableData = Awaited<ReturnType<typeof enableIndexAdvisor>>
export const useIndexAdvisorEnableMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<IndexAdvisorEnableData, ResponseError, IndexAdvisorEnableVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<IndexAdvisorEnableData, ResponseError, IndexAdvisorEnableVariables>(
(vars) => enableIndexAdvisor(vars),
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
queryClient.invalidateQueries(sqlKeys.query(projectRef, ['index-advisor-function-check']))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to enable index advisor: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
-1
View File
@@ -1,5 +1,4 @@
export const databaseKeys = {
schemaList: (projectRef: string | undefined) => ['projects', projectRef, 'schemas'] as const,
backups: (projectRef: string | undefined) => [projectRef, 'database', 'backups'] as const,
poolingConfiguration: (projectRef: string | undefined) =>
[projectRef, 'database', 'pooling-configuration'] as const,
Loaded 100 of 329 files, more files were not shown because too many files have changed in this diff. Show more