mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Make it clear what users can and can't do on `realtime` schema. ## What is the current behavior? After https://github.com/supabase/realtime/pull/1993 creating or altering the realtime schema is no longer allowed, but some users are still trying to execute `ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY` or trying to create objects on that schema. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Clarified Realtime schema protections, including a caution about how the `realtime` schema is restricted and what permission errors to expect when creating objects there. * Confirmed that row level security is enabled by default on `realtime.messages`, and that managing its RLS policies is supported. * Documented the additional binary-capable function, `realtime.send_binary`, alongside the existing `realtime.send` behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
408 lines
12 KiB
Plaintext
408 lines
12 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.
|
|
|
|
Row level security (RLS) is enabled by default on table `realtime.messages`, you don't need to execute `ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY`.
|
|
|
|
</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)
|
|
|
|
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.
|