mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Closes DOCS-1319 Part 4 of 4 in stack #50823. This PR carries **additions**: content the page never had. Style, structure, and snippet fixes land below it in #50820, #50821, and #50822. ## Problem Developers and AI agents read the Database functions guide. The guide shows how to write a function inside the database. A function runs in one of two modes. In `invoker` mode it runs as the caller. In `definer` mode it runs as the creator. The guide gives one rule for `definer` mode. The rule is to set the `search_path`. A reader who obeys that rule still gets an unsafe function. I wrote a function that obeys the rule. I pinned the search path to the empty string. Then I called it three times. | Caller | Result | | --- | --- | | The order's owner | 4330 cents, correct | | A different signed-in customer | 8660 cents, another customer's order | | Nobody, no session at all | 8660 cents | Every role can call a new function in `public`. The guide states that fact under Function privileges. It never connects the fact to the `definer` rule. A reader has no reason to look. Customers report the same failure. AI tools choose `definer` mode. The function then answers the front end with no session. The hole is hard to find, because no policy is involved in it. ## Solution - **Joins the two halves in the Security definer subsection.** A `danger` admonition states three facts: - The function runs with its creator's privileges. - A function created in the Dashboard or by a migration is owned by `postgres`, which bypasses Row Level Security. - Every role can call the function by default. The admonition then gives the fix. Check ownership inside the function body, and narrow the execute privilege as well. - **Applies the regrant to both ways of restricting execute.** The `grant execute` block sat inside the second way. A reader who took the first way saw no way to restore their own app's access. **Not changed:** the existing definer paragraph, the page structure, and the Function privileges statements. The lower PRs in the stack own those. **No eval re-run.** The guide scored 6 of 6 on three baseline runs, so the score has no room to move. All three runs chose `invoker` mode. The eval never enters the branch this PR fixes. Measuring it needs a new check, not a re-run. **Diff size:** one file, 15 lines added and 4 removed. ## Manual testing 1. Open the [Security definer vs invoker section](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions#security-definer-vs-invoker) on the deploy preview. The admonition renders as a red `danger` panel below the definer paragraph. The two fixes appear as bullets. 2. Click the `Function privileges` link inside the admonition. It jumps to the [Function privileges section](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions#function-privileges) on the same page. 3. Read that section. The `grant execute` block sits after the numbered list. It applies to both ways of restricting execute. 4. Run `pnpm build:guides-markdown` from `apps/docs`. Read `apps/docs/public/markdown/guides/database/functions.md`. The admonition appears as a `Danger:` paragraph. Discard the `manifest.json` change. 5. Run `npx prettier --check apps/docs/content/guides/database/functions.mdx`. Clean. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Expanded guidance on the security risks of `security definer` functions, including their privileges, interaction with Row Level Security, and default execution access. * Added recommendations for checking data ownership and restricting execution to intended roles. * Clarified that pinning `search_path` does not limit execution privileges. * Presented the function-privileges example separately from the default-privileges instructions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> ## Preview links | Site | Live | Preview | Search for | | --- | --- | --- | --- | | Docs | [/docs/guides/database/functions](https://supabase.com/docs/guides/database/functions) | [/docs/guides/database/functions](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions) | `every signed-in caller` | ## Review instructions This PR adds one admonition and moves one code block. The question is whether the admonition is correct and whether an agent reading the page would act on it. 1. Open the preview at [Security definer vs invoker](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions#security-definer-vs-invoker). A red `danger` panel sits below the definer paragraph. 2. **Read the first paragraph for accuracy.** It claims the function runs with its creator's privileges, that a function created in the Dashboard or by a migration is owned by `postgres`, and that `postgres` bypasses Row Level Security. This matches [Use security definer functions](https://supabase.com/docs/guides/database/postgres/row-level-security#use-security-definer-functions) in the RLS guide. Flag any drift. 3. **Read the two bullets for sufficiency.** The ownership check is the primary fix. The execute grant is listed as a complement, not an alternative, because granting to `authenticated` still returns any user's row to every signed-in caller. Confirm the wording can't be read as "either one is enough." 4. Click the `Function privileges` link inside the admonition. It jumps down the same page. 5. In the Function privileges section, confirm the `grant execute` block sits after the numbered list rather than inside item 2, so it applies to both ways of restricting execute. 6. **Check the agent-facing copy.** Open [the markdown export](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions.md) and find `Danger:`. This is what an agent reads, and it is the audience this PR exists for. **If you only have two minutes:** do steps 3 and 6. Step 3 is the correctness of the advice. Step 6 is whether the audience that prompted the ticket actually receives it. **A note on running SQL from this page.** Don't hand-paste from the rendered page. Blocks are split across tabs, and the Data tab in Returning data sets holds markdown tables that look pasteable but are not SQL. Use the `.md` export of the page, which flattens every tab in page order. #50822 has a copy-paste command for this.
508 lines
13 KiB
Plaintext
508 lines
13 KiB
Plaintext
---
|
|
id: 'functions'
|
|
title: 'Database functions'
|
|
description: 'Creating and using Postgres functions.'
|
|
video: 'https://www.youtube.com/v/MJZCCpCYEqk'
|
|
---
|
|
|
|
Postgres has built-in support for [SQL functions](https://www.postgresql.org/docs/current/sql-createfunction.html).
|
|
These functions live inside your database, and you can call them from your app with [`rpc()`](../../reference/javascript/rpc).
|
|
|
|
- [Database functions vs Edge Functions](#database-functions-vs-edge-functions) compares the two. Start here if you aren't sure which one fits.
|
|
- [Create a database function](#create-a-database-function) has the steps, from a one-line function to one that takes parameters.
|
|
- [Secure a database function](#secure-a-database-function) covers which user the function runs as and who can call it.
|
|
- [Debugging database functions](/docs/guides/database/debugging-functions) covers logging and error handling.
|
|
|
|
## Quick demo
|
|
|
|
<YouTube id="MJZCCpCYEqk" title="Creating and calling Postgres functions" />
|
|
|
|
## Database functions vs Edge Functions
|
|
|
|
For data-intensive operations, use database functions. They run inside your database, and you can call them remotely with the [REST and GraphQL API](../api).
|
|
|
|
For use cases that need low latency, use [Edge Functions](../../guides/functions). They're globally distributed and you write them in TypeScript.
|
|
|
|
## Create a database function
|
|
|
|
### Getting started
|
|
|
|
Create a database function from the Dashboard, or write the SQL yourself against a
|
|
[direct connection](../../guides/database/connecting-to-postgres).
|
|
|
|
To use the Dashboard:
|
|
|
|
1. Go to the **SQL Editor** section.
|
|
2. Click **New Query**.
|
|
3. Enter the SQL that creates or replaces your database function.
|
|
4. Click **Run**. You can also press `cmd+enter` or `ctrl+enter`.
|
|
|
|
### Basic functions [#simple-functions]
|
|
|
|
Create a basic database function that returns the string "hello world".
|
|
|
|
```sql
|
|
create or replace function hello_world() -- 1
|
|
returns text -- 2
|
|
language sql -- 3
|
|
as $$ -- 4
|
|
select 'hello world'; -- 5
|
|
$$; --6
|
|
|
|
```
|
|
|
|
<details>
|
|
<summary>Show/Hide Details</summary>
|
|
|
|
At its most basic, a function has the following parts:
|
|
|
|
1. `create or replace function hello_world()`: The function declaration, where `hello_world` is the name of the function. Use `create` for a new function, `replace` for one that exists, or `create or replace` when the function might not exist yet.
|
|
2. `returns text`: The type of data the function returns. For a function that returns nothing, write `returns void`.
|
|
3. `language sql`: The language used inside the function body. This can also be a procedural language: `plpgsql`, `plpython`, etc.
|
|
4. `as $$`: The function wrapper. Anything inside the `$$` symbols is part of the function body.
|
|
5. `select 'hello world';`: A basic function body. The function returns the result of its final query, which can be an `insert`, `update`, or `delete` with a `returning` clause.
|
|
6. `$$;`: The closing symbols of the function wrapper.
|
|
|
|
</details>
|
|
|
|
<Admonition type="caution">
|
|
|
|
Overloaded functions aren't supported. Give every function a unique name.
|
|
|
|
</Admonition>
|
|
|
|
After you create the function, you can run it inside the database with SQL, or with one of the client libraries.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
select hello_world();
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = await supabase.rpc('hello_world')
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/javascript/rpc)
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase
|
|
.rpc('hello_world');
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/dart/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
try await supabase.rpc("hello_world").execute()
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/swift/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc("hello_world")
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/kotlin/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
|
|
```python
|
|
data = supabase.rpc('hello_world').execute()
|
|
```
|
|
|
|
Reference: [`rpc()`](../../reference/python/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
await supabase.Rpc("hello_world", null);
|
|
```
|
|
|
|
Reference: [`Rpc()`](../../reference/csharp/rpc)
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
### Returning data sets
|
|
|
|
A database function can also return a data set from a [table](../../guides/database/tables) or a view.
|
|
|
|
For example, create two tables holding some Star Wars data. The **Data** tab shows the rows they end up with.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="example-view"
|
|
>
|
|
<TabPanel id="data" label="Data">
|
|
|
|
#### Planets
|
|
|
|
```
|
|
| id | name |
|
|
| --- | -------- |
|
|
| 1 | Tatooine |
|
|
| 2 | Alderaan |
|
|
| 3 | Kashyyyk |
|
|
```
|
|
|
|
#### People
|
|
|
|
```
|
|
| id | name | planet_id |
|
|
| --- | ---------------- | --------- |
|
|
| 1 | Anakin Skywalker | 1 |
|
|
| 2 | Luke Skywalker | 1 |
|
|
| 3 | Princess Leia | 2 |
|
|
| 4 | Chewbacca | 3 |
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
create table planets (
|
|
id serial primary key,
|
|
name text
|
|
);
|
|
|
|
insert into planets
|
|
(name)
|
|
values
|
|
('Tatooine'),
|
|
('Alderaan'),
|
|
('Kashyyyk');
|
|
|
|
create table people (
|
|
id serial primary key,
|
|
name text,
|
|
planet_id bigint references planets
|
|
);
|
|
|
|
insert into people
|
|
(name, planet_id)
|
|
values
|
|
('Anakin Skywalker', 1),
|
|
('Luke Skywalker', 1),
|
|
('Princess Leia', 2),
|
|
('Chewbacca', 3);
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
The following function returns all the planets:
|
|
|
|
```sql
|
|
create or replace function get_planets()
|
|
returns setof planets
|
|
language sql
|
|
as $$
|
|
select * from planets;
|
|
$$;
|
|
```
|
|
|
|
Because this function returns a table set, you can apply filters and selectors to it. To get the first planet only:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
select *
|
|
from get_planets()
|
|
where id = 1;
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = supabase.rpc('get_planets').eq('id', 1)
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase
|
|
.rpc('get_planets')
|
|
.eq('id', 1);
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
```swift
|
|
let response = try await supabase.rpc("get_planets").eq("id", value: 1).execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc("get_planets") {
|
|
filter {
|
|
eq("id", 1)
|
|
}
|
|
}
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
|
|
```python
|
|
data = supabase.rpc('get_planets').eq('id', 1).execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
### Passing parameters
|
|
|
|
Create a function that inserts a new planet into the `planets` table and returns the new ID. This function uses the `plpgsql` language.
|
|
|
|
```sql
|
|
create or replace function add_planet(name text)
|
|
returns bigint
|
|
language plpgsql
|
|
as $$
|
|
declare
|
|
new_row bigint;
|
|
begin
|
|
insert into planets(name)
|
|
values (add_planet.name)
|
|
returning id into new_row;
|
|
|
|
return new_row;
|
|
end;
|
|
$$;
|
|
```
|
|
|
|
You can run this function inside your database with a `select` query, or with the client libraries:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="sql"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="sql" label="SQL">
|
|
|
|
```sql
|
|
select * from add_planet('Jakku');
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="js" label="JavaScript">
|
|
|
|
```js
|
|
const { data, error } = await supabase.rpc('add_planet', { name: 'Jakku' })
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
|
|
```dart
|
|
final data = await supabase
|
|
.rpc('add_planet', params: { 'name': 'Jakku' });
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
|
|
Using `Encodable` type:
|
|
|
|
```swift
|
|
struct Planet: Encodable {
|
|
let name: String
|
|
}
|
|
|
|
try await supabase.rpc(
|
|
"add_planet",
|
|
params: Planet(name: "Jakku")
|
|
)
|
|
.execute()
|
|
```
|
|
|
|
Using `AnyJSON` convenience` type:
|
|
|
|
```swift
|
|
try await supabase.rpc(
|
|
"add_planet",
|
|
params: ["name": AnyJSON.string("Jakku")]
|
|
)
|
|
.execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
|
|
```kotlin
|
|
val data = supabase.postgrest.rpc(
|
|
function = "add_planet",
|
|
parameters = buildJsonObject { //You can put here any serializable object including your own classes
|
|
put("name", "Jakku")
|
|
}
|
|
)
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
|
|
```python
|
|
data = supabase.rpc('add_planet', params={'name': 'Jakku'}).execute()
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:csharp">
|
|
<TabPanel id="csharp" label="C#">
|
|
|
|
```c#
|
|
await supabase.Rpc("add_planet", new Dictionary<string, object> { { "name", "Jakku" } });
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
## Secure a database function
|
|
|
|
### Security `definer` vs `invoker`
|
|
|
|
Postgres runs a function either as the user _calling_ it (`invoker`) or as its _creator_ (`definer`). For example:
|
|
|
|
```sql
|
|
create or replace function hello_world()
|
|
returns text
|
|
language plpgsql
|
|
security definer set search_path = ''
|
|
as $$
|
|
begin
|
|
return 'hello world';
|
|
end;
|
|
$$;
|
|
```
|
|
|
|
Prefer `security invoker`, which is also the default. When you use `security definer`, you must set the `search_path`.
|
|
|
|
With an empty search path, `search_path = ''`, name the schema for every relation in the function body, such as `from public.table`. An empty search path limits the damage when the function can reach a schema you don't want the calling user to reach.
|
|
|
|
<Admonition type="danger">
|
|
|
|
A `security definer` function can return rows the caller isn't allowed to read. It runs with its creator's privileges. A function created in the Dashboard or by a migration is owned by `postgres`, which bypasses Row Level Security. Any role can call it by default, including `anon`, the role that serves requests carrying no user session.
|
|
|
|
Pinning the `search_path` doesn't change who can call the function. When a `security definer` function reads per-user data:
|
|
|
|
- **Check ownership in the function body.** An execute grant controls which roles can call the function. It doesn't limit the rows a permitted role gets back, so granting to `authenticated` still returns any user's row to every signed-in caller.
|
|
- **Narrow the execute privilege as well.** Revoke it from `public` and from each role holding a direct grant, such as `anon`. Then grant it to the roles that need it. See the [Function privileges](#function-privileges) section of this page.
|
|
|
|
</Admonition>
|
|
|
|
### Function privileges
|
|
|
|
By default, any role can run a database function. You can restrict execution in two ways:
|
|
|
|
1. Revoke on a case-by-case basis. Revoke execute for the functions you want to protect, from both `public` and the role you're restricting:
|
|
|
|
```sql
|
|
revoke execute on function public.hello_world from public;
|
|
revoke execute on function public.hello_world from anon;
|
|
```
|
|
|
|
1. Restrict execution by default, then grant access to the roles that need each function.
|
|
|
|
To restrict every function that exists, revoke execute from both `public` and the role you want to restrict:
|
|
|
|
```sql
|
|
revoke execute on all functions in schema public from public;
|
|
revoke execute on all functions in schema public from anon, authenticated;
|
|
```
|
|
|
|
To restrict every function created later, change the default privileges for both `public` and the role you want to restrict:
|
|
|
|
```sql
|
|
alter default privileges in schema public revoke execute on functions from public;
|
|
alter default privileges in schema public revoke execute on functions from anon, authenticated;
|
|
```
|
|
|
|
You can then regrant permissions for a specific function to a specific role:
|
|
|
|
```sql
|
|
grant execute on function public.hello_world to authenticated;
|
|
```
|
|
|
|
## Resources
|
|
|
|
- Official Client libraries: [JavaScript](../../reference/javascript/rpc) and [Flutter](../../reference/dart/rpc)
|
|
- Community client libraries: [github.com/supabase-community](https://github.com/supabase-community)
|
|
- Postgres Official Docs: [Chapter 9. Functions and Operators](https://www.postgresql.org/docs/current/functions.html)
|
|
- Postgres Reference: [CREATE FUNCTION](https://www.postgresql.org/docs/current/sql-createfunction.html)
|
|
|
|
### Create a database function in the Dashboard
|
|
|
|
<YouTube id="MJZCCpCYEqk" title="Creating a Postgres function in the dashboard" />
|
|
|
|
### Call a database function from JavaScript
|
|
|
|
<YouTube id="I6nnp9AINJk" title="Calling a Postgres function from JavaScript" />
|
|
|
|
### Call an external API from a database function
|
|
|
|
<YouTube id="rARgrELRCwY" title="Calling an external API from a Postgres function" />
|