Realtime DataProvider Requirements

ra-realtime provides helper functions to add real-time capabilities to an existing data provider if you use the following real-time backends:

For other backends, you’ll need to write your own implementation. Check the Writing a custom adapter section below for more information.

Realtime Methods & Signature

To enable real-time features, the dataProvider must implement three new methods:

  • subscribe(topic, callback)
  • unsubscribe(topic, callback)
  • publish(topic, event) (optional - publication is often done server-side)

These methods should return an empty Promise resolved when the action was acknowledged by the real-time bus.

In addition, to support the lock features, the dataProvider must implement 4 more methods:

  • lock(resource, { id, identity, meta })
  • unlock(resource, { id, identity, meta })
  • getLock(resource, { id, meta })
  • getLocks(resource, { meta })

Supabase

The ra-realtime package contains a function augmenting a regular (API-based) dataProvider with real-time methods based on the capabilities of Supabase.

This adapter subscribes to Postgres Changes, and transforms the events into the format expected by ra-realtime.

import { createClient } from '@supabase/supabase-js';
import { supabaseDataProvider } from 'ra-supabase';
import { addRealTimeMethodsBasedOnSupabase, ListLive } from '@react-admin/ra-realtime';
import { Admin, Resource, Datagrid, TextField, EmailField } from 'react-admin';

const supabaseClient = createClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_ANON_KEY
);

const dataProvider = supabaseDataProvider({
    instanceUrl: process.env.SUPABASE_URL,
    apiKey: process.env.SUPABASE_ANON_KEY,
    supabaseClient
});

const realTimeDataProvider = addRealTimeMethodsBasedOnSupabase({
    dataProvider,
    supabaseClient,
});

export const App = () => (
    <Admin dataProvider={realTimeDataProvider}>
        <Resource name="sales" list={SaleList} />
    </Admin>
);

const SaleList = () => (
    <ListLive>
        <Datagrid rowClick="edit">
            <TextField source="id" />
            <TextField source="first_name" />
            <TextField source="last_name" />
            <EmailField source="email" />
        </Datagrid>
    </ListLive>
);

Tip: Realtime features are not enabled in Supabase by default, you need to enable them. This can be done either from the Replication section of your Supabase Dashboard, or by running the following SQL query with the SQL Editor:

begin;

-- remove the supabase_realtime publication
drop
  publication if exists supabase_realtime;

-- re-create the supabase_realtime publication with no tables
create publication supabase_realtime;

commit;

-- add a table to the publication
alter
  publication supabase_realtime add table sales;
alter
  publication supabase_realtime add table contacts;
alter
  publication supabase_realtime add table contactNotes;

Have a look at the Supabase Replication Setup documentation section for more info.

addRealTimeMethodsBasedOnSupabase accepts the following parameters:

Prop Required Type Default Description
dataProvider Required DataProvider - The base dataProvider to augment with realtime methods
supabaseClient Required SupabaseClient - The Supabase JS Client

Tip: You may choose to sign your own tokens to customize claims that can be checked in your RLS policies. In order to use these custom tokens with addRealTimeMethodsBasedOnSupabase, you must pass an apikey field in both Realtime’s headers and params when creating the supabaseClient. Please follow the instructions from the Supabase documentation for more information about how to do so.

API-Platform

The ra-realtime package contains a function augmenting a regular (API-based) dataProvider with real-time methods based on the capabilities of API-Platform. Use it as follows:

import { Datagrid, EditButton, ListProps } from 'react-admin';
import {
    HydraAdmin,
    ResourceGuesser,
    FieldGuesser,
    hydraDataProvider,
} from '@api-platform/admin';
import {
    ListLive,
    addRealTimeMethodsBasedOnApiPlatform,
} from '@react-admin/ra-realtime';

