Postgres Changes
Get up and running with Realtime's Postgres Changes feature
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.
Quick start#
Let's explore how to implement Realtime Postgres Changes so you can integrate it into your use case.
Set up a Supabase project with a 'todos' table
Create a new project in the Supabase Dashboard.
After your project is ready, create a table in your Supabase database. You can do this with either the Table interface or the SQL Editor.
_10-- Create a table called "todos"_10-- with a column to store tasks._10create table todos (_10 id serial primary key,_10 task text_10);
Allow anonymous access
In this example we'll turn on Row Level Security for this table and allow anonymous access. In production, be sure to secure your application with the appropriate permissions.
_10-- Turn on security_10alter table "todos"_10enable row level security;_10_10-- Allow anonymous access_10create policy "Allow anonymous access"_10 on todos_10 for select_10 to anon_10 using (true);
Enable Postgres replication
Go to your project's Replication settings, and under supabase_realtime
, toggle on the tables you want to listen to.
Install the client
Install the Supabase JavaScript client.
_10 npm install @supabase/supabase-js
Create the client
This client will be used to listen to Postgres changes.
_10 import {_10 createClient_10 } from '@supabase/supabase-js'_10_10 const client = createClient(_10 'https://<project>.supabase.co',_10 '<your-anon-key>'_10 )
Listen to changes by schema
Listen to changes on all tables in the public
schema by setting the schema
property to 'public' and event name to *
. The event name can be one of:
INSERT
UPDATE
DELETE
*
The channel name can be any string except 'realtime'.
_11 const channelA = client_11 .channel('schema-db-changes')_11 .on(_11 'postgres_changes',_11 {_11 event: '*',_11 schema: 'public',_11 },_11 (payload) => console.log(payload)_11 )_11 .subscribe()
Listen to changes by table
Listen to just inserts in the todos
table by setting the table
property to 'todos' and event name to INSERT
.
_12const channelB = client_12 .channel('table-db-changes')_12 .on(_12 'postgres_changes',_12 {_12 event: 'INSERT',_12 schema: 'public',_12 table: 'todos',_12 },_12 (payload) => console.log(payload)_12 )_12 .subscribe()
Listen to changes by filter
Listen to changes in the todos
table when a column's value equals a specified value. In this example, we only listen to inserts on todos
where the row id
is 1.
_13const channelC = client_13 .channel('table-filter-changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'INSERT',_13 schema: 'public',_13 table: 'todos',_13 filter: 'id=eq.1',_13 },_13 (payload) => console.log(payload)_13 )_13 .subscribe()
More filters
Check out the full list of available filters.
Insert dummy data
Now we can add some data to our table which will trigger channelA
, channelB
, and channelC
event handlers.
_10insert into todos (task)_10values_10 ('Change!');
Available filters#
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:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'UPDATE',_13 schema: 'public',_13 table: 'messages',_13 filter: 'body=eq.hey',_13 },_13 (payload) => console.log(payload)_13 )_13 .subscribe()
note
=
.neq#
To listen to changes when a column's value in a table does not equal a client-specified value:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'INSERT',_13 schema: 'public',_13 table: 'messages',_13 filter: 'body=neq.bye',_13 },_13 (payload) => console.log(payload)_13 )_13 .subscribe()
note
!=
.lt#
To listen to changes when a column's value in a table is less than a client-specified value:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'INSERT',_13 schema: 'public',_13 table: 'messages',_13 filter: 'id=lt.100',_13 },_13 (payload) => console.log(payload)_13 )_13 .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:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'UPDATE',_13 schema: 'public',_13 table: 'profiles',_13 filter: 'age=lte.65',_13 },_13 (payload) => console.log(payload)_13 )_13 .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:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'INSERT',_13 schema: 'public',_13 table: 'products',_13 filter: 'quantity=gt.10',_13 },_13 (payload) => console.log(payload)_13 )_13 .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:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'INSERT',_13 schema: 'public',_13 table: 'products',_13 filter: 'quantity=gte.10',_13 },_13 (payload) => console.log(payload)_13 )_13 .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:
_13const channel = supabase_13 .channel('changes')_13 .on(_13 'postgres_changes',_13 {_13 event: 'INSERT',_13 schema: 'public',_13 table: 'colors',_13 filter: 'name=in.(red, blue, yellow)',_13 },_13 (payload) => console.log(payload)_13 )_13 .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:
_22const channel = supabase_22 .channel('db-changes')_22 .on(_22 'postgres_changes',_22 {_22 event: '*',_22 schema: 'public',_22 table: 'messages',_22 filter: 'body=eq.bye',_22 },_22 (payload) => console.log(payload)_22 )_22 .on(_22 'postgres_changes',_22 {_22 event: 'INSERT',_22 schema: 'public',_22 table: 'users',_22 },_22 (payload) => console.log(payload)_22 )_22 .subscribe()
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
:
_10alter 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).
Private schemas#
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:
_10grant select on "non_private_schema"."some_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.
Custom tokens#
You may choose to sign your own tokens to customize claims that can be checked in your RLS policies.
Your project JWT secret is found with your Project API keys in your dashboard.
caution
Do not expose the service_role
token on the client because the role is authorized to bypass
row-level security.
To use your own JWT with Realtime make sure to set the token after instantiating the Supabase client and before connecting to a Channel.
_20const { createClient } = require('@supabase/supabase-js')_20_20const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY, {})_20_20// Set your custom JWT here_20supabase.realtime.setAuth('your-custom-jwt')_20_20const channel = supabase_20 .channel('db-changes')_20 .on(_20 'postgres_changes',_20 {_20 event: '*',_20 schema: 'public',_20 table: 'messages',_20 filter: 'body=eq.bye',_20 },_20 (payload) => console.log(payload)_20 )_20 .subscribe()
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_10supabase.realtime.setAuth('fresh-token')