# Postgres Changes

> Stream inserts, updates and deletes from your tables to clients with Supabase Realtime Postgres Changes on SupaHost. Publication setup, filters, RLS, replica identity and scaling.

Source: https://supahost.app/docs/realtime/postgres-changes · Updated 2026-11-11

Postgres Changes lets clients subscribe to inserts, updates and deletes on your tables. When a row changes, Realtime reads the change from the project's database, checks it against the table's RLS policies for each subscriber, and sends it to the clients allowed to see it.

It's the quickest way to keep a view in sync with a table: a kitchen screen that shows new orders, a status page for a booking, a shared shopping list.

## Step 1: add the table to the publication

Realtime only sees tables in the `supabase_realtime` publication. Add the tables you want to stream, in Studio's SQL editor or a migration:

```sql
alter publication supabase_realtime add table public.orders;
```

To check which tables are included:

```sql
select schemaname, tablename
from pg_publication_tables
where pubname = 'supabase_realtime';
```

Only add tables that clients really need to watch. Every change to a published table is processed by Realtime, whether or not anyone is subscribed.

## Step 2: make sure RLS allows reads

Clients receive a change only if they could `select` that row through the Data API. Enable RLS and add a select policy:

```sql
alter table public.orders enable row level security;

create policy "Staff see orders for their restaurant"
on public.orders for select to authenticated
using (
  restaurant_id in (
    select restaurant_id from public.staff where user_id = (select auth.uid())
  )
);
```

If a subscription connects but no events arrive, a missing or too strict select policy is the usual cause. See [Postgres Changes not firing](/kb/realtime-postgres-changes).

## Step 3: subscribe from the client

```ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

const kitchen = supabase
  .channel('kitchen-42')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'orders', filter: 'restaurant_id=eq.42' },
    (payload) => addOrderCard(payload.new)
  )
  .on(
    'postgres_changes',
    { event: 'UPDATE', schema: 'public', table: 'orders', filter: 'restaurant_id=eq.42' },
    (payload) => updateOrderCard(payload.new)
  )
  .on(
    'postgres_changes',
    { event: 'DELETE', schema: 'public', table: 'orders' },
    (payload) => removeOrderCard(payload.old.id)
  )
  .subscribe((status) => console.log('kitchen channel:', status))
```

The user must be signed in for the `authenticated` policy to apply. supabase-js passes the session's access token to Realtime and refreshes it when the session refreshes.

## The event payload

Each callback receives an object like this:

```json
{
  "schema": "public",
  "table": "orders",
  "commit_timestamp": "2026-11-11T12:03:44.120Z",
  "eventType": "UPDATE",
  "new": { "id": 981, "restaurant_id": 42, "status": "ready", "updated_at": "..." },
  "old": { "id": 981 },
  "errors": null
}
```

`new` holds the row after the change (empty for deletes). `old` holds the previous values, but by default only the primary key.

## Old values and replica identity

To receive the full previous row on updates and deletes, set the table's replica identity to full:

```sql
alter table public.orders replica identity full;
```

Two caveats from upstream Realtime:

- With RLS enabled, `old` on a **delete** still contains only the primary key, because Postgres can't check a policy against a row that no longer exists.
- Full replica identity makes Postgres write more data to its write-ahead log for every update and delete on that table. Only turn it on where you need the old values.

## Filters

Filters reduce what each client receives. They use the same operator names as the Data API:

| Filter | Example |
|---|---|
| `eq` | `status=eq.ready` |
| `neq` | `status=neq.cancelled` |
| `lt`, `lte`, `gt`, `gte` | `total=gt.100` |
| `in` | `status=in.(new,cooking)` (up to 100 values) |

You can use one filter per subscription. Delete events can't be filtered, so subscribe to deletes without a filter or handle them in your callback.

Event can be `'INSERT'`, `'UPDATE'`, `'DELETE'` or `'*'` for all three. Leave out `table` to listen to every published table in a schema.

## How it runs on SupaHost

- Realtime polls each project's database for new changes every 100 ms, so changes usually arrive within a fraction of a second.
- The per-record limit for change payloads is 1 MB. Keep large text, JSON or binary columns out of tables you stream, or stream a small "something changed" row and fetch details separately.
- Changes count toward your project's Realtime limits (5,000 events per second) and your instance's egress. See [Realtime capacity and limits](/docs/realtime/limits).

## When to use something else

For every change and every subscriber, Realtime checks RLS by running a query as that user. With a few subscribers per table this is cheap. With hundreds of clients watching the same busy table, it becomes the bottleneck, and changes start to lag.

For those cases, send changes with Broadcast from the database (see [Broadcast](/docs/realtime/broadcast)) instead. A trigger calls `realtime.send()` or `realtime.broadcast_changes()` once per change, to a topic such as `restaurant:42`, and [private channel policies](/docs/realtime/authorization) decide who may join that topic. The authorization check then happens once per join instead of once per message.

A rough rule:

| Situation | Use |
|---|---|
| A handful of users per table, prototypes, admin screens | Postgres Changes |
| Many viewers of the same data, or high write rates | Broadcast from the database |
| Clients only need "refresh now" hints | Broadcast with a tiny payload, then refetch |

## Missed changes

Postgres Changes only delivers changes that happen while the client is subscribed. After a reconnect (for example a laptop waking from sleep), refetch the data you're showing so you don't miss anything that changed in between. A simple way is to reload whenever the status callback reports `SUBSCRIBED`.

## Related

- [Postgres Changes not firing](/kb/realtime-postgres-changes)
- [Row Level Security](/docs/database/row-level-security)
- [Triggers](/docs/database/triggers)
- [Postgres Changes reference in Supabase's docs](https://supabase.com/docs/guides/realtime/postgres-changes)