const dataProvider = hydraDataProvider('https://localhost:8443');
const dataProviderWithRealtime = addRealTimeMethodsBasedOnApiPlatform(
    // The original dataProvider (should be a hydra data provider passed by API-Platform)
    dataProvider,
    // The API-Platform Mercure Hub URL
    'https://localhost:1337/.well-known/mercure',
    // JWT token to authenticate against the API-Platform Mercure Hub
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.obDjwCgqtPuIvwBlTxUEmibbBf0zypKCNzNKP7Op2UM',
    // The topic URL used by API-Platform (without a slash at the end)
    'https://localhost:8443'
);

const App = () => (
    <HydraAdmin
        entrypoint="https://localhost:8443"
        dataProvider={dataProviderWithRealtime}
    >
        <ResourceGuesser name="greetings" list={GreetingsList} />
    </HydraAdmin>
);


// Example for connecting a list of greetings
const GreetingsList = () => (
    <ListLive>
        <Datagrid>
            <FieldGuesser source="name" />
            <EditButton />
        </Datagrid>
    </ListLive>
);

Mercure

The ra-realtime package contains a function augmenting a regular (API-based) dataProvider with real-time methods based on a Mercure hub. Use it as follows:

import { addRealTimeMethodsBasedOnMercure } from '@react-admin/ra-realtime';

const dataProviderWithRealtime = addRealTimeMethodsBasedOnMercure(
    // original dataProvider
    dataProvider,
    // Mercure hub URL
    'http://path.to.my.api/.well-known/mercure',
    // JWT token to authenticate against the Mercure Hub
    'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiKiJdfX0.SWKHNF9wneXTSjBg81YN5iH8Xb2iTf_JwhfUY5Iyhsw'
);

const App = () => (
    <Admin dataProvider={dataProviderWithRealtime}>{/* ... */}</Admin>
);

Writing a Custom Adapter

If you’re using another transport for real-time messages (WebSockets, long polling, GraphQL subscriptions, etc.), you’ll have to implement subscribe, unsubscribe, and publish yourself in your dataProvider. As an example, here is an implementation using a local variable, that ra-realtime uses in tests:

let subscriptions = [];

const dataProvider = {
    // regular dataProvider methods like getList, getOne, etc,
    // ...
    subscribe: async (topic, subscriptionCallback) => {
        subscriptions.push({ topic, subscriptionCallback });
        return Promise.resolve({ data: null });
    },

    unsubscribe: async (topic, subscriptionCallback) => {
        subscriptions = subscriptions.filter(
            subscription =>
                subscription.topic !== topic ||
                subscription.subscriptionCallback !== subscriptionCallback
        );
        return Promise.resolve({ data: null });
    },

    publish: (topic, event) => {
        if (!topic) {
            return Promise.reject(new Error('missing topic'));
        }
        if (!event.type) {
            return Promise.reject(new Error('missing event type'));
        }
        subscriptions.map(
            subscription =>
                topic === subscription.topic &&
                subscription.subscriptionCallback(event)
        );
        return Promise.resolve({ data: null });
    },
};

You can check the behavior of the real-time components by using the default console logging provided in addRealTimeMethodsInLocalBrowser.

Topic And Event Format

You’ve noticed that all the dataProvider real-time methods expect a topic as the first argument. A topic is just a string, identifying a particular real-time channel. Topics can be used e.g. to dispatch messages to different rooms in a chat application or to identify changes related to a particular record.

Most ra-realtime components deal with CRUD logic, so ra-realtime subscribes to special topics named resource/[name] and resource/[name]/[id]. For your own events, use any topic you want.

The event is the name of the message sent from publishers to subscribers. An event should be a JavaScript object with a type and a payload field.

Here is an example event:

{
    type: 'created',
    payload: 'New message',
}

For CRUD operations, ra-realtime expects events to use the types ‘created’, ‘updated’, and ‘deleted’.

CRUD Events

Ra-realtime has deep integration with react-admin, where most of the logic concerns Creation, Update or Deletion (CRUD) of records. To enable this integration, your real-time backend should publish the following events:

  • when a new record is created:
{
    topic: `resource/${resource}`,
    event: {
        type: 'created',
        payload: { ids: [id]},
    },
}
  • when a record is updated:
{
    topic: `resource/${resource}/id`,
    event: {
        type: 'updated',
        payload: { ids: [id]},
    },
}
{
    topic: `resource/${resource}`,
    event: {
        type: 'updated',
        payload: { ids: [id]},
    },
}
  • when a record is deleted:
{
    topic: `resource/${resource}/id`,
    event: {
        type: 'deleted',
        payload: { ids: [id]},
    },
}
{
    topic: `resource/${resource}`,
    event: {
        type: 'deleted',
        payload: { ids: [id]},
    },
}

