mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
All changes are related to Realtime permissions that I observed on support tickets recently: - Migrations can't have `ALTER TABLE realtime.messages`. That's is not allowed and breaks migrations. - Missing realtime.messages partitions are usually due to lack of connections - Errors like "must be owner of table messages" are misleading Closes REAL-1125 ## Additional context https://supabase.slack.com/archives/C01G8CC0X9D/p1789981286693829 and SU-479680 ## Checklist - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [x] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which references [WORD_LIST](https://github.com/supabase/supabase/blob/master/apps/docs/WORD_LIST.md) and the docs [CONTRIBUTING](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Clarified Realtime authorization restrictions, supported RLS policy management, and the existing RLS configuration for `realtime.messages`. * Documented broadcast partition creation, connection requirements, and warning behavior when partitions are unavailable. * Added troubleshooting guidance for ownership errors, including migration rollback implications and supported remedies. * Added instructions for inspecting message partitions and diagnosing missing, expired, or unavailable partition configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
418 lines
13 KiB
Plaintext
418 lines
13 KiB
Plaintext
---
|
|
id: 'authorization'
|
|
title: 'Realtime Authorization'
|
|
description: 'Authorization for Supabase Realtime'
|
|
sidebar_label: 'Authorization'
|
|
---
|
|
|
|
You can control client access to Realtime [Broadcast](/docs/guides/realtime/broadcast) and [Presence](/docs/guides/realtime/presence) by adding Row Level Security policies to the `realtime.messages` table. Each RLS policy can map to a specific action a client can take:
|
|
|
|
- Control which clients can broadcast to a Channel
|
|
- Control which clients can receive broadcasts from a Channel
|
|
- Control which clients can publish their presence to a Channel
|
|
- Control which clients can receive messages about the presence of other clients
|
|
|
|
<Admonition type="note">
|
|
|
|
To enforce private channels you need to disable the 'Allow public access' setting in [Realtime Settings](/dashboard/project/_/realtime/settings)
|
|
|
|
</Admonition>
|
|
## How it works
|
|
|
|
Realtime uses the `messages` table in your database's `realtime` schema to generate access policies for your clients when they connect to a Channel topic.
|
|
|
|
By creating RLS policies on the `realtime.messages` table you can control the access users have to a Channel topic, and features within a Channel topic.
|
|
|
|
<Admonition type="caution">
|
|
|
|
Realtime locks down the `realtime` schema to protect it against unexpected changes to guarantee the healthy operation of the Realtime service and avoid conflicts that could be caused by future migrations. Creating a table or function in `realtime` is expected to fail with `permission denied for schema realtime`, whether you run the SQL yourself or through the dashboard. Managing RLS policies on `realtime.messages` is allowed: [`supautils`](https://github.com/supabase/supautils) lets the `postgres` role run policy statements on that table without owning it.
|
|
|
|
Row Level Security is already enabled on `realtime.messages`, so don't add `ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY` to a migration. Postgres checks table ownership before it checks whether the setting would change, so the statement fails with `42501 must be owner of table messages`. That aborts the transaction, and every statement after it is skipped, including any `create policy` statements that follow.
|
|
|
|
Remove the `ALTER TABLE` line and the rest of the migration passes. If a migration needs to check that RLS is on, read the catalog:
|
|
|
|
```sql
|
|
select relrowsecurity from pg_class where oid = 'realtime.messages'::regclass;
|
|
```
|
|
|
|
If you hit that error, see [must be owner of table messages](/docs/guides/troubleshooting/realtime-must-be-owner-of-table-messages).
|
|
|
|
</Admonition>
|
|
|
|
The validation is done when the user connects. When their WebSocket connection is established and a Channel topic is joined, their permissions are calculated based on:
|
|
|
|
- The RLS policies on the `realtime.messages` table
|
|
- The user information sent as part of their [Auth JWT](/docs/guides/auth/jwts)
|
|
- The request headers
|
|
- The Channel topic the user is trying to connect to
|
|
|
|
When Realtime generates a policy for a client it performs a query on the `realtime.messages` table and then rolls it back. Realtime does not store any messages in your `realtime.messages` table.
|
|
|
|
Using Realtime Authorization involves two steps:
|
|
|
|
- In your database, create RLS policies on the `realtime.messages`
|
|
- In your client, instantiate the Realtime Channel with the `config` option `private: true`
|
|
|
|
<Admonition type="caution">
|
|
|
|
Increased RLS complexity can impact database performance and connection time, leading to higher connection latency and decreased join rates.
|
|
|
|
</Admonition>
|
|
|
|
## Accessing request information
|
|
|
|
### `realtime.topic`
|
|
|
|
You can use the `realtime.topic` helper function when writing RLS policies. It returns the Channel topic the user is attempting to connect to.
|
|
|
|
```sql
|
|
create policy "authenticated can read all messages on topic"
|
|
on "realtime"."messages"
|
|
for select
|
|
to authenticated
|
|
using (
|
|
(select realtime.topic()) = 'room-1'
|
|
);
|
|
```
|
|
|
|
### JWT claims
|
|
|
|
The user claims can be accessed using the `current_setting` function. The claims are available as a JSON object in the `request.jwt.claims` setting.
|
|
|
|
```sql
|
|
create policy "authenticated with supabase.io email can read all"
|
|
on "realtime"."messages"
|
|
for select
|
|
to authenticated
|
|
using (
|
|
-- Only users with the email claim ending with @supabase.io
|
|
(((current_setting('request.jwt.claims'))::json ->> 'email') ~~ '%@supabase.io')
|
|
);
|
|
```
|
|
|
|
## Examples
|
|
|
|
The following examples use this schema:
|
|
|
|
```sql
|
|
create table public.rooms (
|
|
id bigint generated by default as identity primary key,
|
|
topic text not null unique
|
|
);
|
|
|
|
GRANT SELECT ON public.rooms TO anon;
|
|
|
|
alter table public.rooms enable row level security;
|
|
|
|
create table public.profiles (
|
|
id uuid not null references auth.users on delete cascade,
|
|
email text NOT NULL,
|
|
|
|
primary key (id)
|
|
);
|
|
|
|
GRANT SELECT ON public.profiles TO anon;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.profiles TO authenticated;
|
|
|
|
alter table public.profiles enable row level security;
|
|
|
|
create table public.rooms_users (
|
|
user_id uuid references auth.users (id),
|
|
room_topic text references public.rooms (topic),
|
|
created_at timestamptz default current_timestamp
|
|
);
|
|
|
|
GRANT SELECT ON public.rooms_users TO authenticated;
|
|
|
|
alter table public.rooms_users enable row level security;
|
|
|
|
create policy "authenticated can read own room memberships"
|
|
on public.rooms_users
|
|
for select
|
|
to authenticated
|
|
using ((select auth.uid()) = user_id);
|
|
```
|
|
|
|
### Broadcast
|
|
|
|
The `extension` field on the `realtime.messages` table records the message type. For Broadcast messages, the value of `realtime.messages.extension` is `broadcast`. You can check for this in your RLS policies.
|
|
|
|
#### Allow a user to join (and read) a Broadcast topic
|
|
|
|
To join a Broadcast Channel, a user must have at least one read or write permission on the Channel topic.
|
|
|
|
Here, we allow reads (`select`s) for users who are linked to the requested topic within the relationship table `public.room_users`:
|
|
|
|
```sql
|
|
create policy "authenticated can receive broadcast"
|
|
on "realtime"."messages"
|
|
for select
|
|
to authenticated
|
|
using (
|
|
exists (
|
|
select
|
|
user_id
|
|
from
|
|
rooms_users
|
|
where
|
|
user_id = (select auth.uid())
|
|
and room_topic = (select realtime.topic())
|
|
and realtime.messages.extension in ('broadcast')
|
|
)
|
|
);
|
|
```
|
|
|
|
Then, to join a topic with RLS enabled, instantiate the Channel with the `private` option set to `true`.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="js"
|
|
queryGroup="language"
|
|
>
|
|
<TabPanel id="js" label="JavaScript">
|
|
```javascript
|
|
import { createClient } from '@supabase/supabase-js'
|
|
const supabase = createClient('your_project_url', 'your_supabase_api_key')
|
|
|
|
// ---cut---
|
|
const channel = supabase.channel('room-1', {
|
|
config: { private: true },
|
|
})
|
|
|
|
channel
|
|
.on('broadcast', { event: 'test' }, (payload) => console.log(payload))
|
|
.subscribe((status, err) => {
|
|
if (status === 'SUBSCRIBED') {
|
|
console.log('Connected!')
|
|
} else {
|
|
console.error(err)
|
|
}
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
<$Show if="sdk:dart">
|
|
<TabPanel id="dart" label="Dart">
|
|
```dart
|
|
final channel = supabase.channel(
|
|
'room-1',
|
|
opts: const RealtimeChannelConfig(private: true),
|
|
);
|
|
|
|
channel
|
|
.onBroadcast(event: 'test', callback: (payload) => print(payload))
|
|
.subscribe((status, err) {
|
|
if (status == RealtimeSubscribeStatus.subscribed) {
|
|
print('Connected!');
|
|
} else {
|
|
print(err);
|
|
}
|
|
});
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:swift">
|
|
<TabPanel id="swift" label="Swift">
|
|
```swift
|
|
let channel = supabase.channel("room-1") {
|
|
$0.isPrivate = true
|
|
}
|
|
|
|
Task {
|
|
for await payload in channel.broadcastStream(event: "test") {
|
|
print(payload)
|
|
}
|
|
}
|
|
|
|
await channel.subscribe()
|
|
print("Connected!")
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:kotlin">
|
|
<TabPanel id="kotlin" label="Kotlin">
|
|
```kotlin
|
|
val channel = supabase.channel("room-1") {
|
|
isPrivate = true
|
|
}
|
|
channel.broadcastFlow<MyPayload>(event = "test").onEach {
|
|
println(it)
|
|
}.launchIn(scope) // launch in your coroutine scope
|
|
channel.subscribe(blockUntilSubscribed = true)
|
|
println("Connected!")
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
<$Show if="sdk:python">
|
|
<TabPanel id="python" label="Python">
|
|
```py
|
|
channel = realtime.channel(
|
|
"room-1", {"config": {"private": True}}
|
|
)
|
|
|
|
await channel.on_broadcast(
|
|
"test", callback=lambda payload: print(payload)
|
|
).subscribe(
|
|
lambda state, err: (
|
|
print("Connected")
|
|
if state == RealtimeSubscribeStates.SUBSCRIBED
|
|
else print(err)
|
|
)
|
|
)
|
|
```
|
|
|
|
</TabPanel>
|
|
</$Show>
|
|
</Tabs>
|
|
|
|
#### Allow a user to send a Broadcast message
|
|
|
|
To authorize sending Broadcast messages, create a policy for `insert` where the value of `realtime.messages.extension` is `broadcast`.
|
|
|
|
Here, we allow writes (sends) for users who are linked to the requested topic within the relationship table `public.room_users`:
|
|
|
|
```sql
|
|
create policy "authenticated can send broadcast on topic"
|
|
on "realtime"."messages"
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
exists (
|
|
select
|
|
user_id
|
|
from
|
|
rooms_users
|
|
where
|
|
user_id = (select auth.uid())
|
|
and room_topic = (select realtime.topic())
|
|
and realtime.messages.extension in ('broadcast')
|
|
)
|
|
);
|
|
```
|
|
|
|
### Presence
|
|
|
|
The `extension` field on the `realtime.messages` table records the message type. For Presence messages, the value of `realtime.messages.extension` is `presence`. You can check for this in your RLS policies.
|
|
|
|
#### Allow users to listen to Presence messages on a Channel
|
|
|
|
Create a policy for `select` on `realtime.messages` where `realtime.messages.extension` is `presence`.
|
|
|
|
```sql
|
|
create policy "authenticated can listen to presence in topic"
|
|
on "realtime"."messages"
|
|
for select
|
|
to authenticated
|
|
using (
|
|
exists (
|
|
select
|
|
user_id
|
|
from
|
|
rooms_users
|
|
where
|
|
user_id = (select auth.uid())
|
|
and room_topic = (select realtime.topic())
|
|
and realtime.messages.extension in ('presence')
|
|
)
|
|
);
|
|
```
|
|
|
|
#### Allow users to send Presence messages on a channel
|
|
|
|
To update the Presence status for a user create a policy for `insert` on `realtime.messages` where the value of `realtime.messages.extension` is `presence`.
|
|
|
|
```sql
|
|
create policy "authenticated can track presence on topic"
|
|
on "realtime"."messages"
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
exists (
|
|
select
|
|
user_id
|
|
from
|
|
rooms_users
|
|
where
|
|
user_id = (select auth.uid())
|
|
and room_topic = (select realtime.topic())
|
|
and realtime.messages.extension in ('presence')
|
|
)
|
|
);
|
|
```
|
|
|
|
### Presence and Broadcast
|
|
|
|
Authorize both Presence and Broadcast by including both extensions in the `where` filter.
|
|
|
|
#### Broadcast and Presence read
|
|
|
|
Authorize Presence and Broadcast read in one RLS policy.
|
|
|
|
```sql
|
|
create policy "authenticated can listen to broadcast and presence on topic"
|
|
on "realtime"."messages"
|
|
for select
|
|
to authenticated
|
|
using (
|
|
exists (
|
|
select
|
|
user_id
|
|
from
|
|
rooms_users
|
|
where
|
|
user_id = (select auth.uid())
|
|
and room_topic = (select realtime.topic())
|
|
and realtime.messages.extension in ('broadcast', 'presence')
|
|
)
|
|
);
|
|
```
|
|
|
|
#### Broadcast and Presence write
|
|
|
|
Authorize Presence and Broadcast write in one RLS policy.
|
|
|
|
```sql
|
|
create policy "authenticated can send broadcast and presence on topic"
|
|
on "realtime"."messages"
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
exists (
|
|
select
|
|
user_id
|
|
from
|
|
rooms_users
|
|
where
|
|
user_id = (select auth.uid())
|
|
and room_topic = (select realtime.topic())
|
|
and realtime.messages.extension in ('broadcast', 'presence')
|
|
)
|
|
);
|
|
```
|
|
|
|
## Interaction with Postgres Changes
|
|
|
|
When using Postgres Changes on tables with RLS, database records are sent only to clients who are allowed to read them based on your RLS policies.
|
|
|
|
Private and public channels can subscribe to Postgres Changes.
|
|
|
|
## Updating RLS policies
|
|
|
|
Client access policies are cached for the duration of the connection. Your database is not queried for every Channel message.
|
|
|
|
Realtime updates the access policy cache for a client based on your RLS policies when:
|
|
|
|
- A client connects to Realtime and subscribes to a Channel
|
|
- A new JWT is sent to Realtime from a client via the [`access_token` message](/docs/guides/realtime/protocol#access-token)
|
|
|
|
This means that if you revoke a user's access (for example, by removing a row an RLS policy uses to grant it) while they're still connected, they'll keep receiving messages until their JWT expires or a new one is sent.
|
|
|
|
If a new JWT is never received on the Channel, the client will be disconnected when the JWT expires.
|
|
|
|
Make sure to keep the JWT expiration window short.
|