From c6b92060d2d9183ff4708fee346f391ca5253fb5 Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Fri, 25 Sep 2026 13:39:51 -0400 Subject: [PATCH] docs(realtime): realtime permissions (#50735) 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 ## 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. --- .../content/guides/realtime/authorization.mdx | 12 ++- .../content/guides/realtime/broadcast.mdx | 2 + ...altime-must-be-owner-of-table-messages.mdx | 84 +++++++++++++++++++ ...ealtime-warn-sending-broadcast-message.mdx | 18 ++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 apps/docs/content/troubleshooting/realtime-must-be-owner-of-table-messages.mdx diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx index 7dc0410d29c..a467f43d6fa 100644 --- a/apps/docs/content/guides/realtime/authorization.mdx +++ b/apps/docs/content/guides/realtime/authorization.mdx @@ -25,9 +25,17 @@ By creating RLS policies on the `realtime.messages` table you can control the ac -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. +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 (RLS) is enabled by default on table `realtime.messages`, you don't need to execute `ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY`. +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). diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index f56f9e56170..41240dafea0 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -1020,6 +1020,8 @@ Broadcast Changes allows you to trigger messages from your database. To achieve It uses partitioned tables per day, which allows performant deletion of your previous messages by dropping the physical tables of this partitioned table. Tables older than 3 days are deleted. +A client connecting over WebSocket creates those daily partitions. `realtime.send` does not. With no partition for today, the insert fails and Postgres logs a `WarnSendingBroadcastMessage` warning, so connect a client before you broadcast from the database. See [Realtime: `WarnSendingBroadcastMessage`](/docs/guides/troubleshooting/realtime-warn-sending-broadcast-message) if you hit that warning. + Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON payloads. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data. Broadcast Changes provides two functions to help you send messages: diff --git a/apps/docs/content/troubleshooting/realtime-must-be-owner-of-table-messages.mdx b/apps/docs/content/troubleshooting/realtime-must-be-owner-of-table-messages.mdx new file mode 100644 index 00000000000..4e756227e09 --- /dev/null +++ b/apps/docs/content/troubleshooting/realtime-must-be-owner-of-table-messages.mdx @@ -0,0 +1,84 @@ +--- +title = "Realtime: \"must be owner of table messages\" when setting up Authorization" +topics = [ "realtime" ] +keywords = [ "realtime.messages", "must be owner", "42501", "row level security", "policy", "authorization", "broadcast", "migration" ] + +[[errors]] +code = "42501" +message = "must be owner of table messages" +--- + +A migration that adds an RLS policy to `realtime.messages` fails: + +``` +ERROR: 42501: must be owner of table messages +``` + +## Cause + +`realtime.messages` is owned by an internal role, `supabase_realtime_admin`. The `postgres` role is not a member of it and is not a superuser. + +Policies still work because of [`supautils`](https://github.com/supabase/supautils), the extension that gives the `postgres` role its elevated permissions on a Supabase project. It lets `postgres` run `create policy`, `alter policy`, and `drop policy` on a fixed list of tables it does not own, and `realtime.messages` is on that list. The list covers policy statements only, so `ALTER TABLE` is still refused. + +The failing statement is usually not the `create policy`. It's an `ALTER TABLE` earlier in the same migration: + +```sql +alter table realtime.messages enable row level security; +alter table realtime.messages add column ...; +alter table realtime.messages owner to postgres; +``` + +The first one is the common case. RLS is already enabled on `realtime.messages`, and Postgres checks ownership before it checks whether the setting would change, so the statement fails on every project. + +A failed statement aborts the transaction, so Postgres skips everything after it, including the `create policy`. `supabase db push` runs each migration file in one transaction, so the file rolls back and nothing is written to `supabase_migrations.schema_migrations`. + +## Fix + +Remove the `ALTER TABLE` line. Keep the policy: + +```sql +create policy "authenticated can read messages on room-1" +on "realtime"."messages" +for select +to authenticated +using ( + (select realtime.topic()) = 'room-1' +); +``` + +Run it from the SQL editor or with `supabase db push`. Both connect as `postgres`. + +To check that RLS is on, read the catalog instead of setting it: + +```sql +select relrowsecurity from pg_class where oid = 'realtime.messages'::regclass; +``` + +## What doesn't work + +Granting the owning role fails, and support can't grant it either: + +```sql +grant supabase_realtime_admin to postgres; +-- ERROR: 42501: "supabase_realtime_admin" role memberships are reserved, only superusers can grant them +``` + +Restarting the project changes nothing. + +Wrapping the `ALTER TABLE` in a function or a `do` block changes nothing either. The ownership check still runs, and a `security definer` function owned by `postgres` is still not the table owner. + +## What postgres can do on realtime.messages + +The `Yes` rows are what `supautils` delegates, plus the privileges granted to `postgres` directly. + +| Statement | Allowed | +| ----------------------------------------------------- | ------- | +| `create policy`, `alter policy`, `drop policy` | Yes | +| `comment on policy` | Yes | +| `select`, `insert` | Yes | +| `grant select`, `grant insert` to your own roles | Yes | +| `alter table` in any form | No | +| `drop table` | No | +| Creating tables or functions in the `realtime` schema | No | + +See [Realtime Authorization](/docs/guides/realtime/authorization) for writing the policies. diff --git a/apps/docs/content/troubleshooting/realtime-warn-sending-broadcast-message.mdx b/apps/docs/content/troubleshooting/realtime-warn-sending-broadcast-message.mdx index 41dfe26153e..59e34a447e0 100644 --- a/apps/docs/content/troubleshooting/realtime-warn-sending-broadcast-message.mdx +++ b/apps/docs/content/troubleshooting/realtime-warn-sending-broadcast-message.mdx @@ -45,6 +45,24 @@ Calling `realtime.send` or `realtime.send_binary`, the broadcast REST endpoint, +## Check which partitions you have + +```sql +select c.relname +from pg_inherits i + join pg_class c on c.oid = i.inhrelid +where i.inhparent = 'realtime.messages'::regclass +order by c.relname; +``` + +The names are dates, so the result tells you which case you are in: + +- One covers today. The missing partition is not the cause, so go to [When it is a real problem](#when-it-is-a-real-problem). +- They all end on a past date. Your window lapsed. The newest name is three days after the last time partitions were created, because each creation covers yesterday through three days ahead. +- No rows. No client has ever connected, so no partition was ever created. + +Zero rows does not mean the Realtime migrations are missing. Those would fail this query with `42P01 relation "realtime.messages" does not exist`. If you see `42P01`, open a support ticket. + ## How to avoid it - Connect a client before broadcasting from the database. A live WebSocket connection both creates the partitions and starts the consumer that receives the message.