Home

Postgres Changes

Realtime's Postgres Changes feature listens for database changes and sends them to clients. Clients are required to subscribe with a JWT dictating which changes they are allowed to receive based on the database's Row Level Security.

Anyone with access to a valid JWT signed with the project's JWT secret is able to listen to your database's changes, unless tables have Row Level Security enabled and policies in place.

Clients can choose to receive INSERT, UPDATE, DELETE, or * (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the public schema and you must first enable the tables you want your clients to listen to.

Postgres Changes works out of the box for tables in the public schema. You can listen to tables in your private schemas by granting table SELECT permissions to the database role found in your access token. You can run a query similar to the following:


_10
grant
_10
select
_10
on "private_schema"."table" to authenticated;

caution

We strongly encourage you to enable RLS and create policies for tables in private schemas. Otherwise, any role you grant access to will have unfettered read access to the table.

Replication Setup#

You can do this in the Replication section in the Dashboard or with the SQL editor:


_14
begin;
_14
_14
-- remove the supabase_realtime publication
_14
drop
_14
publication if exists supabase_realtime;
_14
_14
-- re-create the supabase_realtime publication with no tables
_14
create publication supabase_realtime;
_14
_14
commit;
_14
_14
-- add a table to the publication
_14
alter
_14
publication supabase_realtime add table messages;

Full old Record#

By default, only new record changes are sent but if you want to receive the old record (previous values) whenever you UPDATE or DELETE a record, you can set the replica identity of your table to full:


_10
alter table
_10
messages replica identity full;

caution

RLS policies are not applied to DELETE statements. When RLS is enabled and replica identity is set to full on a table, the old record contains only the primary key(s).

Schema Changes#

To listen to all changes in the public schema:


_23
const { createClient } = require('@supabase/supabase-js')
_23
_23
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
_23
_23
/*
_23
Channel name can be any string.
_23
Event name can can be one of:
_23
- INSERT
_23
- UPDATE
_23
- DELETE
_23
- *
_23
*/
_23
const channel = supabase
_23
.channel('schema-db-changes')
_23
.on(
_23
'postgres_changes',
_23
{
_23
event: '*',
_23
schema: 'public',
_23
},
_23
(payload) => console.log(payload)
_23
)
_23
.subscribe()

Table Changes#

To listen to changes on a table in the public schema:


_14
// Supabase client setup
_14
_14
const channel = supabase
_14
.channel('table-db-changes')
_14
.on(
_14
'postgres_changes',
_14
{
_14
event: 'INSERT',
_14
schema: 'public',
_14
table: 'messages',
_14
},
_14
(payload) => console.log(payload)
_14
)
_14
.subscribe()

Filter Changes#

Realtime offers filters so you can specify the data your client receives at a more granular level.

eq#

To listen to changes when a column's value in a table equals a client-specified value:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'UPDATE',
_15
schema: 'public',
_15
table: 'messages',
_15
filter: 'body=eq.hey',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' =.

neq#

To listen to changes when a column's value in a table does not equal a client-specified value:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'INSERT',
_15
schema: 'public',
_15
table: 'messages',
_15
filter: 'body=neq.bye',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' !=.

lt#

To listen to changes when a column's value in a table is less than a client-specified value:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'INSERT',
_15
schema: 'public',
_15
table: 'messages',
_15
filter: 'id=lt.100',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' < so it works for non-numeric types but make sure to check the expected behavior of the compared data's type.

lte#

To listen to changes when a column's value in a table is less than or equal to a client-specified value:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'UPDATE',
_15
schema: 'public',
_15
table: 'profiles',
_15
filter: 'age=lte.65',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' <= so it works for non-numeric types but make sure to check the expected behavior of the compared data's type.

gt#

To listen to changes when a column's value in a table is greater than a client-specified value:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'INSERT',
_15
schema: 'public',
_15
table: 'products',
_15
filter: 'quantity=gt.10',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' > so it works for non-numeric types but make sure to check the expected behavior of the compared data's type.

gte#

To listen to changes when a column's value in a table is greater than or equal to a client-specified value:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'INSERT',
_15
schema: 'public',
_15
table: 'products',
_15
filter: 'quantity=gte.10',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' >= so it works for non-numeric types but make sure to check the expected behavior of the compared data's type.

in#

To listen to changes when a column's value in a table equals any client-specified values:


_15
// Supabase client setup
_15
_15
const channel = supabase
_15
.channel('changes')
_15
.on(
_15
'postgres_changes',
_15
{
_15
event: 'INSERT',
_15
schema: 'public',
_15
table: 'colors',
_15
filter: 'name=in.(red, blue, yellow)',
_15
},
_15
(payload) => console.log(payload)
_15
)
_15
.subscribe()

note

This filter uses Postgres' = ANY. Realtime allows a maximum of 100 values for this filter.

Combination Changes#

To listen to different events and schema/tables/filters combinations with the same channel:


_24
// Supabase client setup
_24
_24
const channel = supabase
_24
.channel('db-changes')
_24
.on(
_24
'postgres_changes',
_24
{
_24
event: '*',
_24
schema: 'public',
_24
table: 'messages',
_24
filter: 'body=eq.bye',
_24
},
_24
(payload) => console.log(payload)
_24
)
_24
.on(
_24
'postgres_changes',
_24
{
_24
event: 'INSERT',
_24
schema: 'public',
_24
table: 'users',
_24
},
_24
(payload) => console.log(payload)
_24
)
_24
.subscribe()

Custom Tokens#

You may choose to sign your own tokens to customize claims that can be checked in your RLS policies. In order for this to work you must pass apikey in both Realtime's headers and params when creating the client.

The apikey in params must be either the anon or service_role token that Supabase provides for every project. You can find these tokens under Project API keys in your project's dashboard. This will authenticate your request in the API gateway.

caution

Do not expose the service_role token on the client because the role is authorized to bypass row-level security.

The apikey in headers can be your custom token signed with the JWT secret of your Supabase project. This is forwarded to the Realtime server and it will verify your custom token and use its claims to authorize database changes when RLS is enabled.


_12
const { createClient } = require('@supabase/supabase-js')
_12
_12
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY, {
_12
realtime: {
_12
headers: {
_12
apikey: `Bearer ${your_custom_token}`,
_12
},
_12
params: {
_12
apikey: process.env.SUPABASE_KEY,
_12
},
_12
},
_12
})

Refreshed Tokens#

You will need to refresh tokens on your own, but once generated, you can pass them to Realtime.

For example, if you're using the supabase-js v2 client then you can pass your token like this:


_10
// Client setup
_10
_10
supabase.realtime.setAuth('fresh-token')