A C# client for Supabase Realtime — listen to Postgres changes, and use Broadcast and Presence over a single websocket connection. It is a C#-ification of realtime-js.
Part of the Supabase C# SDK. Most projects
use it through the Supabase meta-package (supabase.Realtime); reference
this package directly to use Realtime on its own.
dotnet add package Supabase.RealtimeTargets .NET Standard 2.0 and 2.1.
ConnectAsync() and Subscribe() are awaitable, so you can be sure a connection exists before
interacting with it. On the initial connection the client is fail-fast — ConnectAsync() throws a
RealtimeException if the socket server is unreachable. Once connected, it reconnects indefinitely
until disconnected.
using Supabase.Realtime;
var client = new Client("ws://realtime-dev.localhost:4000/socket");
await client.ConnectAsync();
// Shorthand for a postgres_changes subscription: database, schema, table
var channel = client.Channel("realtime", "public", "todos");
channel.AddPostgresChangeHandler(ListenType.Updates, (_, change) =>
{
var updated = change.Model<Todo>();
var previous = change.OldModel<Todo>();
});
await channel.Subscribe();Todo derives from Supabase.Postgrest.Models.BaseModel, letting the client coerce change payloads
into your model via change.Model<T>().
Full generated API reference: Supabase.Realtime.
Listen to inserts, updates, and deletes on a table, delivered to authorized clients according to your
Row Level Security policies. The table
must belong to the supabase_realtime publication.
More on Postgres changes.
var channel = client.Channel("public-users");
channel.Register(new PostgresChangesOptions("public", "users"));
channel.AddPostgresChangeHandler(ListenType.All, (_, change) =>
{
switch (change.Event)
{
case EventType.Insert: /* row created */ break;
case EventType.Update: /* row updated */ break;
case EventType.Delete: /* row deleted */ break;
}
});
await channel.Subscribe();Publish-subscribe messaging: a client sends messages to a named channel, and any other client subscribed to that channel receives them in real time — useful for ephemeral, high-frequency data like cursor positions. More on Broadcast.
Given a typed broadcast model:
class MouseBroadcast : BaseBroadcast<MouseStatus> { }
class MouseStatus
{
[JsonProperty("mouseX")] public float MouseX { get; set; }
[JsonProperty("mouseY")] public float MouseY { get; set; }
[JsonProperty("userId")] public string UserId { get; set; }
}Receive typed broadcast events:
var channel = client.Channel("cursor");
var broadcast = channel.Register<MouseBroadcast>(broadcastSelf: true);
broadcast.AddBroadcastEventHandler((_, _) =>
{
var state = broadcast.Current();
Debug.WriteLine($"{state.Payload.MouseX}:{state.Payload.MouseY}");
});
await channel.Subscribe();Send a broadcast event on the same broadcast handle:
await broadcast.Send("cursor", new MouseBroadcast
{
Payload = new MouseStatus { MouseX = 123, MouseY = 456 }
});Presence tracks shared state across clients using a CRDT: each client publishes its own state, and all subscribers converge on the same view. When a client disconnects, its state is removed automatically — which makes "who's online" features straightforward. More on Presence.
Given a typed presence model:
class UserPresence : BasePresence
{
[JsonProperty("lastSeen")] public DateTime LastSeen { get; set; }
}Receive presence sync events:
var presenceId = Guid.NewGuid().ToString();
var channel = client.Channel("last-seen");
var presence = channel.Register<UserPresence>(presenceId);
presence.AddPresenceEventHandler(EventType.Sync, (_, _) =>
{
foreach (var state in presence.CurrentState)
{
var userId = state.Key;
var lastSeen = state.Value.First().LastSeen;
Debug.WriteLine($"{userId}: {lastSeen}");
}
});
await channel.Subscribe();Track this client's presence:
presence.Track(new UserPresence { LastSeen = DateTime.Now });Event handlers are delegates, scoped to the object they concern — socket handlers receive
connectivity events, channel handlers receive join/leave events, and so on. Register and remove them
with the Add/Remove/Clear methods (e.g. RealtimeSocket.AddStateChangedHandler,
RealtimeChannel.AddPostgresChangeHandler, RealtimeBroadcast.AddBroadcastEventHandler).
For logging, attach a debug handler rather than relying on console output:
client.AddDebugHandler((sender, message, exception) => Debug.WriteLine(message));Observability: unlike the HTTP-based Supabase clients, Realtime is not yet instrumented for OpenTelemetry, so no websocket traces or metrics are emitted.
Contributions are welcome. See the repository root for how to build and test the SDK.
Note that the Realtime test suite expects realtime-dev.localhost to resolve locally — add a hosts
entry for 127.0.0.1 realtime-dev.localhost.