Files
Miranda Limonczenko 17512de71d docs(database): connect security definer to the default execute grant (#50817)
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.
2026-10-05 15:18:01 -07:00

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" />