@@ -66,8 +68,12 @@ export const examples = [
},
]
+{/* */}
+
## Integrations
+{/* */}
+
{integrations.map((x) => (
@@ -104,8 +110,12 @@ export const integrations = [
},
]
+{/* */}
+
## Case studies
+{/* */}
+
{[
{
@@ -134,3 +144,5 @@ export const integrations = [
))}
+
+{/* */}
diff --git a/apps/docs/content/guides/ai/hybrid-search.mdx b/apps/docs/content/guides/ai/hybrid-search.mdx
new file mode 100644
index 00000000000..9c5e13c3ec7
--- /dev/null
+++ b/apps/docs/content/guides/ai/hybrid-search.mdx
@@ -0,0 +1,228 @@
+---
+id: 'ai-hybrid-search'
+title: 'Hybrid search'
+description: 'Combine keyword search with semantic search to get both direct and contextual results.'
+subtitle: 'Combine keyword search with semantic search.'
+sidebar_label: 'Hybrid search'
+---
+
+Hybrid search combines [full text search](/docs/guides/database/full-text-search) (searching by keyword) with [semantic search](/docs/guides/ai/concepts#what-are-embeddings) (searching by meaning) to identify results that are both directly and contextually relevant to the user's query.
+
+## Why would I want to use hybrid search?
+
+Sometimes a single search method doesn't quite capture what a user is really looking for. For example, if a user searches for "Italian recipes with tomato sauce" on a cooking app, a keyword search would pull up recipes that specifically mention "Italian," "recipes," and "tomato sauce" in the text. However, it might miss out on dishes that are quintessentially Italian and use tomato sauce but don't explicitly label themselves with these words, or use variations like "pasta sauce" or "marinara." On the other hand, a semantic search might understand the culinary context and find recipes that match the intent, such as a traditional "Spaghetti Marinara," even if they don't match the exact keyword phrase. However, it could also suggest recipes that are contextually related but not what the user is looking for, like a "Mexican salsa" recipe, because it understands the context to be broadly about tomato-based sauces.
+
+Hybrid search combines the strengths of both these methods. It would ensure that recipes explicitly mentioning the keywords are prioritized, thus capturing direct hits that satisfy the keyword criteria. At the same time, it would include recipes identified through semantic understanding as being related in meaning or context, like different Italian dishes that traditionally use tomato sauce but might not have been tagged explicitly with the user's search terms. It identifies results that are both directly and contextually relevant to the user's query while ideally minimizing misses and irrelevant suggestions.
+
+## When would I want to use hybrid search?
+
+The decision to use hybrid search depends on what your users are looking for in your app. For a code repository where developers need to find exact lines of code or error messages, keyword search is likely ideal because it matches specific terms. In a mental health forum where users search for advice or experiences related to their feelings, semantic search may be better because it finds results based on the meaning of a query, not just specific words. For a shopping app where customers might search for specific product names yet also be open to related suggestions, hybrid search combines the best of both worlds - finding exact matches while also uncovering similar products based on the shopping context.
+
+## How to combine search methods
+
+Hybrid search merges keyword search and semantic search, but how does this process work?
+
+First, each search method is executed separately. Keyword search, which involves searching by specific words or phrases present in the content, will yield its own set of results. Similarly, semantic search, which involves understanding the context or meaning behind the search query rather than the specific words used, will generate its own unique results.
+
+Now with these separate result lists available, the next step is to combine them into a single, unified list. This is achieved through a process known as “fusion”. Fusion takes the results from both search methods and merges them together based on a certain ranking or scoring system. This system may prioritize certain results based on factors like their relevance to the search query, their ranking in the individual lists, or other criteria. The result is a final list that integrates the strengths of both keyword and semantic search methods.
+
+## Reciprocal Ranked Fusion (RRF)
+
+One of the most common fusion methods is Reciprocal Ranked Fusion (RRF). The key idea behind RRF is to give more weight to the top-ranked items in each individual result list when building the final combined list.
+
+In RRF, we iterate over each record and assign a score (noting that each record could exist in one or both lists). The score is calculated as 1 divided by that record's rank in each list, summed together between both lists. For example, if a record with an ID of `123` was ranked third in the keyword search and ninth in semantic search, it would receive a score of $$\dfrac{1}{3} + \dfrac{1}{9} = 0.444$$. If the record was found in only one list and not the other, it would receive a score of 0 for the other list. The records are then sorted by this score to create the final list. The items with the highest scores are ranked first, and lowest scores ranked last.
+
+This method ensures that items that are ranked high in multiple lists are given a high rank in the final list. It also ensures that items that are ranked high in only a few lists but low in others are not given a high rank in the final list. Placing the rank in the denominator when calculating score helps penalize the low ranking records.
+
+### Smoothing constant `k`
+
+To prevent extremely high scores for items that are ranked first (since we're dividing by the rank), a `k` constant is often added to the denominator to smooth the score:
+
+$$\dfrac{1}{k+rank}$$
+
+This constant can be any positive number, but is typically small. A constant of 1 would mean that a record ranked first would have a score of $$\dfrac{1}{1+1} = 0.5$$ instead of $$1$$. This adjustment can help balance the influence of items that are ranked very high in individual lists when creating the final combined list.
+
+## Hybrid search in Postgres
+
+Let's implement hybrid search in Postgres using `tsvector` (keyword search) and `pgvector` (semantic search).
+
+First we'll create a `documents` table to store the documents that we will search over. This is just an example - adjust this to match the structure of your application.
+
+```sql
+create table documents (
+ id bigint primary key generated always as identity,
+ content text,
+ fts tsvector generated always as (to_tsvector('english', content)) stored,
+ embedding vector(512)
+);
+```
+
+The table contains 4 columns:
+
+- `id` is an auto-generated unique ID for the record. We'll use this later to match records when performing RRF.
+- `content` contains the actual text we will be searching over.
+- `fts` is an auto-generated `tsvector` column that is generated using the text in `content`. We will use this for [full text search](/docs/guides/database/full-text-search) (search by keyword).
+- `embedding` is a [vector column](/docs/guides/ai/vector-columns) that stores the vector generated from our embedding model. We will use this for [semantic search](/docs/guides/ai/concepts#what-are-embeddings) (search by meaning). We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model.
+
+Next we'll create indexes on the `fts` and `embedding` columns so that their individual queries will remain fast at scale:
+
+```sql
+-- Create an index for the full-text search
+create index on documents using gin(fts);
+
+-- Create an index for the semantic vector search
+create index on documents using hnsw (embedding vector_ip_ops);
+```
+
+For full text search we use a [generalized inverted (GIN) index](https://www.postgresql.org/docs/current/gin-intro.html) which is designed for handling composite values like those stored in a `tsvector`.
+
+For semantic vector search we use an [HNSW index](/docs/guides/ai/vector-indexes/hnsw-indexes), which is a high performing approximate nearest neighbor (ANN) search algorithm. Note that we are using the `vector_ip_ops` (inner product) operator with this index because we plan on using the inner product (`<#>`) operator later in our query. If you plan to use a different operator like cosine distance (`<=>`), be sure to update the index accordingly. For more information, see [distance operators](/docs/guides/ai/vector-indexes#distance-operators).
+
+Finally we'll create our `hybrid_search` function:
+
+```sql
+create or replace function hybrid_search(
+ query_text text,
+ query_embedding vector(512),
+ match_count int,
+ full_text_weight float = 1,
+ semantic_weight float = 1,
+ rrf_k int = 50
+)
+returns setof documents
+language sql
+as $$
+with full_text as (
+ select
+ id,
+ -- Note: ts_rank_cd is not indexable but will only rank matches of the where clause
+ -- which shouldn't be too big
+ row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
+ from
+ documents
+ where
+ fts @@ websearch_to_tsquery(query_text)
+ order by rank_ix
+ limit least(match_count, 30) * 2
+),
+semantic as (
+ select
+ id,
+ row_number() over (order by doc_vector <#> query_embedding) as rank_ix
+ from
+ documents
+ order by rank_ix
+ limit least(match_count, 30) * 2
+)
+select
+ documents.*
+from
+ full_text
+ full outer join semantic
+ on full_text.id = semantic.id
+ join documents
+ on coalesce(full_text.id, semantic.id) = documents.id
+order by
+ coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
+ coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
+ desc
+limit
+ least(match_count, 30)
+$$;
+```
+
+Let's break this down:
+
+- **Parameters:** The function accepts quite a few parameters, but the main (required) ones are `query_text`, `query_embedding`, and `match_count`.
+
+ - `query_text` is the user's query text (more on this shortly)
+ - `query_embedding` is the vector representation of the user's query produced by the embedding model. We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model. This must match the size of the `embedding` vector on the `documents` table (and use the same model).
+ - `match_count` is the number of records returned in the `limit` clause.
+
+ The other parameters are optional, but give more control over the fusion process.
+
+ - `full_text_weight` and `semantic_weight` decide how much weight each search method gets in the final score. These are both 1 by default which means they both equally contribute towards the final rank. A `full_text_weight` of 2 and `semantic_weight` of 1 would give full-text search twice as much weight as semantic search.
+ - `rrf_k` is the `k` [smoothing constant](#smoothing-constant-k) added to the reciprocal rank. The default is 50.
+
+- **Return type:** The function returns a set of records from our `documents` table.
+- **CTE:** We create two [common table expressions (CTE)](https://www.postgresql.org/docs/current/queries-with.html), one for full-text search and one for semantic search. These perform each query individually prior to joining them.
+- **RRF:** The final query combines the results from the two CTEs using [reciprocal rank fusion (RRF)](#reciprocal-ranked-fusion-rrf).
+
+## Running hybrid search
+
+To use this function in SQL, we can run:
+
+```sql
+select
+ *
+from
+ hybrid_search(
+ 'Italian recipes with tomato sauce', -- user query
+ '[...]'::vector(512), -- embedding generated from user query
+ 10
+ );
+```
+
+In practice, you will likely be calling this from the [Supabase client](/docs/reference/javascript/introduction) or through a custom backend layer. Here is a quick example of how you might call this from an [Edge Function](/docs/guides/functions) using JavaScript:
+
+```tsx
+import { createClient } from 'npm:@supabase/supabase-js'
+import OpenAI from 'npm:openai'
+
+const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+const supabaseServiceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+const openaiApiKey = Deno.env.get('OPENAI_API_KEY')!
+
+Deno.serve(async (req) => {
+ // Grab the user's query from the JSON payload
+ const { query } = await req.json()
+
+ // Instantiate OpenAI client
+ const openai = new OpenAI({ apiKey: openaiApiKey })
+
+ // Generate a one-time embedding for the user's query
+ const embeddingResponse = await openai.embeddings.create({
+ model: 'text-embedding-3-large',
+ input: query,
+ dimensions: 512,
+ })
+
+ const [{ embedding }] = embeddingResponse.data
+
+ // Instantiate the Supabase client
+ // (replace service role key with user's JWT if using Supabase auth and RLS)
+ const supabase = createClient(supabaseUrl, supabaseServiceRoleKey)
+
+ // Call hybrid_search Postgres function via RPC
+ const { data: documents } = await supabase.rpc('hybrid_search', {
+ query_text: query,
+ query_embedding: embedding,
+ match_count: 10,
+ })
+
+ return new Response(JSON.stringify(documents), {
+ headers: { 'Content-Type': 'application/json' },
+ })
+})
+```
+
+This uses OpenAI's `text-embedding-3-large` model to generate embeddings (shortened to 512 dimensions for faster retrieval). Swap in your preferred embedding model (and dimension size) accordingly.
+
+To test this, make a `POST` request to the function's endpoint while passing in a JSON payload containing the user's query. Here is an example `POST` request using cURL:
+
+```tsx
+curl -i --location --request POST \
+ 'http://127.0.0.1:54321/functions/v1/hybrid-search' \
+ --header 'Authorization: Bearer
' \
+ --header 'Content-Type: application/json' \
+ --data '{"query":"Italian recipes with tomato sauce"}'
+```
+
+For more information on how to create, test, and deploy edge functions, see [Getting started](/docs/guides/functions/quickstart).
+
+## See also
+
+- [Embedding concepts](/docs/guides/ai/concepts)
+- [Vector columns](/docs/guides/ai/vector-columns)
+- [Vector indexes](/docs/guides/ai/vector-indexes)
+- [Full text (keyword) search](/docs/guides/database/full-text-search)
diff --git a/apps/docs/content/guides/api/api-keys.mdx b/apps/docs/content/guides/api/api-keys.mdx
index b310766f98a..e0faa402039 100644
--- a/apps/docs/content/guides/api/api-keys.mdx
+++ b/apps/docs/content/guides/api/api-keys.mdx
@@ -20,10 +20,10 @@ The keys are both long-lived JWTs. If you decode these keys, you will see that t
## The `anon` key
-The `anon` key has very few privileges. You can use it in your [RLS policies](/docs/guides/auth/row-level-security) for "anonymous" access. For example, this policy will allow access to the `profiles` table:
+The `anon` key has very few privileges. You can use it in your [RLS policies](/docs/guides/auth/row-level-security) to allow unauthenticated access. For example, this policy will allow unauthenticated access to the `profiles` table:
```sql
-create policy "Allow anonymous access" on profiles to anon for
+create policy "Allow public access" on profiles to anon for
select
using (true);
```
@@ -31,7 +31,7 @@ select
And similarity for disallowing access:
```sql
-create policy "Disallow anonymous access" on profiles to anon for
+create policy "Disallow public access" on profiles to anon for
select
using (false);
```
diff --git a/apps/docs/content/guides/api/quickstart.mdx b/apps/docs/content/guides/api/quickstart.mdx
index 1ab1f6640e0..fbd141e9309 100644
--- a/apps/docs/content/guides/api/quickstart.mdx
+++ b/apps/docs/content/guides/api/quickstart.mdx
@@ -59,9 +59,9 @@ We'll create a database table called `todos` for storing tasks. This creates a c
-
+
- Let's turn on Row Level Security for this table and allow anonymous access.
+ Let's turn on Row Level Security for this table and allow public access.
@@ -73,7 +73,7 @@ We'll create a database table called `todos` for storing tasks. This creates a c
enable row level security;
-- Allow anonymous access
- create policy "Allow anonymous access"
+ create policy "Allow public access"
on todos
for select
to anon
diff --git a/apps/docs/content/guides/api/rest/generating-types.mdx b/apps/docs/content/guides/api/rest/generating-types.mdx
index 624749a1d17..45d9c77ce18 100644
--- a/apps/docs/content/guides/api/rest/generating-types.mdx
+++ b/apps/docs/content/guides/api/rest/generating-types.mdx
@@ -125,6 +125,12 @@ export type Database = MergeDeep<
>
```
+
+
+To use `MergeDeep`, set `compilerOptions.strictNullChecks` to `true` in your `tsconfig.json`.
+
+
+
You can also override the type of an individual successful response if needed:
```ts
diff --git a/apps/docs/content/guides/auth/auth-captcha.mdx b/apps/docs/content/guides/auth/auth-captcha.mdx
index 956c962f32a..fb012825136 100644
--- a/apps/docs/content/guides/auth/auth-captcha.mdx
+++ b/apps/docs/content/guides/auth/auth-captcha.mdx
@@ -26,9 +26,9 @@ Supabase provides you with the option of adding captcha to your sign-in, sign-up
queryGroup="captcha-method"
>
-Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the welcome page, copy the **Sitekey** and **Secret key**.
+Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the Welcome page, copy the **Sitekey** and **Secret key**.
-If you have already signed up and didn't copy this information from the welcome page, you can get the **Secret key** from the settings page.
+If you have already signed up and didn't copy this information from the Welcome page, you can get the **Secret key** from the Settings page.

@@ -43,7 +43,7 @@ In the Settings page, look for the **Sitekey** section and copy the key.
- Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below
+ Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the Welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below

diff --git a/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx b/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx
index 0fc93deb684..22c96b16c2b 100644
--- a/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx
+++ b/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx
@@ -220,7 +220,7 @@ const App = () => (
)
```
-If you created your own theme, you may not need to override any of the them.
+If you created your own theme, you may not need to override any of them.
### Create your own theme [#create-theme]
@@ -470,3 +470,14 @@ Setting `showLinks` to `false` will hide the following links:
- Already have an account? Sign in
- Send a magic link email
- Forgot your password?
+
+### Sign in and Sign up views
+
+Add `sign_in` or `sign_up` views with the `view` prop:
+
+```
+
+```
diff --git a/apps/docs/content/guides/auth/auth-helpers/remix.mdx b/apps/docs/content/guides/auth/auth-helpers/remix.mdx
index 4a3a4dc7307..4871094a551 100644
--- a/apps/docs/content/guides/auth/auth-helpers/remix.mdx
+++ b/apps/docs/content/guides/auth/auth-helpers/remix.mdx
@@ -93,9 +93,9 @@ import { redirect } from '@remix-run/node'
import { createServerClient } from '@supabase/auth-helpers-remix'
import type { Database } from 'db_types'
-import type { LoaderArgs } from '@remix-run/node'
+import type { LoaderFunctionArgs } from '@remix-run/node'
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
const response = new Response()
const url = new URL(request.url)
const code = url.searchParams.get('code')
@@ -174,9 +174,9 @@ export const loader = async ({ request }) => {
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'
-import type { LoaderArgs } from '@remix-run/node' // change this import to whatever runtime you are using
+import type { LoaderFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
const response = new Response()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL!,
@@ -246,9 +246,9 @@ export const action = async ({ request }) => {
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'
-import type { ActionArgs } from '@remix-run/node' // change this import to whatever runtime you are using
+import type { ActionFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using
-export const action = async ({ request }: ActionArgs) => {
+export const action = async ({ request }: ActionFunctionArgs) => {
const response = new Response()
const supabaseClient = createServerClient(
@@ -351,7 +351,7 @@ And then we can share this instance across our application with Outlet Context.
```tsx app/root.tsx
-export const loader = ({}: LoaderArgs) => {
+export const loader = ({}: LoaderFunctionArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
@@ -441,7 +441,7 @@ export const loader = async ({ request }) => {
```tsx app/root.tsx
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
@@ -707,9 +707,9 @@ import type { Database } from 'db_types'
type Post = Database['public']['Tables']['posts']['Row']
-import type { LoaderArgs } from '@remix-run/node'
+import type { LoaderFunctionArgs } from '@remix-run/node'
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
const response = new Response()
const supabase = createServerClient(
process.env.SUPABASE_URL!,
diff --git a/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx
index 97f9641eb10..37bf88cee01 100644
--- a/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx
+++ b/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx
@@ -155,7 +155,7 @@ export const GET = async ({ url, locals: { supabase } }) => {
await supabase.auth.exchangeCodeForSession(code)
}
- throw redirect(303, '/')
+ redirect(303, '/')
}
```
@@ -175,7 +175,7 @@ export const GET = async ({ url, locals: { supabase } }) => {
await supabase.auth.exchangeCodeForSession(code)
}
- throw redirect(303, '/')
+ redirect(303, '/')
}
```
@@ -639,7 +639,7 @@ async function authorization({ event, resolve }) {
const session = await event.locals.getSession()
if (!session) {
// the user is not signed in
- throw redirect(303, '/')
+ redirect(303, '/')
}
}
@@ -701,7 +701,7 @@ async function authorization({ event, resolve }) {
const session = await event.locals.getSession()
if (!session) {
// the user is not signed in
- throw redirect(303, '/')
+ redirect(303, '/')
}
}
@@ -773,7 +773,7 @@ import { redirect } from '@sveltejs/kit'
export const load = async ({ parent }) => {
const { supabase, session } = await parent()
if (!session) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data: tableData } = await supabase.from('test').select('*')
@@ -819,12 +819,12 @@ export const actions = {
})
}
- throw redirect(303, '/dashboard')
+ redirect(303, '/dashboard')
},
signout: async ({ locals: { supabase } }) => {
await supabase.auth.signOut()
- throw redirect(303, '/')
+ redirect(303, '/')
},
}
```
@@ -1109,7 +1109,7 @@ import { redirect } from '@sveltejs/kit'
export const load: PageLoad = async (event) => {
const { session, supabaseClient } = await getSupabase(event)
if (!session) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data: tableData } = await supabaseClient.from('test').select('*')
@@ -1145,7 +1145,7 @@ import { redirect } from '@sveltejs/kit'
export const load: PageLoad = async ({ parent }) => {
const { supabase, session } = await parent()
if (!session) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data: tableData } = await supabase.from('test').select('*')
@@ -1179,7 +1179,7 @@ import { json, redirect } from '@sveltejs/kit'
export const GET: RequestHandler = async (event) => {
const { session, supabaseClient } = await getSupabase(event)
if (!session) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data } = await supabaseClient.from('test').select('*')
@@ -1457,7 +1457,7 @@ import type { PageLoad } from './$types'
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data: tableData } = await getSupabaseClient().from('test').select('*')
@@ -1489,7 +1489,7 @@ import { redirect } from '@sveltejs/kit'
export const load: PageLoad = async (event) => {
const { session, supabaseClient } = await getSupabase(event)
if (!session) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data: tableData } = await supabaseClient.from('test').select('*')
@@ -1526,7 +1526,7 @@ interface TestTable {
export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data } = await getSupabaseClient().from('test').select('*')
@@ -1546,7 +1546,7 @@ import { json, redirect } from '@sveltejs/kit'
export const GET: RequestHandler = async (event) => {
const { session, supabaseClient } = await getSupabase(event)
if (!session) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data } = await supabaseClient.from('test').select('*')
@@ -1886,7 +1886,7 @@ import type { PageLoad } from './$types'
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
- throw redirect(303, '/')
+ redirect(303, '/')
}
const { data: tableData } = await getSupabaseClient().from('test').select('*')
@@ -1948,7 +1948,7 @@ interface TestTable {
export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
- throw redirect(303, '/');
+ redirect(303, '/');
}
const { data } = await getSupabaseClient()
diff --git a/apps/docs/content/guides/auth/auth-hooks.mdx b/apps/docs/content/guides/auth/auth-hooks.mdx
index f094433bcd6..c6a833b30fc 100644
--- a/apps/docs/content/guides/auth/auth-hooks.mdx
+++ b/apps/docs/content/guides/auth/auth-hooks.mdx
@@ -572,7 +572,7 @@ as $$
is_admin boolean;
begin
-- Check if the user is marked as admin in the profiles table
- select is_admin into is_admin from profiles where user_id = event->>'user_id'::uuid;
+ select is_admin into is_admin from profiles where user_id = (event->>'user_id')::uuid;
-- Proceed only if the user is an admin
if is_admin then
diff --git a/apps/docs/content/guides/auth/concepts/redirect-urls.mdx b/apps/docs/content/guides/auth/concepts/redirect-urls.mdx
index 2fcd240acd8..3f9f3aab8ad 100644
--- a/apps/docs/content/guides/auth/concepts/redirect-urls.mdx
+++ b/apps/docs/content/guides/auth/concepts/redirect-urls.mdx
@@ -7,7 +7,7 @@ subtitle: 'Set up redirect urls with Supabase Auth.'
## Overview
-When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library methods provide a `redirectTo` parameter to specify where to redirect the user to after authentication. By default, the user will be redirected to the [`SITE_URL`](/docs/reference/auth/config#site_url) but you can modify the `SITE_URL` or add additional redirect URLs to the [allow list](https://supabase.com/dashboard/project/_/auth/url-configuration). Once you've added necessary URLs to the allow list, you can specify the URL you want the user to be redirected to in the `redirectTo` parameter.
+When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library methods provide a `redirectTo` parameter to specify where to redirect the user to after authentication. By default, the user will be redirected to the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls) but you can modify the `SITE_URL` or add additional redirect URLs to the [allow list](https://supabase.com/dashboard/project/_/auth/url-configuration). Once you've added necessary URLs to the allow list, you can specify the URL you want the user to be redirected to in the `redirectTo` parameter.
## Use wildcards in redirect URLs
@@ -81,3 +81,16 @@ const { data, error } = await supabase.auth.signInWithOAuth({
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.
Read more about deep linking and find code examples for different frameworks [here](/docs/guides/auth/native-mobile-deep-linking).
+
+## Error Handling
+
+When authentication fails, the user will still be redirected to the redirect URL provided. However, the error details will be returned as query fragments in the URL. You can parse these query fragments and show a custom error message to the user. For example:
+
+```js
+const params = new URLSearchParams(window.location.hash.slice())
+
+if (params.get('error_code').startsWith('4')) {
+ // show error message if error is a 4xx error
+ window.alert(params.get('error_description'))
+}
+```
diff --git a/apps/docs/content/guides/auth/custom-claims-and-role-based-access-control-rbac.mdx b/apps/docs/content/guides/auth/custom-claims-and-role-based-access-control-rbac.mdx
new file mode 100644
index 00000000000..9697e315275
--- /dev/null
+++ b/apps/docs/content/guides/auth/custom-claims-and-role-based-access-control-rbac.mdx
@@ -0,0 +1,278 @@
+---
+id: 'custom-claims-and-role-based-access-control-rbac'
+title: 'Custom Claims & Role-based Access Control (RBAC)'
+description: 'Use Auth Hooks to add custom claims for managing role-based access control.'
+---
+
+Custom Claims are special attributes attached to a user that you can use to control access to portions of your application. For example:
+
+```json
+{
+ "user_role": "admin",
+ "plan": "TRIAL",
+ "user_level": 100,
+ "group_name": "Super Guild!",
+ "joined_on": "2022-05-20T14:28:18.217Z",
+ "group_manager": false,
+ "items": ["toothpick", "string", "ring"]
+}
+```
+
+To implement Role-Based Access Control (RBAC) with `custom claims`, use a [Custom Access Token Auth Hook](/docs/guides/auth/auth-hooks#hook-custom-access-token). This hook runs before a token is issued. You can use it to add additional claims to the user's JWT.
+
+This guide uses the [Slack Clone example](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone) to demonstrate how to add a `user_role` claim and use it in your [Row Level Security (RLS) policies](/docs/guides/auth/row-level-security).
+
+## Create a table to track user roles and permissions
+
+In this example, you will implement two user roles with specific permissions:
+
+- `moderator`: A moderator can delete all messages but not channels.
+- `admin`: An admin can delete all messages and channels.
+
+```sql supabase/migrations/init.sql
+-- Custom types
+create type public.app_permission as enum ('channels.delete', 'messages.delete');
+create type public.app_role as enum ('admin', 'moderator');
+
+-- USER ROLES
+create table public.user_roles (
+ id bigint generated by default as identity primary key,
+ user_id uuid references public.users on delete cascade not null,
+ role app_role not null,
+ unique (user_id, role)
+);
+comment on table public.user_roles is 'Application roles for each user.';
+
+-- ROLE PERMISSIONS
+create table public.role_permissions (
+ id bigint generated by default as identity primary key,
+ role app_role not null,
+ permission app_permission not null,
+ unique (role, permission)
+);
+comment on table public.role_permissions is 'Application permissions for each role.';
+```
+
+
+
+ For the [full schema](https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone/README.md), see the example application on [GitHub](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone).
+
+
+
+You can now manage your roles and permissions in SQL. For example, to add the mentioned roles and permissions from above, run:
+
+```sql supabase/seed.sql
+insert into public.role_permissions (role, permission)
+values
+ ('admin', 'channels.delete'),
+ ('admin', 'messages.delete'),
+ ('moderator', 'messages.delete');
+```
+
+## Create Auth Hook to apply user role
+
+The [Custom Access Token Auth Hook](/docs/guides/auth/auth-hooks#hook-custom-access-token) runs before a token is issued. You can use it edit the JWT.
+
+
+
+
+```sql supabase/migrations/auth_hook.sql
+-- Create the auth hook function
+create or replace function public.custom_access_token_hook(event jsonb)
+returns jsonb
+language plpgsql
+immutable
+as $$
+ declare
+ claims jsonb;
+ user_role public.app_role;
+ begin
+ -- Check if the user is marked as admin in the profiles table
+ select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
+
+ claims := event->'claims';
+
+ if user_role is not null then
+ -- Set the claim
+ claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
+ else
+ claims := jsonb_set(claims, '{user_role}', 'null');
+ end if;
+
+ -- Update the 'claims' object in the original event
+ event := jsonb_set(event, '{claims}', claims);
+
+ -- Return the modified or original event
+ return event;
+ end;
+$$;
+
+grant usage on schema public to supabase_auth_admin;
+
+grant execute
+ on function public.custom_access_token_hook
+ to supabase_auth_admin;
+
+revoke execute
+ on function public.custom_access_token_hook
+ from authenticated, anon;
+
+grant all
+ on table public.user_roles
+to supabase_auth_admin;
+
+revoke all
+ on table public.user_roles
+ from authenticated, anon;
+
+create policy "Allow auth admin to read user roles" ON public.user_roles
+as permissive for select
+to supabase_auth_admin
+using (true)
+```
+
+
+
+
+```sql supabase/migrations/auth_hook.sql
+-- Enable the "plv8" extension
+create extension plv8;
+
+-- Create the auth hook function
+create or replace function custom_access_token_hook(event jsonb)
+returns jsonb
+language plv8
+as $$
+ var user_role;
+
+ // Fetch the current user's user_role from the public user_roles table.
+ var result = plv8.execute("select role from public.user_roles where user_id = $1", [event.user_id]);
+ if (result.length > 0) {
+ user_role = result[0].role;
+ } else {
+ // Assign null
+ user_role = null;
+ }
+
+ // Check if 'claims' exists in the event object; if not, initialize it
+ if (!event.claims) {
+ event.claims = {};
+ }
+
+ // Update the level in the claims
+ event.claims.user_role = user_role;
+
+ return event;
+$$;
+
+grant usage on schema public to supabase_auth_admin;
+
+grant execute
+ on function public.custom_access_token_hook
+ to supabase_auth_admin;
+
+revoke execute
+ on function public.custom_access_token_hook
+ from authenticated, anon;
+
+grant all
+ on table public.user_roles
+to supabase_auth_admin;
+
+revoke all
+ on table public.user_roles
+ from authenticated, anon;
+
+create policy "Allow auth admin to read user roles" ON public.user_roles
+as permissive for select
+to supabase_auth_admin
+using (true)
+```
+
+
+
+
+### Enable the hook
+
+In the dashboard, navigate to [`Authentication > Hooks (Beta)`](/dashboard/project/_/auth/hooks) and select the appropriate PostgreSQL function from the dropdown menu.
+
+When developing locally, follow the [local development](/docs/guides/auth/auth-hooks#local-development) instructions.
+
+
+
+To learn more about Auth Hooks, see the [Auth Hooks docs](/docs/guides/auth/auth-hooks).
+
+
+
+## Accessing custom claims in RLS policies
+
+To utilize Role-Based Access Control (RBAC) in Row Level Security (RLS) policies, create an `authorize` method that reads the user's role from their JWT and checks the role's permissions:
+
+```sql supabase/migrations/init.sql
+create function public.authorize(
+ requested_permission app_permission,
+ user_id uuid
+)
+returns boolean as $$
+declare
+ bind_permissions int;
+begin
+ select count(*)
+ from public.role_permissions
+ where role_permissions.permission = authorize.requested_permission
+ and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
+ into bind_permissions;
+
+ return bind_permissions > 0;
+end;
+$$ language plpgsql security definer set search_path = public;
+```
+
+
+
+You can read more about using functions in RLS policies in the [RLS guide](/docs/guides/auth/row-level-security#using-functions).
+
+
+
+You can then use the `authorize` method within your RLS policies. For example, to enable the desired delete access, you would add the following policies:
+
+```sql
+create policy "Allow authorized delete access" on public.channels for delete using ( authorize('channels.delete', auth.uid()) );
+create policy "Allow authorized delete access" on public.messages for delete using ( authorize('messages.delete', auth.uid()) );
+```
+
+## Accessing custom claims in your application
+
+The auth hook will only modify the access token JWT but not the auth response. Therefore, to access the custom claims in your application, e.g. your browser client, or server-side middleware, you will need to decode the `access_token` JWT on the auth session.
+
+In a JavaScript client application you can for example use the [`jwt-decode` package](https://www.npmjs.com/package/jwt-decode):
+
+```js
+import { jwtDecode } from 'jwt-decode'
+
+const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
+ if (session) {
+ const jwt = jwtDecode(session.access_token)
+ const userRole = jwt.user_role
+ }
+})
+```
+
+For server-side logic you can use packages like [express-jwt](https://github.com/auth0/express-jwt), [koa-jwt](https://github.com/stiang/koa-jwt), [PyJWT](https://github.com/jpadilla/pyjwt), [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken), [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer), etc.
+
+## Conclusion
+
+You now have a robust system in place to manage user roles and permissions within your database that automatically propagates to Supabase Auth.
+
+## More resources
+
+- [Auth Hooks](/docs/guides/auth/auth-hooks)
+- [Row Level Security](/docs/guides/auth/row-level-security)
+- [RLS Functions](/docs/guides/auth/row-level-security#using-functions)
+- [Next.js Slack Clone Example](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone)
diff --git a/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx b/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx
index 7017d7a779b..f80a71671a5 100644
--- a/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx
+++ b/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx
@@ -343,4 +343,58 @@ In certain auth scenarios you will need to handle linking back into your applica
```
+
+ ### Deep link config
+
+ 1. Go to your [auth settings](https://supabase.com/dashboard/project/_/auth/url-configuration) page.
+ 2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link.
+
+ The redirect callback url should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
+
+ Now, edit the Android manifest to make sure the app opens when the user clicks on the magic link.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ Check the [Android documentation](https://developer.android.com/training/app-links/deep-linking) for more information.
+
+ Next, specify the scheme and host in the Supabase Client:
+ ```kotlin
+ install(Auth) {
+ host = "login-callback"
+ scheme = "io.supabase.user-management"
+ }
+ ```
+
+ Finally, call `Auth#handleDeeplinks` when the app opens:
+
+ ```kotlin
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ supabase.handleDeeplinks(intent)
+ }
+ ```
+
+ The user will now be authenticated when your app receives a valid deeplink!
+
+
diff --git a/apps/docs/content/guides/auth/phone-login.mdx b/apps/docs/content/guides/auth/phone-login.mdx
index a608bcb26eb..e35520a002a 100644
--- a/apps/docs/content/guides/auth/phone-login.mdx
+++ b/apps/docs/content/guides/auth/phone-login.mdx
@@ -108,7 +108,7 @@ curl -X POST 'https://cvwawazfelidkloqmbma.supabase.co/auth/v1/signup' \
The user receives an SMS with a 6-digit pin that you must [verify](/docs/guides/auth/phone-login#verify-a-user) within 60 seconds.
-After their phone number is verified, the user can use their phone number and password to sign in with needing to reverify each time:
+Once a user's phone number is verified, they can sign in using their phone number and password without requiring phone number re-verification at each sign-in:
+
+To manage costs related to SMS authentication, consider setting project rate limits and configuring CAPTCHA. For additional cost-control strategies, visit the Production Checklist.
+
+Also check country-specific regulations around sending SMS messages, for example, India's TRAI DLT
+regulations.
+
+
+
+## Prerequisites
+
+Before you begin, ensure you have the following:
+
+- **A Textlocal account**: Sign up for [TextLocal](https://www.textlocal.com/signup) to start sending SMS messages.
+- **A Supabase project**: Necessary for SMS authentication integration. Create your project in the [Supabase Dashboard](https://supabase.com/dashboard).
+- **A mobile phone**: To receive SMS messages and test your setup.
+
+## Setting up Textlocal as your SMS provider
+
+To integrate Textlocal with Supabase:
+
+1. [Get a Textlocal API key](#get-a-textlocal-api-key)
+2. [Customize your sender name](#customize-your-sender-name-optional)
+3. [Configure your Supabase project](#configure-supabase)
+
+### Get a Textlocal API key
+
+1. Log into your Textlocal account and go to `Settings` > `API Keys`.
+2. Generate a new API Key. Save your new API key in a safe location.
+
+### Customize your sender name (Optional)
+
+Textlocal defaults to `TXTLCL` as the sender name for all messages. You can customize this to better reflect your brand:
+
+1. In your Textlocal dashboard, go to `Settings` > `All Settings` > `Sender Names`.
+2. Change your sender name.
+
+### Configure Supabase
+
+To set up Textlocal as your SMS provider in Supabase, follow these steps:
+
+1. In your Supabase Dashboard, go to the [Auth Providers section](https://supabase.com/dashboard/project/_/auth/providers).
+2. From the list of available authentication providers, select `Phone`.
+3. Toggle `Enable Phone Provider`.
+4. Under `SMS Provider`, select `Textlocal.`
+5. Enter your Textlocal API Key and Sender Name.
+6. Customize your SMS Message (optional).
+
+## Next steps
+
+To implement Phone Login, see the docs on [using Phone Login](/docs/guides/auth/phone-login#using-phone-login).
+
+## Resources
+
+- [Textlocal Signup](https://www.textlocal.com/sign-up)
+- [Supabase Dashboard](https://supabase.com/dashboard)
diff --git a/apps/docs/content/guides/auth/phone-login/twilio.mdx b/apps/docs/content/guides/auth/phone-login/twilio.mdx
index 86bbd8bae8b..179002d8ae4 100644
--- a/apps/docs/content/guides/auth/phone-login/twilio.mdx
+++ b/apps/docs/content/guides/auth/phone-login/twilio.mdx
@@ -11,7 +11,7 @@ Authenticating users via SMS can become expensive. Adjust your project's rate li
-## Preqrequisites
+## Prerequisites
You'll need:
@@ -32,7 +32,7 @@ To set up Twilio Verify, you will need to:
When using Twilio Verify, OTPs are generated by Twilio. This means that:
- Unlike other providers, the OTP expiry duration and message content fields are not configurable via the Supabase dashboard. Please head to Twilio Verify to configure these settings.
-- The token remains the same during its validity period until the verification is successful. This means if your user make another request within that period, they will receive the same token.
+- The token remains the same during its validity period until the verification is successful. This means if your user makes another request within that period, they will receive the same token.
- Twilio Verify has a separate set of rate limits that apply. Visit Twilio's [Rate Limit and Timeouts page](https://www.twilio.com/docs/verify/api/rate-limits-and-timeouts) to find out more.
diff --git a/apps/docs/content/guides/auth/row-level-security.mdx b/apps/docs/content/guides/auth/row-level-security.mdx
index f7fb52a63df..1ca5491e198 100644
--- a/apps/docs/content/guides/auth/row-level-security.mdx
+++ b/apps/docs/content/guides/auth/row-level-security.mdx
@@ -40,11 +40,11 @@ where auth.uid() = todos.user_id;
-- Policy is implicitly added.
```
-## Authenticated and anonymous roles
+## Authenticated and unauthenticated roles
Supabase Auth maps every request to one of the roles:
-- `anon`: an anonymous request (the user is not logged in)
+- `anon`: an unauthenticated request (the user is not logged in)
- `authenticated`: an authenticated request (the user is logged in)
These are actually [Postgres Roles](/docs/guides/database/postgres/roles), and so they have significant value for the [performance](/docs/guides/database/postgres/row-level-security#specify-roles-in-your-policies) of your RLS Policies. You can use these roles within your Policies using the `TO` clause:
diff --git a/apps/docs/content/guides/auth/server-side-rendering.mdx b/apps/docs/content/guides/auth/server-side-rendering.mdx
index 78e0d7e31c4..ad5473b285d 100644
--- a/apps/docs/content/guides/auth/server-side-rendering.mdx
+++ b/apps/docs/content/guides/auth/server-side-rendering.mdx
@@ -153,7 +153,7 @@ This is traditionally done by setting cookies. Here's an example you can add to
```typescript
supabase.auth.onAuthStateChange((event, session) => {
- if (event === 'SIGNED_OUT' || event === 'USER_DELETED') {
+ if (event === 'SIGNED_OUT') {
// delete cookies on sign out
const expires = new Date(0).toUTCString()
document.cookie = `my-access-token=; path=/; expires=${expires}; SameSite=Lax; secure`
diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx
index 080dadd151e..f90f3c2c781 100644
--- a/apps/docs/content/guides/auth/server-side/nextjs.mdx
+++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx
@@ -203,6 +203,7 @@ It's safe to trust `getUser()` because it sends a request to the Supabase Auth s
```ts middleware.ts
+import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) {
@@ -400,7 +401,7 @@ If you have email confirmation turned on (the default), a new user will receive
Change the email template to support a server-side authentication flow.
-Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In each email template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
+Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
diff --git a/apps/docs/content/guides/auth/server-side/oauth-with-pkce-flow-for-ssr.mdx b/apps/docs/content/guides/auth/server-side/oauth-with-pkce-flow-for-ssr.mdx
index 9ff80b227e0..bc69e411514 100644
--- a/apps/docs/content/guides/auth/server-side/oauth-with-pkce-flow-for-ssr.mdx
+++ b/apps/docs/content/guides/auth/server-side/oauth-with-pkce-flow-for-ssr.mdx
@@ -207,6 +207,17 @@ app.get("/auth/callback", async function (req, res) {
Let's point our `.signInWithOAuth` method's redirect to the callback route we create above:
+
+
+
+In the browser, `signInWithOAuth` automatically redirects to the SSO provider's authentication endpoint, which then redirects to your endpoint.
+
```js
await supabase.auth.signInWithOAuth({
provider,
@@ -215,3 +226,26 @@ await supabase.auth.signInWithOAuth({
},
})
```
+
+
+
+
+
+In the server, you need to handle the redirect to the SSO provider's authentication endpoint. The `signInWithOAuth` method returns the endpoint URL, which you can redirect to.
+
+```js
+const { data, error } = await supabase.auth.signInWithOAuth({
+ provider,
+ options: {
+ redirectTo: '/auth/callback',
+ },
+})
+
+if (data.url) {
+ redirect(data.url) // use the redirect API for your server framework
+}
+```
+
+
+
+
diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx
index d7134a4ad6c..8360c023780 100644
--- a/apps/docs/content/guides/auth/social-login/auth-google.mdx
+++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx
@@ -452,8 +452,7 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
## Using Google sign-in with Kotlin Multiplatform
- When using [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/), you can use the [compose-auth](https://supabase.com/docs/reference/kotlin/installing) plugin. On Android it uses Google OneTap (or Credential Manager on Android 14+) automatically and on other platforms it uses `auth.signInWith(Google)`.
- Alternatively, you can have a look at [Google One Tap](https://developers.google.com/identity/one-tap/android/overview), just login with the IdToken after you receive it.
+ When using [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/), you can use the [compose-auth](https://supabase.com/docs/reference/kotlin/installing) plugin. On Android it uses the Credential Manager automatically and on other platforms it uses `auth.signInWith(Google)`.
**Initialize the Supabase Client**
diff --git a/apps/docs/content/guides/cli/getting-started.mdx b/apps/docs/content/guides/cli/getting-started.mdx
index 692ee795ef7..ab42d2f9db1 100644
--- a/apps/docs/content/guides/cli/getting-started.mdx
+++ b/apps/docs/content/guides/cli/getting-started.mdx
@@ -123,9 +123,20 @@ npm update supabase --save-dev
-If you have any Supabase containers running locally, remember to restart them after upgrading to use the new features.
+If you have any Supabase containers running locally, stop them and delete their data volumes before proceeding with the upgrade. This ensures that Supabase managed services can apply new migrations on a clean state of the local database.
-```bash
+
+
+Remember to save any local schema and data changes before stopping because the `--no-backup` flag will delete them.
+
+```sh
+supabase db diff my_schema
+supabase db dump --local --data-only > supabase/seed.sql
+```
+
+
+
+```sh
supabase stop --no-backup
supabase start
```
diff --git a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx
new file mode 100644
index 00000000000..a712c85801b
--- /dev/null
+++ b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx
@@ -0,0 +1,158 @@
+---
+id: 'serverless-drivers'
+title: 'Serverless Drivers'
+description: 'Connecting to your Postgres database in serverless environments.'
+subtitle: 'Connecting to your Postgres database in serverless environments.'
+---
+
+Supabase provides several options for connecting to your Postgres database from serverless environments.
+
+[supabase-js](/reference/javascript/introduction) is an isomorphic JavaScript client that uses the [auto-generated REST API](/guides/api) and therefore works in any environment that supports HTTPS connections. This API has a built-in [connection pooler](/guides/database/connecting-to-postgres#connection-pooler) and can serve thousands of simultaneous requests, and therefore is ideal for Serverless workloads.
+
+## Vercel Edge Functions
+
+Vercel's [Edge runtime](https://vercel.com/docs/functions/runtimes/edge-runtime) is built on top of the [V8 engine](https://v8.dev/), that provides a limited set of Web Standard APIs.
+
+### Quickstart
+
+Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integration](https://vercel.com/integrations/supabase) to automatically configure your connection strings as environment variables on your Vercel project!
+
+
+
+ {[
+ {
+ title: 'supabase-js',
+ hasLightIcon: true,
+ href: 'https://supabase.link/nextjs-with-supabase-starter',
+ description: 'A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript and Tailwind CSS.'
+ },
+ {
+ title: 'Drizzle',
+ hasLightIcon: true,
+ href: 'https://supabase.link/nextjs-supabase-drizzle',
+ description: "Simple Next.js template that uses Supabase as the database and Drizzle as the ORM.",
+ },
+ {
+ title: 'Kysely',
+ hasLightIcon: true,
+ href: 'https://supabase.link/nextjs-supabase-kysely',
+ description: 'Simple Next.js template that uses Supabase as the database and Kysely as the query builder.',
+ },
+ /* { TODO: figure out how to get around Prisma accelerate requirement...
+ title: 'Prisma',
+ hasLightIcon: true,
+ href: 'https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fstorage%2Fpostgres-prisma&project-name=postgres-prisma&repository-name=postgres-prisma&demo-title=Vercel%20Postgres%20%2B%20Prisma%20Next.js%20Starter&demo-description=Simple%20Next.js%20template%20that%20uses%20Vercel%20Postgres%20as%20the%20database%20and%20Prisma%20as%20the%20ORM.&demo-url=https%3A%2F%2Fpostgres-prisma.vercel.app%2F&demo-image=https%3A%2F%2Fpostgres-prisma.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6',
+ description: 'Simple Next.js template that uses Vercel Postgres as the database and Prisma as the ORM.',
+ } */
+ ].map((resource) => {
+ return (
+
+
+ {resource.description}
+
+
+ )
+
+})}
+
+
+
+
+### Manual configuration
+
+In your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database), make sure `Use connection pooler` is checked and `Transaction` mode is selected, then copy the URI and save it as the `POSTGRES_URL` environment variable. Remember to replace the password placeholder with your actual database password and add the following suffix `?workaround=supabase-pooler.vercel`.
+
+```txt .env.local
+POSTGRES_URL="postgres://postgres.cfcxynqnhdybqtbhjemm:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?workaround=supabase-pooler.vercel"
+```
+
+
+
+
+
+```ts lib/drizzle.ts
+import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
+import { InferSelectModel, InferInsertModel } from 'drizzle-orm'
+import { sql } from '@vercel/postgres'
+import { drizzle } from 'drizzle-orm/vercel-postgres'
+
+export const UsersTable = pgTable(
+ 'users',
+ {
+ id: serial('id').primaryKey(),
+ name: text('name').notNull(),
+ email: text('email').notNull(),
+ image: text('image').notNull(),
+ createdAt: timestamp('createdAt').defaultNow().notNull(),
+ },
+ (users) => {
+ return {
+ uniqueIdx: uniqueIndex('unique_idx').on(users.email),
+ }
+ }
+)
+
+export type User = InferSelectModel
+export type NewUser = InferInsertModel
+
+// Connect to Vercel Postgres
+export const db = drizzle(sql)
+```
+
+
+
+
+```ts lib/kysely.ts
+import { Generated, ColumnType } from 'kysely'
+import { createKysely } from '@vercel/postgres-kysely'
+
+interface UserTable {
+ // Columns that are generated by the database should be marked
+ // using the `Generated` type. This way they are automatically
+ // made optional in inserts and updates.
+ id: Generated
+ name: string
+ email: string
+ image: string
+
+ // You can specify a different type for each operation (select, insert and
+ // update) using the `ColumnType`
+ // wrapper. Here we define a column `createdAt` that is selected as
+ // a `Date`, can optionally be provided as a `string` in inserts and
+ // can never be updated:
+ createdAt: ColumnType
+}
+
+// Keys of this interface are table names.
+export interface Database {
+ users: UserTable
+}
+
+export const db = createKysely()
+export { sql } from 'kysely'
+```
+
+
+
+
+## Cloudflare Workers
+
+Cloudflare's Workers runtime also uses the [V8 engine](https://v8.dev/) but provides polyfills for a subset of Node.js APIs and [TCP Sockets API](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/), giving you a couple of options:
+
+- [supabase-js](https://developers.cloudflare.com/workers/databases/native-integrations/supabase/)
+- [Postgres.js](https://github.com/porsager/postgres?tab=readme-ov-file#cloudflare-workers-support)
+- [node-postgres](https://developers.cloudflare.com/workers/tutorials/postgres/)
+
+## Supabase Edge Functions
+
+Supabase Edge Functions uses the [Deno runtime](https://deno.com/) which has native support for TCP connections allowing you to choose your favorite client:
+
+- [supabase-js](/guides/functions/connect-to-postgres#using-supabase-js)
+- [Deno Postgres driver](/guides/functions/connect-to-postgres#using-a-postgres-client)
+- [Postgres.js](https://github.com/porsager/postgres)
+- [Drizzle](/guides/functions/connect-to-postgres#using-drizzle)
diff --git a/apps/docs/content/guides/database/extensions.mdx b/apps/docs/content/guides/database/extensions.mdx
index 5566a391f6a..d5fb2834e26 100644
--- a/apps/docs/content/guides/database/extensions.mdx
+++ b/apps/docs/content/guides/database/extensions.mdx
@@ -36,7 +36,7 @@ drop
extension pgtap;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension call `drop extension`.
@@ -48,7 +48,7 @@ Most extensions are installed under the `extensions` schema, which is accessible
If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension.
-Some extensions can only be created under a specific schema, for eg. `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
+Some extensions can only be created under a specific schema, for example, `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
In addition to the pre-configured extensions, you can also install your own SQL extensions directly in the database using Supabase's SQL editor. The SQL code for the extensions, including plpgsql extensions, can be added through the SQL editor.
diff --git a/apps/docs/content/guides/database/extensions/hypopg.mdx b/apps/docs/content/guides/database/extensions/hypopg.mdx
index 90c8184bbfe..643c0322352 100644
--- a/apps/docs/content/guides/database/extensions/hypopg.mdx
+++ b/apps/docs/content/guides/database/extensions/hypopg.mdx
@@ -21,7 +21,7 @@ The motivation for HypoPG is to allow users to quickly search for an index to im
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "hypopg" and enable the extension.
+3. Search for `hypopg` and enable the extension.
@@ -35,7 +35,7 @@ create extension hypopg with schema extensions;
drop extension if exists hypopg;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -104,10 +104,10 @@ create index on account(id);
- [`hypo_create_index(text)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#create-a-hypothetical-index): A function to create a hypothetical index.
- [`hypopg_list_indexes`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A View that lists all hypothetical indexes that have been created.
-- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as pg_index.
+- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as `pg_index`.
- [`hypopg_get_index_def(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to display the `create index` statement that would create the index.
- [`hypopg_get_relation_size(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to estimate how large a hypothetical index would be.
-- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by oid.
+- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by `oid`.
- [`hypopg_reset()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove all hypothetical indexes.
## Resources
diff --git a/apps/docs/content/guides/database/extensions/pg_jsonschema.mdx b/apps/docs/content/guides/database/extensions/pg_jsonschema.mdx
index 9261c0a65f8..2770de88178 100644
--- a/apps/docs/content/guides/database/extensions/pg_jsonschema.mdx
+++ b/apps/docs/content/guides/database/extensions/pg_jsonschema.mdx
@@ -19,7 +19,7 @@ description: 'Validate json/jsonb with JSON Schema in PostgreSQL.'
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "pg_jsonschema" and enable the extension.
+3. Search for `pg_jsonschema` and enable the extension.
@@ -33,7 +33,7 @@ create extension pg_jsonschema with schema extensions;
drop extension if exists pg_jsonschema;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
diff --git a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx
index 9bc6095b4d2..4c9cba45291 100644
--- a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx
+++ b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx
@@ -4,7 +4,7 @@ title: 'pg_plan_filter: Restrict Total Cost'
description: 'Block queries over a total cost limit'
---
-[pg_plan_filter](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
+[`pg_plan_filter`](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
## Enable the extension
@@ -27,7 +27,7 @@ alter database some_db set session_preload_libraries = 'plan_filter';
`plan_filter.statement_cost_limit`: restricts the maximum total cost for executed statements
`plan_filter.limit_select_only`: restricts to `select` statements
-Note that `limit_select_only = true` is not the same as read-only because `select` statements may modify data e.g. through a function call.
+Note that `limit_select_only = true` is not the same as read-only because `select` statements may modify data, for example, through a function call.
## Example
@@ -88,4 +88,4 @@ HINT: The plan for your query shows that it would probably have an excessive ru
## Resources
-- Official [pg_plan_filter documentation](https://github.com/pgexperts/pg_plan_filter)
+- Official [`pg_plan_filter` documentation](https://github.com/pgexperts/pg_plan_filter)
diff --git a/apps/docs/content/guides/database/extensions/pgaudit.mdx b/apps/docs/content/guides/database/extensions/pgaudit.mdx
index 5229ffb0ee9..2d9f5a2afc9 100644
--- a/apps/docs/content/guides/database/extensions/pgaudit.mdx
+++ b/apps/docs/content/guides/database/extensions/pgaudit.mdx
@@ -4,11 +4,15 @@ title: 'PGAudit: Postgres Auditing'
description: 'Session and object auditing via PostgreSQL standard logging'
---
-[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension for logging session and object auditing over the standard PostgreSQL logging utility.
+[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension that extends PostgreSQL's built-in logging abilities. It can be used to selectively track activities within your database.
-PGAudit grants fine grain control over which statements and objects are emitted to logs.
+This helps you with:
-## Enable the extension
+- **Compliance**: Meeting audit requirements for regulations
+- **Security**: Detecting suspicious database activity
+- **Troubleshooting**: Identifying and fixing database issues
+
+## Enable the Extension
@@ -35,74 +39,364 @@ create extension pgaudit;
drop extension if exists pgaudit;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
-To disable an extension you can call `drop extension`.
-
-## Settings
+## Configure the Extension
-The `pgaudit.log` setting controls which statements to log. Available values include:
+PGAudit can be configured with multiple different levels of precision.
-- **read**: `SELECT` and `COPY` when the source is a relation or a query.
-- **write**: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and `COPY` when the destination is a relation.
-- **function**: Function calls and `DO` blocks.
-- **role**: Statements related to roles and privileges: `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`.
-- **ddl**: All `DDL` that is not included in the `ROLE` class.
-- **misc**: Miscellaneous commands, e.g. `DISCARD`, `FETCH`, `CHECKPOINT`, `VACUUM`, `SET`.
-- **misc_set**: Miscellaneous `SET` commands, e.g. `SET ROLE`.
+**PGAudit Logging Precision:**
-- **all**: Include all of the above.
+- **[Session](#session-logging):** Logs activity within a connection, such as a [PSQL](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection.
+- **[User](#user-logging):** Logs activity by a particular database user (e.g., anon or postgres).
+- **[Global](#global-logging):** Logs activity across the entire database.
+- **[Object](#object-mode-settings):** Logs events related to specific database objects (for example, the auth.users table).
-For a full list of available settings see [settings docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#settings). Be aware that the `all` setting will generate a very large volume of logs.
+Although Session, User, and Global modes differ in their precision, they are all considered variants of **Session Mode** and are configured with the same input categories.
+
+### Session Mode Categories
+
+These modes can monitor predefined categories of database operations:
+
+| Category | What it Logs | Description |
+| -------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| read | Data retrieval (SELECT, COPY) | Tracks what data is being accessed. |
+| write | Data modification (INSERT, DELETE, UPDATE, TRUNCATE, COPY) | Tracks changes made to your database. |
+| function | FUNCTION, PROCEDURE, and DO/END block executions | Tracks routine/function executions |
+| role | User management actions (CREATE, DROP, ALTER on users and privileges) | Tracks changes to user permissions and access. |
+| ddl | Schema changes (CREATE, DROP, ALTER statements) | Monitors modifications to your database structure (tables, indexes, etc.). |
+| misc | Less common commands (FETCH, CHECKPOINT) | Captures obscure actions for deeper analysis if needed. |
+| all | Everything above | Comprehensive logging for complete audit trails. |
+
+Below is a limited example of how to assign pgaudit to monitor specific categories.
+
+```sql
+-- log all CREATE, ALTER, and DROP events
+... pgaudit.log = 'ddl';
+
+-- log all CREATE, ALTER, DROP, and SELECT events
+... pgaudit.log = 'read, ddl';
+
+-- log nothing
+... pgaudit.log = 'none';
+```
+
+### Session Logging
+
+When you are connecting in a session environment, such as a PSQL connection, you can configure pgaudit to record events initiated within the session.
-We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [pgsodium](/docs/guides/database/extensions/pgsodium) or [Vault](/docs/guides/database/vault).
-
-You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) if you'd like this restriction lifted.
+The [Dashboard](https://supabase.com/dashboard/project/_) is a transactional environment and will not sustain a session.
-## Example
+Inside a session, by default, pgaudit will log nothing:
-Given a pgaudit setting
-
-{/* prettier-ignore */}
```sql
-set pgaudit.log = 'read, ddl';
+-- returns 'none'
+show pgaudit.log;
```
-The following create table, insert and select statements
+In the session, you can `SET` the `pgaudit.log` variable to record events:
+
+```sql
+-- log CREATE, ALTER, and DROP events
+set pgaudit.log = 'ddl';
+
+-- log all CREATE, ALTER, DROP, and SELECT events
+set pgaudit.log = 'read, ddl';
+
+-- log nothing
+set pgaudit.log = 'none';
+```
+
+### User Logging
+
+There are some cases where you may want to monitor a database user's actions. For instance, let's say you connected your database to [Zapier](https://supabase.com/partners/integrations/zapier) and created a custom role for it to use:
+
+```sql
+create user "zapier" with password '';
+```
+
+You may want to log all actions initiated by `zapier`, which can be done with the following command:
+
+```sql
+alter role "zapier" set pgaudit.log to 'all';
+```
+
+To remove the settings, execute the following code:
+
+```sql
+-- disables role's log
+alter role "zapier" set pgaudit.log TO 'none';
+
+-- check to make sure the changes are finalized:
+select rolname,
+ rolconfig
+from pg_roles
+where rolname = 'zapier';
+-- should return a rolconfig path with "pgaudit.log=none" present
+```
+
+### Global Logging
+
+
+
+Use global logging sparingly as it can generate many logs and make it difficult to find important events. Consider limiting the scope of what is logged by using user or object logging where possible.
+
+
+
+The below SQL configures pgaudit to record all events associated with the "postgres" role. Since postgres has extensive privileges, this effectively monitors all database activity.
+
+```sql
+alter role "postgres" set pgaudit.log to 'all';
+```
+
+To check if the postgres role is auditing, execute the following command:
+
+```sql
+select
+ rolname,
+ rolconfig
+from pg_roles
+where rolname = 'postgres';
+-- should return a rolconfig path with "pgaudit.log=all" present
+```
+
+To remove the settings, execute the following code:
+
+```sql
+alter role "postgres" set pgaudit.log to 'none';
+```
+
+## Object Mode Settings
+
+Assigning a role to pgaudit will result in the logging of **any actions** that role is **capable** of performing, _regardless of who or what actually initiated the event_.
+
+### Object Logging
+
+To fine-tune what pgaudit will record, you must create a custom database role with limited permissions:
+
+```sql
+create role "some_audit_role" noinherit;
+```
+
+No other Postgres user can assume or login via this role. It solely exists to securely define what pgaudit will record.
+
+Once the role is created, you can direct pgaudit to log by assigning it to the `pgaudit.role` variable:
+
+```sql
+alter role "postgres" set pgaudit.role to 'some_audit_role';
+```
+
+You can then assign the role to monitor only approved events, such as `select` statements that include a specific table:
+
+```sql
+grant select on random_table to "some_audit_role";
+```
+
+With this privilege granted, pgaudit will record all select statements against the `random_table`. Note, that this does include joins that feature the table, too. All assignignable priveleges can be viewed in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
+
+If you would no longer like to use object logging, you will need to unnasign the `pgaudit.role` variable:
+
+```sql
+-- change pgaudit.role to no longer reference some_audit_role
+alter role "postgres" set pgaudit.role to '';
+
+-- view if pgaudit.role changed with the following command:
+select
+ rolname,
+ rolconfig
+from pg_roles
+where rolname = 'postgres';
+-- should return a rolconfig path with "pgaudit.role="
+```
+
+If you are no longer using a `ROLE`, it is best to `DROP`it:
+
+```sql
+drop role "some_audit_role";
+```
+
+## Interpreting Audit Logs
+
+pgaudit was designed for storing logs as CSV files with the following headers:
+
+> Referenced from the [pgaudit official docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#format)
+
+| header | Description |
+| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| AUDIT_TYPE | SESSION or OBJECT |
+| STATEMENT_ID | Unique statement ID for this session. Sequential even if some statements are not logged. |
+| SUBSTATEMENT_ID | Sequential ID for each sub-statement within the main statement. Continuous even if some are not logged. |
+| CLASS | e.g., READ, ROLE (see pgaudit.log). |
+| COMMAND | e.g., ALTER TABLE, SELECT. |
+| OBJECT_TYPE | TABLE, INDEX, VIEW, etc. Available for SELECT, DML, and most DDL statements. |
+| OBJECT_NAME | The fully qualified object name (e.g., public.account). Available for SELECT, DML, and most DDL. |
+| STATEMENT | Statement executed on the backend. |
+| PARAMETER | If pgaudit.log_parameter is set, this field contains the statement parameters as quoted CSV, or \. Otherwise, it's \. |
+
+A log made from the following create statement:
-{/* prettier-ignore */}
```sql
create table account (
id int primary key,
name text,
description text
);
-
-insert into account (id, name, description)
-values (1, 'Foo Barsworth', 'Customer account');
-
-select * from account;
```
-Results in the log output
+Generates the following log in the [Dashboard's Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs):
-```text
-AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
+```
+ AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
id int,
name text,
description text
-);,
-AUDIT: SESSION,2,1,READ,SELECT,,,select * from account,,
+);
```
-Note that the insert statement is not logged because we did not include the `write` option for `pgaudit.log`.
+## Finding and Filtering Audit Logs
+
+Logs generated by pgaudit can be found in [Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs?s=AUDIT). To find a specific log, you can use the log explorer. Below is a basic example to extract logs referencing `CREATE TABLE` events
+
+```sql
+select
+ cast(t.timestamp as datetime) as timestamp,
+ event_message
+from
+ postgres_logs as t
+ cross join unnest(metadata) as m
+ cross join unnest(m.parsed) as p
+where event_message like 'AUDIT%CREATE TABLE%'
+order by timestamp desc
+limit 100;
+```
+
+## Practical Examples
+
+### Monitoring API events
+
+
+
+API requests are already recorded in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) logs.
+
+
+
+To monitor all writes initiated by the Postgrest API roles:
+
+```sql
+alter role "authenticator" set pgaudit.log to 'write';
+
+-- the above is the practical equivalent to:
+-- alter role "anon" set pgaudit.log TO 'write';
+-- alter role "authenticated" set pgaudit.log TO 'write';
+-- alter role "service_role" set pgaudit.log TO 'write';
+```
+
+### Monitoring the `auth.users` Table
+
+In the worst case scenario, where a privileged roles' password is exposed, you can use pgaudit to monitor if the `auth.users` table was targeted. It should be stated that API requests are already monitored in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) and this is more about providing greater clarity about what is happening at the database level.
+
+Logging `auth.user` should be done in Object Mode and requires a custom role:
+
+```sql
+-- create logging role
+create role "auth_auditor" noinherit;
+
+-- give role permission to observe relevant table events
+grant select on auth.users to "auth_auditor";
+grant delete on auth.users to "auth_auditor";
+
+-- assign auth_auditor to pgaudit.role
+alter role "postgres" set pgaudit.role to 'auth_auditor';
+
+```
+
+With the above code, any query involving reading or deleting the auth.users email column will be logged.
+
+## Best Practices
+
+### Over-logging: Help! Too many logs!
+
+pgaudit, if not configured mindfully, can log all database events, including background tasks. This can generate an undesirablely large amount of logs in a few hours.
+
+The first step to solve this problem is to identify which database users pgaudit is observing:
+
+```sql
+-- find all users monitored by pgaudit
+select
+ rolname,
+ rolconfig
+from pg_roles
+where
+ exists (
+ select
+ 1
+ from UNNEST(rolconfig) as c
+ where c like '%pgaudit.role%' or c like '%pgaudit.log%'
+ );
+```
+
+To prevent pgaudit from monitoring the problematic roles, you'll want to change their `pgaudit.log` values to `none` and `pgaudit.role` values to `empty quotes ''`
+
+```sql
+ -- Use to disable object level logging
+ alter role "" set pgaudit.role to '';
+
+ -- Use to disable global and user level logging
+ alter role "" set pgaudit.log to 'none';
+```
+
+## FAQ
+
+#### How can I use pgaudit to debug database functions?
+
+Technically yes, but it is not the best approach. It is better to check out our [function debugging guide](https://supabase.com/docs/guides/database/functions#general-logging) instead.
+
+#### How can I download database logs?
+
+In the [Logs Dashboard](https://supabase.com/dashboard/project/_/logs/postgres-logs) you can download logs as CSVs.
+
+#### Can I log observed table rows?
+
+By default, pgaudit records queries, but not the returned rows. You can modify this behavior with the `pgaudit.log_rows` variable:
+
+```sql
+--enable
+alter role "postgres" set pgaudit.log_rows to 'on';
+
+-- disable
+alter role "postgres" set pgaudit.log_rows to 'off';
+```
+
+You should not do this unless you are _absolutely_ certain it is necessary for your use case. It can expose sensitive values to your logs that ideally should not be preserved. Furthermore, if done in excess, it can noticably reduce database performance.
+
+#### Can I log function parameters?
+
+We don't currently support configuring pgaudit.log_parameter because it may log secrets in encrypted columns if you are using [pgsodium](https://supabase.com/docs/guides/database/extensions/pgsodium) or[Vault](https://supabase.com/docs/guides/database/vault).
+
+You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) with your use-case if you'd like this restriction lifted.
+
+#### Why do the official pgaudit docs differ from the Supabase one?
+
+pgaudit allows settings to be applied at 3 different database scopes:
+
+| Scope | Definition | Configuration File/Command |
+| -------- | ------------------ | -------------------------- |
+| System | Entire server | ALTER SYSTEM commands |
+| Database | Specific database | ALTER DATABASE commands |
+| Role | Specific user/role | ALTER ROLE commands |
+
+Supabase limits full privileges for file system and database variables, meaning pgaudit modifications can only occur at the ROLE level. Assigning pgaudit to the 'postgres' role provides it with the role's database visibility, allowing `ROLE` level adjustments a practical alternative to DATABASE/SYSTEM level configurations.
+
+pgaudit's [official documentation](https://www.pgaudit.org) focuses on system and database level configs, but its docs officially supports `ROLE` level configs, too:
## Resources
- Official [`PGAudit` documentation](https://www.pgaudit.org)
+- [Database Function Logging](https://supabase.com/docs/guides/database/functions#general-logging)
+- [Supabase Logging](https://supabase.com/docs/guides/platform/logs)
+- [Self-Hosting Logs](https://supabase.com/docs/reference/self-hosting-analytics/introduction)
diff --git a/apps/docs/content/guides/database/extensions/pgjwt.mdx b/apps/docs/content/guides/database/extensions/pgjwt.mdx
index 3b4fcc23aca..7d8c71069d1 100644
--- a/apps/docs/content/guides/database/extensions/pgjwt.mdx
+++ b/apps/docs/content/guides/database/extensions/pgjwt.mdx
@@ -4,7 +4,7 @@ title: 'pgjwt: JSON Web Tokens'
description: 'Encode and decode JWTs in PostgreSQL'
---
-The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
+The [`pgjwt`](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
## Enable the extension
@@ -19,7 +19,7 @@ The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extens
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "pgjwt" and enable the extension.
+3. Search for `pgjwt` and enable the extension.
@@ -33,7 +33,7 @@ create extension pgjwt schema extensions;
drop extension if exists pgjwt;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -67,7 +67,7 @@ select
);
```
-The pgjwt_encode function returns a string that represents the JWT, which can then be safely transmitted between parties.
+The `pgjwt_encode` function returns a string that represents the JWT, which can then be safely transmitted between parties.
{/* prettier-ignore */}
```
diff --git a/apps/docs/content/guides/database/extensions/pgroonga.mdx b/apps/docs/content/guides/database/extensions/pgroonga.mdx
index dbdbbe423b5..e3728d85ed2 100644
--- a/apps/docs/content/guides/database/extensions/pgroonga.mdx
+++ b/apps/docs/content/guides/database/extensions/pgroonga.mdx
@@ -20,7 +20,7 @@ tocVideo: 'Mmmv9g_MiBA'
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "pgroonga" and enable the extension.
+3. Search for `pgroonga` and enable the extension.
@@ -34,7 +34,7 @@ create extension pgroonga with schema extensions;
drop extension if exists pgroonga;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
@@ -92,7 +92,7 @@ Index Scan using ix_memos_content on memos (cost=0.00..1.11 rows=1 width=36)
(2 rows)
```
-The pgroonga index is used to retrieve the result set:
+The `pgroonga` index is used to retrieve the result set:
```markdown
| id | content |
@@ -102,7 +102,7 @@ The pgroonga index is used to retrieve the result set:
## Full text search
-The `&@~` operator performs full text search. It returns any matching results. Unlike `LIKE` operator, pgroonga can search any text that contains the keyword case insensitive.
+The `&@~` operator performs full text search. It returns any matching results. Unlike `LIKE` operator, `pgroonga` can search any text that contains the keyword case insensitive.
Take the following example:
diff --git a/apps/docs/content/guides/database/extensions/pgrouting.mdx b/apps/docs/content/guides/database/extensions/pgrouting.mdx
index c257613decc..083b19df884 100644
--- a/apps/docs/content/guides/database/extensions/pgrouting.mdx
+++ b/apps/docs/content/guides/database/extensions/pgrouting.mdx
@@ -4,7 +4,7 @@ title: 'pgrouting: Geospatial Routing'
description: 'Extends PostGIS with Geospatial Routing'
---
-[pgRouting](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
+[`pgRouting`](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
The core functionality of `pgRouting` is a set of path finding algorithms including:
@@ -33,7 +33,7 @@ The core functionality of `pgRouting` is a set of path finding algorithms includ
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "pgrouting" and enable the extension.
+3. Search for `pgrouting` and enable the extension.
@@ -47,7 +47,7 @@ create extension pgrouting cascade;
drop extension if exists pgRouting;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
@@ -55,9 +55,9 @@ To disable an extension you can call `drop extension`.
## Example
-As an example, we'll solve the [traveling salesman problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the pgRouting's `pgr_TSPeuclidean` function from some PostGIS coordinates.
+As an example, we'll solve the [traveling salesperson problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the `pgRouting`'s `pgr_TSPeuclidean` function from some PostGIS coordinates.
-A summary of the traveling salesman problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
+A summary of the traveling salesperson problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
First we populate a table with some X, Y coordinates
diff --git a/apps/docs/content/guides/database/extensions/pgsodium.mdx b/apps/docs/content/guides/database/extensions/pgsodium.mdx
index 4c68884799d..8c139f9ef80 100644
--- a/apps/docs/content/guides/database/extensions/pgsodium.mdx
+++ b/apps/docs/content/guides/database/extensions/pgsodium.mdx
@@ -4,13 +4,13 @@ title: 'pgsodium: Encryption Features'
description: 'Encryption library for PostgreSQL'
---
-[pgsodium](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to [libsodium's](https://doc.libsodium.org/) high-level cryptographic algorithms.
+[`pgsodium`](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to [`libsodium`'s](https://doc.libsodium.org/) high-level cryptographic algorithms.
-libsodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing, and more. It is a portable, cross-compilable, installable, and packageable fork of the [NaCl](http://nacl.cr.yp.to/) library, with a compatible but extended API to improve usability even further.
+`libsodium` is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing, and more. It is a portable, cross-compilable, installable, and packageable fork of the [NaCl](http://nacl.cr.yp.to/) library, with a compatible but extended API to improve usability even further.
The design choices emphasize security and ease of use. But despite the emphasis on high security, primitives are faster across-the-board than most implementations.
-pgsodium exposes the following libsodium APIs to SQL:
+`pgsodium` exposes the following `libsodium` APIs to SQL:
- [Generating Random Data](https://github.com/michelp/pgsodium/#generating-random-data)
- [Secret key cryptography](https://github.com/michelp/pgsodium/#secret-key-cryptography)
@@ -53,7 +53,7 @@ Note that Supabase projects are already encrypted at rest by default.
## Get the root encryption key for your Supabase project
-Encryption requires keys. Keeping the keys in the same database as the encrypted data would be unsafe. For more information about managing the pgsodium root encryption key on your Supabase project see **[encryption key location](/docs/guides/database/vault#encryption-key-location)**. This key is required to decrypt values stored in [Supabase Vault](/docs/guides/database/vault) and data encrypted with Transparent Column Encryption.
+Encryption requires keys. Keeping the keys in the same database as the encrypted data would be unsafe. For more information about managing the `pgsodium` root encryption key on your Supabase project see **[encryption key location](/docs/guides/database/vault#encryption-key-location)**. This key is required to decrypt values stored in [Supabase Vault](/docs/guides/database/vault) and data encrypted with Transparent Column Encryption.
## Enable the extension
@@ -68,7 +68,7 @@ Encryption requires keys. Keeping the keys in the same database as the encrypted
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "pgsodium" and enable the extension.
+3. Search for `pgsodium` and enable the extension.
diff --git a/apps/docs/content/guides/database/extensions/pgtap.mdx b/apps/docs/content/guides/database/extensions/pgtap.mdx
index 8b00e4c1bbb..bbcd7c3b125 100644
--- a/apps/docs/content/guides/database/extensions/pgtap.mdx
+++ b/apps/docs/content/guides/database/extensions/pgtap.mdx
@@ -26,7 +26,7 @@ Let's cover some basic concepts:
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
-3. Search for "pgtap" and enable the extension.
+3. Search for `pgtap` and enable the extension.
@@ -39,7 +39,7 @@ create extension pgtap with schema extensions;
drop extension if exists pgtap;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -149,7 +149,7 @@ rollback;
API:
- [`function_returns()`](https://pgtap.org/documentation.html#function_returns): Tests that a particular function returns a particular data type
-- [`is_definer()`](https://pgtap.org/documentation.html#is_definer): Tests that a function is a security definer (i.e., a “setuid” function).
+- [`is_definer()`](https://pgtap.org/documentation.html#is_definer): Tests that a function is a security definer (that is, a `setuid` function).
## Resources
diff --git a/apps/docs/content/guides/database/extensions/plv8.mdx b/apps/docs/content/guides/database/extensions/plv8.mdx
index 30ac72e01da..a27592d1270 100644
--- a/apps/docs/content/guides/database/extensions/plv8.mdx
+++ b/apps/docs/content/guides/database/extensions/plv8.mdx
@@ -8,7 +8,7 @@ The `plv8` extension allows you use JavaScript within Postgres.
## Overview
-While Postgres natively runs SQL, it can also run other "procedural languages".
+While Postgres natively runs SQL, it can also run other procedural languages.
`plv8` allows you to run JavaScript code - specifically any code that runs on the [V8 JavaScript engine](https://v8.dev).
It can be used for database functions, triggers, queries and more.
@@ -39,7 +39,7 @@ create extension plv8;
drop extension if exists plv8;
```
-Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
+Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension, call `drop extension`.
Procedural languages are automatically installed within `pg_catalog`, so you don't need to specify a schema.
diff --git a/apps/docs/content/guides/database/full-text-search.mdx b/apps/docs/content/guides/database/full-text-search.mdx
index c8511c962ab..fde0c4b5b60 100644
--- a/apps/docs/content/guides/database/full-text-search.mdx
+++ b/apps/docs/content/guides/database/full-text-search.mdx
@@ -30,6 +30,8 @@ For this guide we'll use the following example data:
>
+{/* */}
+
| id | title | author | description |
| --- | ----------------------------------- | ---------------------- | ------------------------------------------------------------------ |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
@@ -38,6 +40,8 @@ For this guide we'll use the following example data:
| 4 | Green Eggs and Ham | Dr. Seuss | Sam has changing food preferences and eats unusually colored food. |
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
+{/* */}
+
@@ -80,7 +84,7 @@ The functions we'll cover in this guide are:
### `to_tsvector()` [#to-tsvector]
-Converts your data into searchable "tokens". `to_tsvector()` stands for "to text search vector". For example:
+Converts your data into searchable tokens. `to_tsvector()` stands for "to text search vector." For example:
```sql
select to_tsvector('green eggs and ham');
@@ -91,10 +95,10 @@ Collectively these tokens are called a "document" which Postgres can use for com
### `to_tsquery()` [#to-tsquery]
-Converts a query string into "tokens" to match. `to_tsquery()` stands for "to text search query".
+Converts a query string into tokens to match. `to_tsquery()` stands for "to text search query."
This conversion step is important because we will want to "fuzzy match" on keywords.
-For example if a user searches for "eggs", and a column has the value "egg", we probably still want to return a match.
+For example if a user searches for `eggs`, and a column has the value `egg`, we probably still want to return a match.
### Match: `@@` [#match]
@@ -253,11 +257,15 @@ val data = supabase.from("books").select {
+{/* */}
+
| id | title | author | description |
| --- | ----------------------------------- | ----------------- | ----------------------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
+{/* */}
+
@@ -338,11 +346,15 @@ val data = supabase.from("books").select {
+{/* */}
+
| id | title | author | description |
| --- | --------------------- | ---------------------- | ------------------------------------------- |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
+{/* */}
+
@@ -403,10 +415,14 @@ val data = supabase.from("books").select {
+{/* */}
+
| id | title | author | description |
| --- | ------ | ----------------- | -------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
+{/* */}
+
@@ -467,11 +483,15 @@ val data = supabase.from("books").select {
+{/* */}
+
| id | title | author | description |
| --- | --------------------- | ---------------------- | ------------------------------------------- |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
+{/* */}
+
@@ -514,6 +534,8 @@ from books;
+{/* */}
+
| id | fts |
| --- | --------------------------------------------------------------------------------------------------------------- |
| 1 | 'anim':7 'bigger':6 'littl':10 'poki':9 'puppi':1,11 'slower':3 |
@@ -522,6 +544,8 @@ from books;
| 4 | 'chang':3 'color':9 'eat':7 'egg':12 'food':4,10 'green':11 'ham':14 'prefer':5 'sam':1 'unus':8 |
| 5 | 'big':6 'drama':7 'ensu':8 'fire':15 'fourth':1 'goblet':13 'harri':9 'potter':10 'school':4 'start':5 'year':2 |
+{/* */}
+
@@ -578,10 +602,14 @@ val data = supabase.from("books").select {
+{/* */}
+
| id | title | author | description | fts |
| --- | ------ | ----------------- | -------------------------------- | ------------------------------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. | 'big':5 'dream':6 'littl':1 'tootl':7 'toy':2 'train':3 |
+{/* */}
+
diff --git a/apps/docs/content/guides/database/functions.mdx b/apps/docs/content/guides/database/functions.mdx
index 13a902b532e..c3f9ecb900c 100644
--- a/apps/docs/content/guides/database/functions.mdx
+++ b/apps/docs/content/guides/database/functions.mdx
@@ -336,6 +336,174 @@ GRANT EXECUTE ON FUNCTION hello_world TO authenticated;
GRANT EXECUTE ON FUNCTION hello_world TO service_role;
```
+### Debugging functions
+
+You can add logs to help you debug functions. This is especially recommended for complex functions.
+
+Good targets to log include:
+
+- Values of (non-sensitive) variables
+- Returned results from queries
+
+#### General logging
+
+To create custom logs in the [Dashboard's Postgres Logs](/apps/docs/public/img/database/postgres-logs.png), you can use the `raise` keyword. By default, there are 3 observed severity levels:
+
+- `log`
+- `warning`
+- `exception` (error level)
+
+```sql
+create function logging_example(
+ log_message text,
+ warning_message text,
+ error_message text
+)
+returns void
+language plpgsql
+as $$
+begin
+ raise log 'logging message: %', log_message;
+ raise warning 'logging warning: %', warning_message;
+
+ -- immediately ends function and reverts transaction
+ raise exception 'logging error: %', error_message;
+end;
+$$;
+
+select logging_example('LOGGED MESSAGE', 'WARNING MESSAGE', 'ERROR MESSAGE');
+```
+
+#### Error handling
+
+You can create custom errors with the `raise exception` keywords.
+
+A common pattern is to throw an error when a variable doesn't meet a condition:
+
+```sql
+create or replace function error_if_null(some_val text)
+returns text
+language plpgsql
+as $$
+begin
+ -- error if some_val is null
+ if some_val is null then
+ raise exception 'some_val should not be NULL';
+ end if;
+ -- return some_val if it is not null
+ return some_val;
+end;
+$$;
+
+select error_if_null(null);
+```
+
+Value checking is common, so Postgres provides a shorthand: the `assert` keyword. It uses the following format:
+
+```sql
+-- throw error when condition is false
+assert , 'message';
+```
+
+Below is an example
+
+```sql
+create function assert_example(name text)
+returns uuid
+language plpgsql
+as $$
+declare
+ student_id uuid;
+begin
+ -- save a user's id into the user_id variable
+ select
+ id into student_id
+ from attendance_table
+ where student = name;
+
+ -- throw an error if the student_id is null
+ assert student_id is not null, 'assert_example() ERROR: student not found';
+
+ -- otherwise, return the user's id
+ return student_id;
+end;
+$$;
+
+select assert_example('Harry Potter');
+```
+
+Error messages can also be captured and modified with the `exception` keyword:
+
+```sql
+create function error_example()
+returns void
+language plpgsql
+as $$
+begin
+ -- fails: cannot read from nonexistent table
+ select * from table_that_does_not_exist;
+
+ exception
+ when others then
+ raise exception 'An error occurred in function : %', sqlerrm;
+end;
+$$;
+```
+
+#### Advanced logging
+
+For more complex functions or complicated debugging, try logging:
+
+- Formatted variables
+- Individual rows
+- Start and end of function calls
+
+```sql
+create or replace function advanced_example(num int default 10)
+returns text
+language plpgsql
+as $$
+declare
+ var1 int := 20;
+ var2 text;
+begin
+ -- Logging start of function
+ raise log 'logging start of function call: (%)', (select now());
+
+ -- Logging a variable from a SELECT query
+ select
+ col_1 into var1
+ from some_table
+ limit 1;
+ raise log 'logging a variable (%)', var1;
+
+ -- It is also possible to avoid using variables, by returning the values of your query to the log
+ raise log 'logging a query with a single return value(%)', (select col_1 from some_table limit 1);
+
+ -- If necessary, you can even log an entire row as JSON
+ raise log 'logging an entire row as JSON (%)', (select to_jsonb(some_table.*) from some_table limit 1);
+
+ -- When using INSERT or UPDATE, the new value(s) can be returned
+ -- into a variable.
+ -- When using DELETE, the deleted value(s) can be returned.
+ -- All three operations use "RETURNING value(s) INTO variable(s)" syntax
+ insert into some_table (col_2)
+ values ('new val')
+ returning col_2 into var2;
+
+ raise log 'logging a value from an INSERT (%)', var2;
+
+ return var1 || ',' || var2;
+exception
+ -- Handle exceptions here if needed
+ when others then
+ raise exception 'An error occurred in function : %', sqlerrm;
+end;
+$$;
+
+select advanced_example();
+```
+
## Resources
- Official Client libraries: [JavaScript](../../reference/javascript/rpc) and [Flutter](../../reference/dart/rpc)
diff --git a/apps/docs/content/guides/database/joins-and-nesting.mdx b/apps/docs/content/guides/database/joins-and-nesting.mdx
index f19646bcd9a..997dc5935f7 100644
--- a/apps/docs/content/guides/database/joins-and-nesting.mdx
+++ b/apps/docs/content/guides/database/joins-and-nesting.mdx
@@ -260,3 +260,60 @@ GET https://[REF].supabase.co/rest/v1/teams?select=id,team_name,users(id,name)
+
+## Specifying the `ON` clause for joins with multiple foreign keys
+
+For example, if you have a project that tracks when employees check in and out of work shifts:
+
+```sql
+-- Employees
+create table users (
+ "id" serial primary key,
+ "name" text
+);
+
+-- Badge scans
+create table scans (
+ "id" serial primary key,
+ "user_id" int references users,
+ "badge_scan_time" timestamp
+);
+
+-- Work shifts
+create table shifts (
+ "id" serial primary key,
+ "user_id" int references users,
+ "scan_id_start" int references scans, -- clocking in
+ "scan_id_end" int references scans, -- clocking out
+ "attendance_status" text
+);
+```
+
+In this case, you need to explicitly define the join because the joining column on `shifts` is ambiguous.
+
+To fetch all the `shifts` with `scan_id_start` related to a specific `scan`, use the following syntax:
+
+
+
+
+```js
+const { data, error } = await supabase.from('shifts').select(
+ `
+ *,
+ scans:scan_id_start (
+ id,
+ user_id,
+ badge_scan_time
+ )
+ `
+)
+```
+
+
+
diff --git a/apps/docs/content/guides/database/managing-timezones.mdx b/apps/docs/content/guides/database/managing-timezones.mdx
index 2b3ab247ae6..d74a40bf348 100644
--- a/apps/docs/content/guides/database/managing-timezones.mdx
+++ b/apps/docs/content/guides/database/managing-timezones.mdx
@@ -6,7 +6,7 @@ description: 'How to change your database timezone.'
---
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
-This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
+This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time."
### Change timezone
diff --git a/apps/docs/content/guides/database/postgres/cascade-deletes.mdx b/apps/docs/content/guides/database/postgres/cascade-deletes.mdx
index 8f3b223a120..cad764f94dd 100644
--- a/apps/docs/content/guides/database/postgres/cascade-deletes.mdx
+++ b/apps/docs/content/guides/database/postgres/cascade-deletes.mdx
@@ -6,10 +6,10 @@ footerHelpType: 'postgres'
There are 5 options for foreign key constraint deletes:
-1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child table(s) are deleted as well.
-2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child table(s).
-3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to NULL.
-4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to their default values.
+1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child tables are deleted as well.
+2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child tables.
+3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child tables are set to NULL.
+4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child tables are set to their default values.
5. **NO ACTION:** This option is similar to RESTRICT, but it also has the option to be “deferred” to the end of a transaction. This means that other cascading deletes can run first, and then this delete constraint will only throw an error if there is referenced data remaining _at the end of the transaction_.
These options can be specified when defining a foreign key constraint using the "ON DELETE" clause. For example, the following SQL statement creates a foreign key constraint with the `CASCADE` option:
@@ -20,7 +20,7 @@ add constraint fk_parent foreign key (parent_id) references parent_table (id)
on delete cascade;
```
-This means that when a row is deleted from the "parent_table", all related rows in the "child_table" will be deleted as well.
+This means that when a row is deleted from the `parent_table`, all related rows in the `child_table` will be deleted as well.
## `RESTRICT` vs `NO ACTION`
@@ -28,11 +28,11 @@ The difference between `NO ACTION` and `RESTRICT` is subtle and can be a bit con
Both `NO ACTION` and `RESTRICT` are used to prevent deletion of a row in a parent table if there are related rows in a child table. However, there is a subtle difference in how they behave.
-When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced table(s).
+When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced tables.
When a foreign key constraint is defined with the option `NO ACTION`, it means that if a row in the parent table is deleted, the database will also raise an error and prevent the deletion of the row in the parent table. However unlike `RESTRICT`, `NO ACTION` has the option defer the check using `INITIALLY DEFERRED`. This will only raise the above error _if_ the referenced rows still exist at the end of the transaction.
-The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced table(s).
+The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced tables.
In practice, you can use either `NO ACTION` or `RESTRICT` depending on your needs. `NO ACTION` is the default behavior if you do not specify anything. If you prefer to defer the check until the end of the transaction, use `NO ACTION INITIALLY DEFERRED`.
@@ -48,10 +48,10 @@ Let's further illustrate the difference with an example. We'll use the following
`parent`
-| id | name | parent_id |
-| --- | ------- | --------- |
-| 1 | Charles | 1 |
-| 2 | Diana | 1 |
+| id | name | `parent_id` |
+| --- | ------- | ----------- |
+| 1 | Charles | 1 |
+| 2 | Diana | 1 |
`child`
diff --git a/apps/docs/content/guides/database/postgres/configuration.mdx b/apps/docs/content/guides/database/postgres/configuration.mdx
index f9177a0a251..087176ba1d2 100644
--- a/apps/docs/content/guides/database/postgres/configuration.mdx
+++ b/apps/docs/content/guides/database/postgres/configuration.mdx
@@ -36,7 +36,7 @@ This data can further be used in conjunction with the [`explain`](https://www.po
## Managing timezones
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
-This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
+This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time."
### Change timezone
diff --git a/apps/docs/content/guides/database/postgres/roles.mdx b/apps/docs/content/guides/database/postgres/roles.mdx
index 257d777cd2d..41d8d6f7577 100644
--- a/apps/docs/content/guides/database/postgres/roles.mdx
+++ b/apps/docs/content/guides/database/postgres/roles.mdx
@@ -5,7 +5,7 @@ description: 'Managing access to your Postgres database and configuring permissi
subtitle: 'Managing access to your Postgres database and configuring permissions.'
---
-Postgres manages database access permissions using the concept of roles. Generally you wouldn't use these roles for your own application - they are mostly for configuring _system access_ to your database. If you want to configure _application access_, then you should use [Row Level Security](/docs/guides/database/postgres/row-level-security).
+Postgres manages database access permissions using the concept of roles. Generally you wouldn't use these roles for your own application - they are mostly for configuring _system access_ to your database. If you want to configure _application access_, then you should use [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS). You can also implement [Role-based Access Control](/docs/guides/auth/custom-claims-and-role-based-access-control-rbac) on top of RLS.
## Users vs roles
@@ -38,11 +38,11 @@ Your Postgres database is the core of your Supabase project, so it's important t
### Special symbols in passwords
-If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
+If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string, for example, `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
### Changing your project password
-When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to 3rd-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
+When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to third-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
## Granting permissions
@@ -86,7 +86,7 @@ The default Postgres role. This has admin privileges.
### `anon`
-For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
+For unauthenticated, public access. This is the role which the API (PostgREST) will use when a user _is not_ logged in.
### `authenticator`
@@ -95,7 +95,7 @@ A special role for the API (PostgREST). It has very limited access, and is used
### `authenticated`
-For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
+For "authenticated access." This is the role which the API (PostgREST) will use when a user _is_ logged in.
### `service_role`
diff --git a/apps/docs/content/guides/database/postgres/row-level-security.mdx b/apps/docs/content/guides/database/postgres/row-level-security.mdx
index 286391079de..6d7450c3407 100644
--- a/apps/docs/content/guides/database/postgres/row-level-security.mdx
+++ b/apps/docs/content/guides/database/postgres/row-level-security.mdx
@@ -11,7 +11,7 @@ When you need granular authorization rules, nothing beats Postgres's [Row Level
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/auth/row-level-security) for end-to-end user security from the browser to the database.
-RLS is a Postgres primitive and can provide "[defense in depth]()" to protect your data from malicious actors even when accessed through 3rd party tooling.
+RLS is a Postgres primitive and can provide "[defense in depth]()" to protect your data from malicious actors even when accessed through third-party tooling.
## Policies
@@ -219,7 +219,7 @@ to authenticated
using ( (select auth.uid()) = user_id );
```
-This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an initPlan to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
+This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
@@ -239,7 +239,7 @@ You can only use this technique if the results of the query or function do not c
### Add filters to every query
-Policies are "implicit where clauses", so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):
+Policies are "implicit where clauses," so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):
{/* prettier-ignore */}
```js
diff --git a/apps/docs/content/guides/database/postgres/triggers.mdx b/apps/docs/content/guides/database/postgres/triggers.mdx
index 21db2217f59..e405dfff549 100644
--- a/apps/docs/content/guides/database/postgres/triggers.mdx
+++ b/apps/docs/content/guides/database/postgres/triggers.mdx
@@ -99,7 +99,7 @@ execute function after_delete_function();
There are two options available for executing triggers:
- `for each row`: specifies that the trigger function should be executed once for each affected row.
-- `for each statement`: the trigger is executed once for the entire operation (eg, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
+- `for each statement`: the trigger is executed once for the entire operation (for example, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
## Dropping a trigger
diff --git a/apps/docs/content/guides/database/query-optimization.mdx b/apps/docs/content/guides/database/query-optimization.mdx
index 7602ae4603e..bf3c2f169b3 100644
--- a/apps/docs/content/guides/database/query-optimization.mdx
+++ b/apps/docs/content/guides/database/query-optimization.mdx
@@ -14,7 +14,7 @@ This guide is intended to:
This is not a comprehensive resource, but rather a helpful starting point for your optimization journey.
-If you're new to query optimization, you may be interested in [index_advisor](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query.
+If you're new to query optimization, you may be interested in [`index_advisor`](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query.
## Example query
@@ -66,7 +66,7 @@ create index idx_orders_customer_id on orders (customer_id);
Sorting can also be optimized by indexing:
-- An index on `b.date_of_purchase` wcan improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause.
+- An index on `b.date_of_purchase` can improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause.
```sql
create index idx_orders_date_of_purchase on orders (date_of_purchase);
diff --git a/apps/docs/content/guides/database/testing.mdx b/apps/docs/content/guides/database/testing.mdx
index e7d95cca60f..f3d7f5e75d8 100644
--- a/apps/docs/content/guides/database/testing.mdx
+++ b/apps/docs/content/guides/database/testing.mdx
@@ -63,6 +63,6 @@ Result: PASS
## More resources
-- [Testing RLS policies](/docs/guides/auth/row-level-security#testing-policies)
+- [Testing RLS policies](/docs/guides/database/extensions/pgtap#testing-rls-policies)
- [pgTAP extension](/docs/guides/database/extensions/pgtap)
- Official [pgTAP documentation](https://pgtap.org/)
diff --git a/apps/docs/content/guides/database/vault.mdx b/apps/docs/content/guides/database/vault.mdx
index a16f814633d..12aa4cc193b 100644
--- a/apps/docs/content/guides/database/vault.mdx
+++ b/apps/docs/content/guides/database/vault.mdx
@@ -79,7 +79,7 @@ updated_at | 2022-12-14 02:34:23.85159+00
-Alternatively, you can create a secret by `insert`ing data into the `vault.secret` table:
+Alternatively, you can create a secret by inserting data into the `vault.secret` table:
{/* prettier-ignore */}
```sql
@@ -206,11 +206,11 @@ updated_at | 2022-12-14 02:51:13.938396+00
>
-As we mentioned, the Vault uses pgsodium's Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
+As we mentioned, the Vault uses `pgsodium`'s Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
### Authenticated encryption with associated data
-The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data](