# must be owner of table messages

> A migration that touches realtime.messages fails with 42501 must be owner of table messages. Why it happens, which statement to remove, and how to add Realtime policies safely.

Source: https://supahost.app/kb/realtime-must-be-owner-messages · Updated 2026-11-11

## What you see

A migration or SQL script fails partway through:

```text
ERROR: 42501: must be owner of table messages
```

Policies you expected to exist on `realtime.messages` aren't there afterwards. A related message is `permission denied for schema realtime` when you try to create a table or function inside that schema.

## Why it happens

`realtime.messages` is the table Realtime uses for Broadcast and Presence authorization. It belongs to Realtime's own admin role, not to `postgres`, and the `realtime` schema is locked down so your migrations can't break the service.

Row Level Security is already enabled on `realtime.messages`. A script that runs this line anyway:

```sql
alter table realtime.messages enable row level security;
```

fails, because Postgres checks that you own the table before it checks whether anything would change. The error aborts the transaction, so every statement after it, including your `create policy` lines, is rolled back too.

Creating and dropping policies on the table is allowed for `postgres`; it's only ownership-level changes (alter table, new tables or functions in `realtime`) that fail.

## How to fix it

1. **Delete the `alter table realtime.messages ...` line** from the migration. Nothing else needs to change.
2. **Keep only policy statements** for this table. For example, a team chat where members can read and send on their team's topics:

   ```sql
   create policy "Team members can read team topics"
   on realtime.messages for select
   to authenticated
   using (
     exists (
       select 1 from public.team_members m
       where m.user_id = (select auth.uid())
         and 'team:' || m.team_id::text = (select realtime.topic())
     )
   );

   create policy "Team members can send on team topics"
   on realtime.messages for insert
   to authenticated
   with check (
     exists (
       select 1 from public.team_members m
       where m.user_id = (select auth.uid())
         and 'team:' || m.team_id::text = (select realtime.topic())
     )
   );
   ```

3. **If you need to confirm RLS is on**, read the catalog instead of altering the table:

   ```sql
   select relrowsecurity from pg_class where oid = 'realtime.messages'::regclass;
   ```

4. **Put your own helper functions in `public`** (or a private schema), not in `realtime`.
5. **Re-run the migration.** Since the failed one rolled back completely, it's safe to apply the fixed version.

## On SupaHost

Migrations applied through the SupaHost MCP server's `apply_migration` tool run as a single transaction, so one failing line means none of the migration was applied, and it isn't recorded in the migration history. Fix the file and apply it again. The same is true if you paste the script into Studio's SQL editor as one batch.

If the migration came from a schema diff or dump of another project, check it for other statements on the `realtime` schema and remove them too; Realtime creates and manages that schema itself in each project database.

## Related

- [Realtime authorization](/docs/realtime/authorization)
- [SQL migrations](/docs/database/migrations)
- [Broadcasting from the database](/kb/realtime-broadcast-from-database)
- [Partial migrations from transaction mistakes](/kb/partial-migrations)