Lock Format

A lock stores the record that is locked, the identity of the locker, and the time at which the lock was acquired. It is used to prevent concurrent editing of the same record. A typical lock looks like this:

{
    resource: 'posts',
    recordId: 123,
    identity: 'julien',
    createdAt: '2023-01-02T21:36:35.133Z',
}

The dataProvider.getLock() and dataProvider.getLocks() methods should return these locks.

As for the mutation methods (dataProvider.lock(), dataProvider.unlock()), they expect the following parameters:

  • resource: the resource name (e.g. 'posts')
  • params: an object containing the following
    • id: the record id (e.g. 123)
    • identity: an identifier (string or number) corresponding to the identity of the locker (e.g. 'julien'). This could be an authentication token for instance.
    • meta: an object that will be forwarded to the dataProvider (optional)

Locks Based On A Lock Resource

The ra-realtime package offers a function augmenting a regular (API-based) dataProvider with locks methods based on a locks resource.

It will translate a dataProvider.getLocks() call to a dataProvider.getList('locks') call, and a dataProvider.lock() call to a dataProvider.create('locks') call.

The lock resource should contain the following fields:

{
    "id": 123,
    "identity": "Toad",
    "resource": "people",
    "recordId": 18,
    "createdAt": "2020-09-29 10:20"
}

Please note that the identity and the createdAt formats depend on your API.

Here is how to use it in your react-admin application:

import { Admin } from 'react-admin';
import { addLocksMethodsBasedOnALockResource } from '@react-admin/ra-realtime';

const dataProviderWithLocks = addLocksMethodsBasedOnALockResource(
    dataProvider // original dataProvider
);

const App = () => (
    <Admin dataProvider={dataProviderWithLocks}>{/* ... */}</Admin>
);

Calling the dataProvider Methods Directly

Once you’ve set a real-time dataProvider in your <Admin>, you can call the real-time methods in your React components via the useDataProvider hook.

For instance, here is a component displaying messages posted to the ‘messages’ topic in real time:

import React, { useState } from 'react';
import { useDataProvider, useNotify } from 'react-admin';

const MessageList = () => {
    const notify = useNotify();
    const [messages, setMessages] = useState([]);
    const dataProvider = useDataProvider();

    useEffect(() => {
        const callback = event => {
            // event is like
            // {
            //     topic: 'messages',
            //     type: 'created',
            //     payload: 'New message',
            // }
            setMessages(messages => [...messages, event.payload]);
            notify('New message');
        };
        // subscribe to the 'messages' topic on mount
        dataProvider.subscribe('messages', callback);
        // unsubscribe on unmount
        return () => dataProvider.unsubscribe('messages', callback);
    }, [setMessages, notify, dataProvider]);

    return (
        <ul>
            {messages.map((message, index) => (
                <li key={index}>{message}</li>
            ))}
        </ul>
    );
};

And here is a button for publishing an event to the messages topic. All the subscribers to this topic will execute their callback:

import React from 'react';
import { useDataProvider, useNotify } from 'react-admin';

const SendMessageButton = () => {
    const dataProvider = useDataProvider();
    const notify = useNotify();
    const handleClick = () => {
        dataProvider
            .publish('messages', { type: 'created', payload: 'New message' })
            .then(() => notify('Message sent'));
    };

    return <Button onClick={handleClick}>Send new message</Button>;
};

Tip: You should not need to call publish() directly very often. Most real-time backends publish events in reaction to a change in the data. So the previous example is fictive. In reality, a typical <SendMessageButton> would simply call dataProvider.create('messages'), and the API would create the new message AND publish the ‘created’ event to the real-time bus.