Skip to main content
Refine’s live provider system enables real-time updates across your application. When data changes on the server, all connected clients automatically receive updates and refresh their views.

Understanding Live Providers

A live provider handles pub/sub functionality for real-time updates:
const liveProvider = {
  subscribe: ({ channel, types, params, callback, meta }) => any,
  unsubscribe: (subscription) => void,
  publish: ({ channel, type, payload, date, meta }) => void,
};

Supported Solutions

Refine has built-in integrations with:
  • Ably - Real-time messaging platform
  • Supabase - PostgreSQL real-time subscriptions
  • Hasura - GraphQL subscriptions
  • Appwrite - Real-time API
  • Custom - Build your own with WebSockets, SSE, etc.

Quick Setup

1
Choose a Live Provider
2
# Ably
npm install @refinedev/ably ably

# Supabase (already includes live features)
npm install @refinedev/supabase

# For custom WebSocket implementation
npm install socket.io-client
3
Configure the Provider
4
Ably
import { Refine } from "@refinedev/core";
import { liveProvider } from "@refinedev/ably";
import { Ably } from "ably";

const ablyClient = new Ably.Realtime("YOUR_API_KEY");

const App = () => (
  <Refine
    liveProvider={liveProvider(ablyClient)}
    liveMode="auto"
    options={{ liveMode: "auto" }}
  >
    {/* Your app */}
  </Refine>
);
Supabase
import { Refine } from "@refinedev/core";
import { liveProvider, dataProvider } from "@refinedev/supabase";
import { createClient } from "@supabase/supabase-js";

const supabaseClient = createClient(
  "YOUR_SUPABASE_URL",
  "YOUR_SUPABASE_KEY"
);

const App = () => (
  <Refine
    dataProvider={dataProvider(supabaseClient)}
    liveProvider={liveProvider(supabaseClient)}
    options={{ liveMode: "auto" }}
  >
    {/* Your app */}
  </Refine>
);
Custom WebSocket
import { Refine, LiveProvider } from "@refinedev/core";
import { io, Socket } from "socket.io-client";

const socket = io("http://localhost:3000");

const liveProvider: LiveProvider = {
  subscribe: ({ channel, types, params, callback }) => {
    const listener = (event: any) => {
      if (types.includes(event.type) || types.includes("*")) {
        callback(event);
      }
    };

    socket.on(channel, listener);

    return {
      unsubscribe: () => {
        socket.off(channel, listener);
      },
    };
  },

  unsubscribe: (subscription) => {
    subscription.unsubscribe();
  },

  publish: ({ channel, type, payload, date }) => {
    socket.emit(channel, {
      type,
      payload,
      date,
    });
  },
};

const App = () => (
  <Refine
    liveProvider={liveProvider}
    options={{ liveMode: "auto" }}
  >
    {/* Your app */}
  </Refine>
);
5
Enable Live Mode
6
Configure globally or per-hook:
7
// Global configuration
<Refine
  liveProvider={liveProvider}
  options={{
    liveMode: "auto", // "auto" | "manual" | "off"
  }}
/>

// Per-hook configuration
useList({
  resource: "posts",
  liveMode: "auto",
});

Live Modes

Auto Mode

