Back to Supabase

Postgres Changes

apps/docs/content/guides/realtime/postgres-changes.mdx

1.26.0875.5 KB
Original Source

Use Realtime's Postgres Changes to listen to database events.

Quick start

In this example we'll set up a database table, secure it with Row Level Security, and subscribe to all changes using the Supabase client libraries.

<StepHikeCompact>

<StepHikeCompact.Step step={1}> <StepHikeCompact.Details title="Set up a Supabase project with a 'todos' table">

[Create a new project](https://app.supabase.com) 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](https://app.supabase.com/project/_/sql).

</StepHikeCompact.Details>

<StepHikeCompact.Code>

  <Tabs
    scrollable
    size="small"
    type="underlined"
    defaultActiveId="sql"
    queryGroup="database-method"
  >
  <TabPanel id="sql" label="SQL">

  ```sql
  -- Create a table called "todos"
  -- with a column to store tasks.
  create table todos (
    id serial primary key,
    task text
  );
  ```

  </TabPanel>
  <TabPanel id="dashboard" label="Dashboard">

  <video width="99%" muted playsInline controls={true}>
    <source
      src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
      type="video/mp4"
    />
  </video>

  </TabPanel>
  </Tabs>

</StepHikeCompact.Code>

</StepHikeCompact.Step>

<StepHikeCompact.Step step={2}>

<StepHikeCompact.Details title="Allow anonymous access">

In this example we'll turn on [Row Level Security](/docs/guides/database/postgres/row-level-security) for this table and allow anonymous access. In production, be sure to secure your application with the appropriate permissions.

</StepHikeCompact.Details>

<StepHikeCompact.Code>

  ```sql
  -- Grant the privileges roles need
  GRANT SELECT ON public.todos TO anon;

  -- Turn on security
  alter table "todos"
  enable row level security;

  -- Allow anonymous access
  create policy "Allow anonymous access"
  on todos
  for select
  to anon
  using (true);
  ```

</StepHikeCompact.Code>

</StepHikeCompact.Step>

<StepHikeCompact.Step step={3}>

<StepHikeCompact.Details title="Enable Postgres replication">

  Go to your project's [Publications settings](/dashboard/project/_/database/publications), and under `supabase_realtime`, toggle on the tables you want to listen to.

  Alternatively, add tables to the `supabase_realtime` publication by running the given SQL:

</StepHikeCompact.Details>

<StepHikeCompact.Code>

  ```sql
  alter publication supabase_realtime
  add table your_table_name;
  ```

</StepHikeCompact.Code>

</StepHikeCompact.Step>

<StepHikeCompact.Step step={4}>

<StepHikeCompact.Details title="Install the client">

  Install the Supabase JavaScript client.

</StepHikeCompact.Details>

<StepHikeCompact.Code>

  ```bash
  npm install @supabase/supabase-js
  ```

</StepHikeCompact.Code>

</StepHikeCompact.Step>

<StepHikeCompact.Step step={5}>

<StepHikeCompact.Details title="Create the client">

  This client will be used to listen to Postgres changes.

</StepHikeCompact.Details>

<StepHikeCompact.Code>

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

  const supabase = createClient(
    'https://<project>.supabase.co',
    '<sb_publishable_... key>'
  )
  ```

</StepHikeCompact.Code>

</StepHikeCompact.Step>

<StepHikeCompact.Step step={6}> <StepHikeCompact.Details title="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'.

</StepHikeCompact.Details>

<StepHikeCompact.Code>

  ```js
  import { createClient } from '@supabase/supabase-js'
  const supabase = createClient('your_project_url', 'your_supabase_api_key')

  // ---cut---
  const channelA = supabase
    .channel('schema-db-changes')
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
      },
      (payload) => console.log(payload)
    )
    .subscribe()
  ```

</StepHikeCompact.Code>

</StepHikeCompact.Step>

<StepHikeCompact.Step step={7}>
<StepHikeCompact.Details title="Insert dummy data">

Now we can add some data to our table which will trigger the `channelA` event handler.

</StepHikeCompact.Details>

<StepHikeCompact.Code>

  ```sql
  insert into todos (task)
  values
    ('Change!');
  ```

</StepHikeCompact.Code>

</StepHikeCompact.Step>

</StepHikeCompact>

Usage

You can use the Supabase client libraries to subscribe to database changes.

Listening to specific schemas

Subscribe to specific schema events using the schema parameter:

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript"> ```js const changes = supabase .channel('schema-db-changes') .on( 'postgres_changes', { schema: 'public', // Subscribes to the "public" schema in Postgres event: '*', // Listen to all changes }, (payload) => console.log(payload) ) .subscribe() ``` </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('schema-db-changes')
    .onPostgresChanges(
        schema: 'public', // Subscribes to the "public" schema in Postgres
        event: PostgresChangeEvent.all, // Listen to all changes

        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("schema-db-changes")

let changes = await myChannel.postgresChange(AnyAction.self, schema: "public")

await myChannel.subscribe()

for await change in changes {
  switch change {
  case .insert(let action): print(action)
  case .update(let action): print(action)
  case .delete(let action): print(action)
  case .select(let action): print(action)
  }
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("schema-db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction>(schema = "public")

changes
    .onEach {
        when(it) { //You can also check for <is PostgresAction.Insert>, etc.. manually
            is HasRecord -> println(it.record)
            is HasOldRecord -> println(it.oldRecord)
            else -> println(it)
        }
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('schema-db-changes').on_postgres_changes(
  "*",
  schema="public",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("schema-db-changes");

// Subscribes to the "public" schema in Postgres
channel.Register(new PostgresChangesOptions("public"));
channel.AddPostgresChangeHandler(ListenType.All, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

The channel name can be any string except 'realtime'.

Listening to specific events

Use the event parameter to listen only to a specific database event. event can be INSERT, UPDATE, DELETE, or * to listen to all changes.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="insert" queryGroup="pg-changes-event"

<TabPanel id="insert" label="Insert">
js
const changes = supabase
  .channel('schema-db-changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="update" label="Update">
js
const changes = supabase
  .channel('schema-db-changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="delete" label="Delete">
js
const changes = supabase
  .channel('schema-db-changes')
  .on(
    'postgres_changes',
    {
      event: 'DELETE',
      schema: 'public',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="all" label="All events">
js
const changes = supabase
  .channel('schema-db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">

<Tabs scrollable size="small" type="underlined" defaultActiveId="insert" queryGroup="pg-changes-event"

<TabPanel id="insert" label="Insert">
dart
supabase
    .channel('schema-db-changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> <TabPanel id="update" label="Update">
dart
supabase
    .channel('schema-db-changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.update,
        schema: 'public',
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> <TabPanel id="delete" label="Delete">
dart
supabase
    .channel('schema-db-changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.delete,
        schema: 'public',
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> <TabPanel id="all" label="All events">
dart
supabase
    .channel('schema-db-changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </Tabs> </TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">

Pass the action type to select the event.

<Tabs scrollable size="small" type="underlined" defaultActiveId="insert" queryGroup="pg-changes-event"

<TabPanel id="insert" label="Insert">
swift
let myChannel = await supabase.channel("schema-db-changes")

let changes = await myChannel.postgresChange(InsertAction.self, schema: "public")

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> <TabPanel id="update" label="Update">
swift
let myChannel = await supabase.channel("schema-db-changes")

let changes = await myChannel.postgresChange(UpdateAction.self, schema: "public")

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> <TabPanel id="delete" label="Delete">
swift
let myChannel = await supabase.channel("schema-db-changes")

let changes = await myChannel.postgresChange(DeleteAction.self, schema: "public")

await myChannel.subscribe()

for await change in changes {
  print(change.oldRecord)
}
</TabPanel> <TabPanel id="all" label="All events">
swift
let myChannel = await supabase.channel("schema-db-changes")

let changes = await myChannel.postgresChange(AnyAction.self, schema: "public")

await myChannel.subscribe()

for await change in changes {
  switch change {
  case .insert(let action): print(action.record)
  case .update(let action): print(action.record)
  case .delete(let action): print(action.oldRecord)
  case .select(let action): print(action.record)
  }
}
</TabPanel> </Tabs> </TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">

Pass the action type to select the event.

<Tabs scrollable size="small" type="underlined" defaultActiveId="insert" queryGroup="pg-changes-event"

<TabPanel id="insert" label="Insert">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public")

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> <TabPanel id="update" label="Update">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public")

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> <TabPanel id="delete" label="Delete">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Delete>(schema = "public")

changes
    .onEach {
        println(it.oldRecord)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> <TabPanel id="all" label="All events">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction>(schema = "public")

changes
    .onEach {
        when (it) { //You can also check for <is PostgresAction.Insert>, etc.. manually
            is HasRecord -> println(it.record)
            is HasOldRecord -> println(it.oldRecord)
            else -> println(it)
        }
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </Tabs> </TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">

<Tabs scrollable size="small" type="underlined" defaultActiveId="insert" queryGroup="pg-changes-event"

<TabPanel id="insert" label="Insert">
python
changes = supabase.channel('schema-db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> <TabPanel id="update" label="Update">
python
changes = supabase.channel('schema-db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> <TabPanel id="delete" label="Delete">
python
changes = supabase.channel('schema-db-changes').on_postgres_changes(
  "DELETE",
  schema="public",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> <TabPanel id="all" label="All events">
python
changes = supabase.channel('schema-db-changes').on_postgres_changes(
  "*",
  schema="public",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </Tabs> </TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">

Pass the ListenType to select the event.

<Tabs scrollable size="small" type="underlined" defaultActiveId="insert" queryGroup="pg-changes-event"

<TabPanel id="insert" label="Insert">
c#
var channel = supabase.Realtime.Channel("schema-db-changes");

channel.Register(new PostgresChangesOptions("public", eventType: ListenType.Inserts));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> <TabPanel id="update" label="Update">
c#
var channel = supabase.Realtime.Channel("schema-db-changes");

channel.Register(new PostgresChangesOptions("public", eventType: ListenType.Updates));
channel.AddPostgresChangeHandler(ListenType.Updates, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> <TabPanel id="delete" label="Delete">
c#
var channel = supabase.Realtime.Channel("schema-db-changes");

channel.Register(new PostgresChangesOptions("public", eventType: ListenType.Deletes));
channel.AddPostgresChangeHandler(ListenType.Deletes, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> <TabPanel id="all" label="All events">
c#
var channel = supabase.Realtime.Channel("schema-db-changes");

channel.Register(new PostgresChangesOptions("public", eventType: ListenType.All));
channel.AddPostgresChangeHandler(ListenType.All, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </Tabs> </TabPanel> </$Show> </Tabs>

The channel name can be any string except 'realtime'.

Listening to specific tables

Subscribe to specific table events using the table parameter:

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">
js
const changes = supabase
  .channel('table-db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'todos',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('table-db-changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        table: 'todos',
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swfit"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(AnyAction.self, schema: "public", table: "todos")

await myChannel.subscribe()

for await change in changes {
  switch change {
  case .insert(let action): print(action)
  case .update(let action): print(action)
  case .delete(let action): print(action)
  case .select(let action): print(action)
  }
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction>(schema = "public") {
    table = "todos"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="todos",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("table-db-changes");

channel.Register(new PostgresChangesOptions("public", "todos"));
channel.AddPostgresChangeHandler(ListenType.All, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

The channel name can be any string except 'realtime'.

Listening to multiple changes

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">
js
const channel = supabase
  .channel('db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'messages',
    },
    (payload) => console.log(payload)
  )
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'users',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('db-changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        table: 'messages',
        callback: (payload) => print(payload))
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'users',
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let messageChanges = await myChannel.postgresChange(AnyAction.self, schema: "public", table: "messages")
let userChanges = await myChannel.postgresChange(InsertAction.self, schema: "public", table: "users")

await myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")
val messageChanges = myChannel.postgresChangeFlow<PostgresAction>(schema = "public") {
    table = "messages"
}
val userChanges = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
    table = "users"
}
myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "*",
  schema="public",
  table="messages",
  callback=lambda payload: print(payload)
).on_postgres_changes(
  "INSERT",
  schema="public",
  table="users",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("db-changes");

channel.Register(new PostgresChangesOptions("public", "messages"));
channel.Register(new PostgresChangesOptions("public", "users", eventType: ListenType.Inserts));

channel.AddPostgresChangeHandler(ListenType.All, (sender, change) => Console.WriteLine(change.Payload));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) => Console.WriteLine(change.Payload));

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

Filtering for specific changes

Use the filter parameter for granular changes:

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">
js
const changes = supabase
  .channel('table-filter-changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'todos',
      filter: 'id=eq.1',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
  supabase
      .channel('table-filter-changes')
      .onPostgresChanges(
          event: PostgresChangeEvent.insert,
          schema: 'public',
          table: 'todos',
          filter: PostgresChangeFilter(
            type: PostgresChangeFilterType.eq,
            column: 'id',
            value: 1,
          ),
          callback: (payload) => print(payload))
      .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "todos",
  filter: .eq("id", value: 1)
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
    table = "todos"
    filter = "id=eq.1"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  table="todos",
  filter="id=eq.1",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("table-filter-changes");

channel.Register(new PostgresChangesOptions("public", "todos", ListenType.Inserts, "id=eq.1"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

Available filters

Realtime offers filters so you can specify the data your client receives at a more granular level. A filter is a column=operator.value expression (for example id=eq.1 or title=like.%foo%) that Realtime evaluates on the server, so filtered-out events never leave the database.

The following operators are available:

OperatorMatches when the column…Example
eqequals the valueid=eq.1
neqdoes not equal the valuestatus=neq.done
lt / lteis less than / less than or equal toage=lt.65
gt / gteis greater than / greater than or equal toquantity=gte.10
inis one of a list (max 100 values)name=in.(red,blue)
like / ilikematches a pattern (case-sensitive / insensitive)title=like.%foo%
match / imatchmatches a POSIX regex (case-sensitive / insensitive)slug=match.^post-
isIS null / true / false / unknowndeleted_at=is.null
isdistinctis distinct from the value (NULL-safe !=)state=isdistinct.active

You can also negate any operator with not. and combine multiple conditions with commas (applied as an AND).

<$Show if="sdk:js">

<Admonition type="note">

In JavaScript you can pass a raw filter string, or build one with the type-safe postgresChangesFilter() helper, which handles operator names, negation, AND composition, and escaping for you:

js
import { postgresChangesFilter } from '@supabase/supabase-js'

// → 'quantity=gte.10,status=eq.open'
const filter = postgresChangesFilter().gte('quantity', 10).eq('status', 'open')
</Admonition>

</$Show>

Equal to (eq)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'messages',
      filter: postgresChangesFilter().eq('body', 'hey'),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'messages',
      filter: 'body=eq.hey',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.update,
        schema: 'public',
        table: 'messages',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.eq,
          column: 'body',
          value: 'hey',
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  UpdateAction.self,
  schema: "public",
  table: "messages",
  filter: .eq("body", value: "hey")
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "messages"
    filter = "body=eq.hey"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="messages",
  filter="body=eq.hey",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "messages", ListenType.Updates, "body=eq.hey"));
channel.AddPostgresChangeHandler(ListenType.Updates, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

This filter uses Postgres's = filter.

Not equal to (neq)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: postgresChangesFilter().neq('body', 'bye'),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: 'body=neq.bye',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'messages',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.neq,
          column: 'body',
          value: 'bye',
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  UpdateAction.self,
  schema: "public",
  table: "messages",
  filter: .neq("body", value: "hey")
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.realtime.createChannel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "messages"
    filter = "body=neq.bye"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

supabase.realtime.connect()
myChannel.join()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  table="messages",
  filter="body=neq.bye",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "messages", ListenType.Inserts, "body=neq.bye"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

This filter uses Postgres's != filter.

Less than (lt)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'profiles',
      filter: postgresChangesFilter().lt('age', 65),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'profiles',
      filter: 'age=lt.65',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'profiles',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.lt,
          column: 'age',
          value: 65,
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "profiles",
  filter: .lt("age", value: 65)
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
    table = "profiles"
    filter = "age=lt.65"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  table="profiles",
  filter="age=lt.65",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "profiles", ListenType.Inserts, "age=lt.65"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

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

Less than or equal to (lte)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'profiles',
      filter: postgresChangesFilter().lte('age', 65),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'profiles',
      filter: 'age=lte.65',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'profiles',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.lte,
          column: 'age',
          value: 65,
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "profiles",
  filter: .lte("age", value: 65)
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "profiles"
    filter = "age=lte.65"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="profiles",
  filter="age=lte.65",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "profiles", ListenType.Updates, "age=lte.65"));
channel.AddPostgresChangeHandler(ListenType.Updates, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

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

Greater than (gt)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'products',
      filter: postgresChangesFilter().gt('quantity', 10),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'products',
      filter: 'quantity=gt.10',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'products',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.gt,
          column: 'quantity',
          value: 10,
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "products",
  filter: .gt("quantity", value: 10)
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "products"
    filter = "quantity=gt.10"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="products",
  filter="quantity=gt.10",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "products", ListenType.Inserts, "quantity=gt.10"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

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

Greater than or equal to (gte)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'products',
      filter: postgresChangesFilter().gte('quantity', 10),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'products',
      filter: 'quantity=gte.10',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'products',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.gte,
          column: 'quantity',
          value: 10,
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "products",
  filter: .gte("quantity", value: 10)
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "products"
    filter = "quantity=gte.10"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="products",
  filter="quantity=gte.10",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "products", ListenType.Inserts, "quantity=gte.10"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

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

Contained in list (in)

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

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'colors',
      filter: postgresChangesFilter().in('name', ['red', 'blue', 'yellow']),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'colors',
      filter: 'name=in.(red,blue,yellow)',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'colors',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.inFilter,
          column: 'name',
          value: ['red', 'blue', 'yellow'],
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "products",
  filter: .in("name", values: ["red", "blue", "yellow"])
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "products"
    filter = "name=in.(red,blue,yellow)"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="products",
  filter="name=in.(red,blue,yellow)",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "colors", ListenType.Inserts, "name=in.(red,blue,yellow)"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

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

Pattern matching (like, ilike)

To listen to changes when a text column matches a pattern, use like (case-sensitive) or ilike (case-insensitive). Use % to match any sequence of characters and _ to match a single character.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'articles',
      // matches "Breaking News", "BREAKING", ...
      filter: postgresChangesFilter().ilike('title', '%breaking%'),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'articles',
      filter: 'title=ilike.%breaking%', // matches "Breaking News", "BREAKING", ...
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'articles',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.ilike,
          column: 'title',
          value: '%breaking%',
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "articles",
  filter: .ilike("title", value: "%breaking%")
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
    table = "articles"
    filter = "title=ilike.%breaking%"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  table="articles",
  filter="title=ilike.%breaking%",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "articles", ListenType.Inserts, "title=ilike.%breaking%"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

like uses Postgres's LIKE and ilike uses ILIKE. Both require a text-compatible column. The examples above use ilike; swap in like for case-sensitive matching—usage is otherwise identical.

Regular expression matching (match, imatch)

To listen to changes when a text column matches a POSIX regular expression, use match (case-sensitive) or imatch (case-insensitive).

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts',
      // matches "post-1", "post-42", ...
      filter: postgresChangesFilter().match('slug', '^post-\\d+$'),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts',
      filter: 'slug=match.^post-\\d+$', // matches "post-1", "post-42", ...
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'posts',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.match,
          column: 'slug',
          value: r'^post-\d+$',
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "posts",
  filter: .match("slug", value: "^post-\\d+$")
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
    table = "posts"
    filter = "slug=match.^post-\\d+$"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  table="posts",
  filter="slug=match.^post-\\d+$",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "posts", ListenType.Inserts, @"slug=match.^post-\d+$"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

match uses Postgres's ~ operator and imatch uses ~*. Both require a text-compatible column, and the pattern is validated when you subscribe. The examples above use match; swap in imatch for case-insensitive matching—usage is otherwise identical.

Null and boolean checks (is)

To listen to changes when a column IS null, true, false, or unknown, use is. is.null works on any column type; is.true, is.false, and is.unknown require a boolean column.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'todos',
      // only rows that are not yet completed
      filter: postgresChangesFilter().is('completed_at', null),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'todos',
      filter: 'completed_at=is.null', // only rows that are not yet completed
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.update,
        schema: 'public',
        table: 'todos',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.isFilter,
          column: 'completed_at',
          value: null,
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  UpdateAction.self,
  schema: "public",
  table: "todos",
  filter: .is("completed_at", value: .null)
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "todos"
    filter = "completed_at=is.null"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="todos",
  filter="completed_at=is.null",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "todos", ListenType.Updates, "completed_at=is.null"));
channel.AddPostgresChangeHandler(ListenType.Updates, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

This filter uses Postgres's IS operator.

Distinct from (isdistinct)

isdistinct is a NULL-safe inequality (IS DISTINCT FROM). Unlike neq, it treats null as a comparable value, so a null column is considered distinct from a non-null value.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'orders',
      // includes rows where status is null
      filter: postgresChangesFilter().isDistinct('status', 'shipped'),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'orders',
      filter: 'status=isdistinct.shipped', // includes rows where status is null
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.update,
        schema: 'public',
        table: 'orders',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.isDistinct,
          column: 'status',
          value: 'shipped',
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  UpdateAction.self,
  schema: "public",
  table: "orders",
  filter: .isDistinct("status", value: "shipped")
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "orders"
    filter = "status=isdistinct.shipped"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="orders",
  filter="status=isdistinct.shipped",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "orders", ListenType.Updates, "status=isdistinct.shipped"));
channel.AddPostgresChangeHandler(ListenType.Updates, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

Negating a filter (not)

Prefix any operator with not. to invert it — for example not.in, not.is, or not.like.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts',
      // anything except drafts and archived
      filter: postgresChangesFilter().not('status', 'in', ['draft', 'archived']),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts',
      filter: 'status=not.in.(draft,archived)', // anything except drafts and archived
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">

Set negate: true on a PostgresChangeFilter to apply the not. prefix.

dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        table: 'posts',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.inFilter,
          column: 'status',
          value: ['draft', 'archived'],
          negate: true,
        ),
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">

Wrap any single-condition filter in .not(...).

swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  AnyAction.self,
  schema: "public",
  table: "posts",
  filter: .not(.in("status", values: ["draft", "archived"]))
)

await myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction>(schema = "public") {
    table = "posts"
    filter = "status=not.in.(draft,archived)"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "*",
  schema="public",
  table="posts",
  filter="status=not.in.(draft,archived)",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "posts", ListenType.All, "status=not.in.(draft,archived)"));
channel.AddPostgresChangeHandler(ListenType.All, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

Combining filters with AND

Combine multiple conditions by separating them with commas. All conditions must match (logical AND). You can only combine conditions with ANDOR is not supported.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

<Tabs scrollable size="small" type="underlined" defaultActiveId="builder" queryGroup="realtime-js-filter"

<TabPanel id="builder" label="Builder">

The builder composes conditions and escapes reserved characters for you.

js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'orders',
      // amount > 100 AND status = "open"
      filter: postgresChangesFilter().gt('amount', 100).eq('status', 'open'),
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <TabPanel id="string" label="Filter string">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'orders',
      filter: 'amount=gt.100,status=eq.open', // amount > 100 AND status = "open"
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> </Tabs> </TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">

Pass a list of filters to filters to combine them with AND.

dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'orders',
        filters: [
          PostgresChangeFilter(
            type: PostgresChangeFilterType.gt,
            column: 'amount',
            value: 100,
          ),
          PostgresChangeFilter(
            type: PostgresChangeFilterType.eq,
            column: 'status',
            value: 'open',
          ),
        ],
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">

Use .and([...]) to combine multiple conditions.

swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  InsertAction.self,
  schema: "public",
  table: "orders",
  filter: .and([
    .gt("amount", value: 100),
    .eq("status", value: "open"),
  ])
)

await myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Insert>(schema = "public") {
    table = "orders"
    filter = "amount=gt.100,status=eq.open"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('db-changes').on_postgres_changes(
  "INSERT",
  schema="public",
  table="orders",
  filter="amount=gt.100,status=eq.open",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
var channel = supabase.Realtime.Channel("changes");

channel.Register(new PostgresChangesOptions("public", "orders", ListenType.Inserts, "amount=gt.100,status=eq.open"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs> <Admonition type="note">

Values that contain reserved characters (,, (, ), ", or \) must be double-quoted PostgREST-style so the server doesn't read them as condition or list boundaries — for example name=eq."Doe, Jane". The postgresChangesFilter() builder (JavaScript), PostgresChangeFilter (Dart), and RealtimePostgresFilter (Swift) apply this quoting for you.

</Admonition>

Selecting specific columns

By default each change event contains the full row. Use select to receive only a subset of columns instead. This reduces payload size and the data transferred per event, which is especially useful for tables with large bytea, jsonb, or text columns.

The listed columns must be selectable by the subscribing role, and the table's primary key is always included so you can identify the row. select requires an explicit schema and table — it's not supported on wildcard subscriptions.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">
js
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'profiles',
      select: ['id', 'username'], // payload.new only contains { id, username }
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase
    .channel('changes')
    .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        table: 'profiles',
        select: ['id', 'username'],
        callback: (payload) => print(payload))
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  AnyAction.self,
  schema: "public",
  table: "profiles",
  select: ["id", "username"]
)

await myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
changes = supabase.channel('changes').on_postgres_changes(
  "*",
  schema="public",
  table="profiles",
  select=["id", "username"],
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> </Tabs>

Receiving old records

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:

sql
alter table
  messages replica identity full;
<Admonition type="caution">

RLS policies are not applied to DELETE statements, because there is no way for Postgres to verify that a user has access to a deleted record.

</Admonition>

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:

sql
grant select on "non_private_schema"."some_table" to authenticated;
<Admonition type="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.

</Admonition>

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 in the Settings > API keys section of the Dashboard.

<Admonition type="caution">

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

</Admonition>

To use your own JWT with Realtime make sure to set the token after instantiating the Supabase client and before connecting to a Channel.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">
js
const { createClient } = require('@supabase/supabase-js')

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY, {})

// Set your custom JWT here
supabase.realtime.setAuth('your-custom-jwt')

const channel = supabase
  .channel('db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'messages',
      filter: 'body=eq.bye',
    },
    (payload) => console.log(payload)
  )
  .subscribe()
</TabPanel> <$Show if="sdk:dart"> <TabPanel id="dart" label="Dart">
dart
supabase.realtime.setAuth('your-custom-jwt');

supabase
    .channel('db-changes')
    .onPostgresChanges(
      event: PostgresChangeEvent.all,
      schema: 'public',
      table: 'messages',
      filter: PostgresChangeFilter(
        type: PostgresChangeFilterType.eq,
        column: 'body',
        value: 'bye',
      ),
      callback: (payload) => print(payload),
    )
    .subscribe();
</TabPanel> </$Show> <$Show if="sdk:swift"> <TabPanel id="swift" label="Swift">
swift
await supabase.realtime.setAuth("your-custom-jwt")

let myChannel = await supabase.channel("db-changes")

let changes = await myChannel.postgresChange(
  UpdateAction.self,
  schema: "public",
  table: "products",
  filter: "name=in.(red,blue,yellow)"
)

await myChannel.subscribe()

for await change in changes {
  print(change.record)
}
</TabPanel> </$Show> <$Show if="sdk:kotlin"> <TabPanel id="kotlin" label="Kotlin">
kotlin
val supabase = createSupabaseClient(supabaseUrl, supabaseKey) {
	install(Realtime) {
		jwtToken = "your-custom-jwt"
	}
}
val myChannel = supabase.channel("db-changes")

val changes = myChannel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
    table = "products"
    filter = "name=in.(red,blue,yellow)"
}

changes
    .onEach {
        println(it.record)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe()
</TabPanel> </$Show> <$Show if="sdk:python"> <TabPanel id="python" label="Python">
python
supabase.realtime.set_auth('your-custom-jwt')

changes = supabase.channel('db-changes').on_postgres_changes(
  "UPDATE",
  schema="public",
  table="products",
  filter="name=in.(red,blue,yellow)",
  callback=lambda payload: print(payload)
).subscribe()
</TabPanel> </$Show> <$Show if="sdk:csharp"> <TabPanel id="csharp" label="C#">
c#
// Set your custom JWT after instantiating the client and before subscribing to a channel
supabase.Realtime.SetAuth("your-custom-jwt");

var channel = supabase.Realtime.Channel("db-changes");

channel.Register(new PostgresChangesOptions("public", "messages", ListenType.All, "body=eq.bye"));
channel.AddPostgresChangeHandler(ListenType.All, (sender, change) =>
{
    Console.WriteLine(change.Payload);
});

await channel.Subscribe();
</TabPanel> </$Show> </Tabs>

Limitations

Delete events

You can only filter Delete events when tracking Postgres Changes if the table has the replica identity set to full. See Receiving old records.

Scaling Postgres Changes

Postgres Changes authorizes every event against each subscriber. When you make a single change to a table with 100 subscribed users, Realtime performs 100 authorization checks — one per user — so throughput scales with the number of subscribers, not the write rate. Changes are also processed on a single thread to preserve their order, which means larger compute add-ons don't meaningfully increase Postgres Changes throughput.

For most applications this is plenty. To get the best performance:

Use the estimator below to gauge the maximum throughput for your instance, and run your own benchmarks to confirm it fits your use case:

<RealtimeLimitsEstimator /> <Admonition type="note">

If you expect more than ~3,000 concurrent subscribers on the same changes, use Broadcast to stream database changes instead. Broadcast sends each change once and fans it out to all subscribers, so it scales to far higher connection counts than per-subscriber authorization allows.

</Admonition>

If you're unsure which approach fits your use case, reach out through the Support Form — our engineers are happy to help you find the best solution.