Automatically refetch data when updates occur:
const PostList = () => {
  const { data, isLoading } = useList({
    resource: "posts",
    liveMode: "auto",
  });

  // When a post is created/updated/deleted:
  // 1. Live provider receives event
  // 2. Query is automatically invalidated
  // 3. Data is refetched
  // 4. Component re-renders with fresh data

  return (
    <div>
      {data?.data.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
};

Manual Mode

Receive events without automatic refetch:
const PostList = () => {
  const [notification, setNotification] = useState<string | null>(null);

  const { data, refetch } = useList({
    resource: "posts",
    liveMode: "manual",
    onLiveEvent: (event) => {
      // Show notification instead of auto-refetch
      setNotification(`New ${event.type} event received`);
    },
  });

  return (
    <div>
      {notification && (
        <div>
          {notification}
          <button onClick={() => refetch()}>Refresh</button>
          <button onClick={() => setNotification(null)}>Dismiss</button>
        </div>
      )}
      
      {data?.data.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
};

Off Mode

Disable live updates:
useList({
  resource: "posts",
  liveMode: "off",
});

Using Live Hooks

In Lists

import { useTable } from "@refinedev/core";

const PostList = () => {
  const { tableQuery } = useTable({
    resource: "posts",
    liveMode: "auto",
  });

  // Table automatically updates when:
  // - New posts are created
  // - Existing posts are updated
  // - Posts are deleted

  return <Table data={tableQuery.data?.data} />;
};

In Detail Views

import { useShow } from "@refinedev/core";

const PostShow = () => {
  const { queryResult } = useShow({
    resource: "posts",
    liveMode: "auto",
  });

  const post = queryResult.data?.data;

  // Automatically updates when this specific post changes

  return (
    <div>
      <h1>{post?.title}</h1>
      <p>{post?.content}</p>
    </div>
  );
};

In Forms

import { useForm } from "@refinedev/core";

const PostEdit = () => {
  const { query } = useForm({
    action: "edit",
    liveMode: "manual",
    onLiveEvent: (event) => {
      if (event.type === "updated") {
        alert("This post was updated by another user!");
      }
    },
  });

  // Warn user if record changes while editing
};

Publishing Events

Automatic Publishing

Refine automatically publishes events when using mutation hooks:
import { useCreate, useUpdate, useDelete } from "@refinedev/core";

const PostActions = () => {
  const { mutate: create } = useCreate();
  const { mutate: update } = useUpdate();
  const { mutate: remove } = useDelete();

  const handleCreate = () => {
    create({
      resource: "posts",
      values: { title: "New Post" },
    });
    // Automatically publishes:
    // {
    //   channel: "resources/posts",
    //   type: "created",
    //   payload: { ids: ["new-post-id"] },
    //   date: new Date(),
    // }
  };

  const handleUpdate = () => {
    update({
      resource: "posts",
      id: "1",
      values: { title: "Updated Title" },
    });
    // Publishes "updated" event
  };

  const handleDelete = () => {
    remove({
      resource: "posts",
      id: "1",
    });
    // Publishes "deleted" event
  };
};

Manual Publishing

import { usePublish } from "@refinedev/core";

const CustomAction = () => {
  const publish = usePublish();

  const handleCustomAction = () => {
    // Perform custom action
    // ...

    // Manually publish event
    publish({
      channel: "resources/posts",
      type: "custom-action",
      payload: {
        ids: ["1", "2"],
        action: "archived",
      },
      date: new Date(),
    });
  };

  return <button onClick={handleCustomAction}>Archive Selected</button>;
};

Event Subscription

Subscribe to Specific Events

import { useSubscription } from "@refinedev/core";

const PostNotifications = () => {
  const [events, setEvents] = useState([]);

  useSubscription({
    channel: "resources/posts",
    types: ["created", "updated"], // Only these events
    onLiveEvent: (event) => {
      setEvents((prev) => [event, ...prev].slice(0, 10));
    },
  });

  return (
    <div>
      <h3>Recent Events</h3>
      {events.map((event, index) => (
        <div key={index}>
          {event.type} - {event.payload.ids.join(", ")}
        </div>
      ))}
    </div>
  );
};

Subscribe to Multiple Channels

const MultiChannelSubscriber = () => {
  useSubscription({
    channel: "resources/posts",
    types: ["*"],
    onLiveEvent: (event) => {
      console.log("Post event:", event);
    },
  });

  useSubscription({
    channel: "resources/comments",
    types: ["*"],
    onLiveEvent: (event) => {
      console.log("Comment event:", event);
    },
  });

  useSubscription({
    channel: "notifications",
    types: ["*"],
    onLiveEvent: (event) => {
      console.log("Notification:", event);
    },
  });
};

Filter Events by ID

const PostShow = ({ id }: { id: string }) => {
  const { queryResult } = useShow({
    resource: "posts",
    id,
    liveMode: "auto",
    // Only subscribes to events for this specific post
  });

  // Subscription:
  // {
  //   channel: "resources/posts",
  //   types: ["*"],
  //   params: { ids: [id] },
  // }
};

Advanced Patterns

Optimistic Updates

Combine with optimistic mutation mode:
const PostEdit = () => {
  const { mutate } = useUpdate({
    mutationMode: "optimistic",
  });

  const handleSave = (values: any) => {
    mutate({
      resource: "posts",
      id: "1",
      values,
      // UI updates immediately
      // Live event published
      // Other clients receive update
      // Reverts if server returns error
    });
  };
};

Presence Detection

Show who’s viewing/editing:
const PostEdit = ({ id }: { id: string }) => {
  const [activeUsers, setActiveUsers] = useState<string[]>([]);
  const { data: identity } = useGetIdentity();

  useEffect(() => {
    const channel = `presence/posts/${id}`;
    
    // Announce presence
    publish({
      channel,
      type: "join",
      payload: { userId: identity?.id, userName: identity?.name },
    });

    // Listen for other users
    const subscription = subscribe({
      channel,
      types: ["join", "leave"],
      onLiveEvent: (event) => {
        if (event.type === "join") {
          setActiveUsers((prev) => [...prev, event.payload.userId]);
        } else {
          setActiveUsers((prev) => 
            prev.filter((id) => id !== event.payload.userId)
          );
        }
      },
    });

    // Announce leave on unmount
    return () => {
      publish({
        channel,
        type: "leave",
        payload: { userId: identity?.id },
      });
      subscription.unsubscribe();
    };
  }, [id]);

  return (
    <div>
      <div>Editing: {activeUsers.length} user(s) online</div>
      {/* Form */}
    </div>
  );
};

Conflict Resolution

Handle concurrent edits:
const PostEdit = () => {
  const [hasConflict, setHasConflict] = useState(false);
  const [serverVersion, setServerVersion] = useState(null);

  const { query, mutation } = useForm({
    action: "edit",
    liveMode: "manual",
    onLiveEvent: (event) => {
      if (event.type === "updated" && mutation.isIdle) {
        // Someone else updated while we were editing
        setHasConflict(true);
        setServerVersion(event.payload.data);
      }
    },
  });

  if (hasConflict) {
    return (
      <div>
        <p>This post was updated by another user.</p>
        <button onClick={() => query.refetch()}>Load Latest</button>
        <button onClick={() => setHasConflict(false)}>Keep My Changes</button>
      </div>
    );
  }

  return <form>{/* Form fields */}</form>;
};

Typing Indicators

const CommentBox = ({ postId }: { postId: string }) => {
  const [comment, setComment] = useState("");
  const [typingUsers, setTypingUsers] = useState<string[]>([]);
  const { data: identity } = useGetIdentity();

  const debouncedPublishTyping = useMemo(
    () =>
      debounce(() => {
        publish({
          channel: `typing/posts/${postId}`,
          type: "typing",
          payload: { userId: identity?.id, userName: identity?.name },
        });
      }, 500),
    [postId, identity]
  );

  useSubscription({
    channel: `typing/posts/${postId}`,
    types: ["typing"],
    onLiveEvent: (event) => {
      if (event.payload.userId !== identity?.id) {
        setTypingUsers((prev) => {
          if (!prev.includes(event.payload.userName)) {
            return [...prev, event.payload.userName];
          }
          return prev;
        });
        
        // Remove after 3 seconds
        setTimeout(() => {
          setTypingUsers((prev) =>
            prev.filter((name) => name !== event.payload.userName)
          );
        }, 3000);
      }
    },
  });

  return (
    <div>
      <textarea
        value={comment}
        onChange={(e) => {
          setComment(e.target.value);
          debouncedPublishTyping();
        }}
      />
      
      {typingUsers.length > 0 && (
        <div>{typingUsers.join(", ")} {typingUsers.length === 1 ? "is" : "are"} typing...</div>
      )}
    </div>
  );
};

Live Notifications

const LiveNotifications = () => {
  const [notifications, setNotifications] = useState([]);
  const { data: identity } = useGetIdentity();

  useSubscription({
    channel: `notifications/user/${identity?.id}`,
    types: ["*"],
    onLiveEvent: (event) => {
      setNotifications((prev) => [
        {
          id: Date.now(),
          message: event.payload.message,
          type: event.type,
        },
        ...prev,
      ]);
    },
  });

  return (
    <div>
      {notifications.map((notification) => (
        <div key={notification.id}>
          {notification.message}
        </div>
      ))}
    </div>
  );
};

Server-Side Implementation

Event Format

Publish events in this format:
interface LiveEvent {
  channel: string;          // e.g., "resources/posts"
  type: "created" | "updated" | "deleted" | "*" | string;
  payload: {
    ids: string[];          // Affected record IDs
    [key: string]: any;     // Additional data
  };
  date: Date;
  meta?: Record<string, any>;
}

Example: Node.js + Socket.io

// server.ts
import express from "express";
import { Server } from "socket.io";
import http from "http";

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: { origin: "http://localhost:3000" },
});

io.on("connection", (socket) => {
  console.log("Client connected");

  // Client subscribes to a channel
  socket.on("subscribe", ({ channel }) => {
    socket.join(channel);
  });

  socket.on("disconnect", () => {
    console.log("Client disconnected");
  });
});

// Publish events after CRUD operations
app.post("/api/posts", async (req, res) => {
  const post = await createPost(req.body);
  
  // Notify all subscribers
  io.to("resources/posts").emit("resources/posts", {
    type: "created",
    payload: { ids: [post.id] },
    date: new Date(),
  });

  res.json(post);
});

app.put("/api/posts/:id", async (req, res) => {
  const post = await updatePost(req.params.id, req.body);
  
  io.to("resources/posts").emit("resources/posts", {
    type: "updated",
    payload: { ids: [post.id] },
    date: new Date(),
  });

  res.json(post);
});

server.listen(3001);

Best Practices

  1. Use auto mode for data views - Lists, tables, detail pages
  2. Use manual mode for critical operations - Forms, editors
  3. Filter subscriptions by ID - Reduce unnecessary updates
  4. Handle connection states - Show offline indicators
  5. Implement reconnection logic - Auto-reconnect on disconnect
  6. Debounce frequent updates - Prevent UI thrashing
  7. Clean up subscriptions - Unsubscribe on unmount
  8. Test offline scenarios - Ensure graceful degradation

Troubleshooting

Events Not Received

  1. Check live provider is configured
  2. Verify channel names match
  3. Ensure liveMode is not “off”
  4. Check network connection
  5. Verify server is publishing events

Too Many Refetches

  1. Use manual mode with controlled refetch
  2. Implement debouncing
  3. Filter subscriptions by ID
  4. Consider batching updates

Memory Leaks

  1. Always unsubscribe on unmount
  2. Clean up timers and intervals
  3. Use proper dependency arrays

Next Steps

Build docs developers (and LLMs) love