apps/docs/content/guides/realtime/presence.mdx
Use Realtime Presence to track state between multiple users.
You can use the Supabase client libraries to track Presence state between users.
Presence lets each connected client publish a small piece of state—called a “presence payload”—to a shared channel. Supabase stores each client’s payload under a unique presence key and keeps a merged view of all connected clients.
When any client subscribes, disconnects, or updates their presence payload, Supabase triggers one of three events:
sync — the full presence state has been updatedjoin — a new client has started tracking presenceleave — a client has stopped tracking presencePresence syncs state through the server and notifies all subscribers on every change. Calling track() rapidly — for example on every mouse move to share cursor positions — will flood the channel and cause performance problems.
For high-frequency or fire-and-forget updates, use Broadcast instead. Presence is best suited for slow-changing state such as online/offline status, active document, or current page.
</Admonition> <Admonition type="note" title="Sync event behavior">During a sync event, you may receive join and leave events simultaneously, even though no users are joining or leaving. This is expected behavior—Presence reconciles its local state with the server state, which can trigger these events as part of the synchronization process. This reflects state reconciliation, not real user movement.
The complete presence state returned by presenceState() looks like this:
{
"client_key_1": [{ "userId": 1, "typing": false }],
"client_key_2": [{ "userId": 2, "typing": true }]
}
Get the Project URL and key from the project's Connect dialog.
<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }} />
<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"
<TabPanel id="js" label="JavaScript">
import { createClient } from '@supabase/supabase-js'
const SUPABASE_URL = 'https://<project>.supabase.co'
const SUPABASE_KEY = '<sb_publishable_... key>'
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
void main() {
Supabase.initialize(
url: 'https://<project>.supabase.co',
publishableKey: '<sb_publishable_... key>',
);
runApp(MyApp());
}
final supabase = Supabase.instance.client;
let supabaseURL = "https://<project>.supabase.co"
let supabaseKey = "<sb_publishable_... key>"
let supabase = SupabaseClient(supabaseURL: URL(string: supabaseURL)!, supabaseKey: supabaseKey)
let realtime = supabase.realtime
val supabaseUrl = "https://<project>.supabase.co"
val supabaseKey = "<sb_publishable_... key>"
val supabase = createSupabaseClient(supabaseUrl, supabaseKey) {
install(Realtime)
}
from supabase import create_client
SUPABASE_URL = 'https://<project>.supabase.co'
SUPABASE_KEY = '<sb_publishable_... key>'
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
var supabase = new Supabase.Client(
"https://<project>.supabase.co",
"<sb_publishable_... key>"
);
await supabase.InitializeAsync();
<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"
<TabPanel id="js" label="JavaScript">
Listen to the sync, join, and leave events triggered whenever any client joins or leaves the channel or changes their slice of state:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
const roomOne = supabase.channel('room_01')
roomOne
.on('presence', { event: 'sync' }, () => {
const newState = roomOne.presenceState()
console.log('sync', newState)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('join', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('leave', key, leftPresences)
})
.subscribe()
final supabase = Supabase.instance.client;
final roomOne = supabase.channel('room_01');
roomOne.onPresenceSync((_) {
final newState = roomOne.presenceState();
print('sync: $newState');
}).onPresenceJoin((payload) {
print('join: $payload');
}).onPresenceLeave((payload) {
print('leave: $payload');
}).subscribe();
Listen to the presence change stream, emitting a new PresenceAction whenever someone joins or leaves:
let roomOne = await supabase.channel("room_01")
let presenceStream = await roomOne.presenceChange()
await roomOne.subscribe()
for await presence in presenceStream {
print(presence.join) // You can also use presence.decodeJoins(as: MyType.self)
print(presence.leaves) // You can also use presence.decodeLeaves(as: MyType.self)
}
Listen to the presence change flow, emitting new a new PresenceAction whenever someone joins or leaves:
val roomOne = supabase.channel("room_01")
val presenceFlow: Flow<PresenceAction> = roomOne.presenceChangeFlow()
presenceFlow
.onEach {
println(it.joins) //You can also use it.decodeJoinsAs<YourType>()
println(it.leaves) //You can also use it.decodeLeavesAs<YourType>()
}
.launchIn(yourCoroutineScope) //You can also use .collect { } here
roomOne.subscribe()
Listen to the sync, join, and leave events triggered whenever any client joins or leaves the channel or changes their slice of state:
room_one = supabase.channel('room_01')
room_one
.on_presence_sync(lambda: print('sync', room_one.presenceState()))
.on_presence_join(lambda key, curr_presences, joined_presences: print('join', key, curr_presences, joined_presences))
.on_presence_leave(lambda key, curr_presences, left_presences: print('leave', key, curr_presences, left_presences))
.subscribe()
Listen to the Sync, Join, and Leave events triggered whenever any client joins or leaves the channel or changes their slice of state:
class UserStatus : BasePresence
{
[JsonProperty("user")]
public string User { get; set; }
[JsonProperty("online_at")]
public string OnlineAt { get; set; }
}
var roomOne = supabase.Realtime.Channel("room_01");
var presence = roomOne.Register<UserStatus>(Guid.NewGuid().ToString());
presence.AddPresenceEventHandler(EventType.Sync, (sender, type) =>
{
Console.WriteLine($"sync: {presence.CurrentState}");
});
presence.AddPresenceEventHandler(EventType.Join, (sender, type) => Console.WriteLine("join"));
presence.AddPresenceEventHandler(EventType.Leave, (sender, type) => Console.WriteLine("leave"));
await roomOne.Subscribe();
You can send state to all subscribers using track():
<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"
<TabPanel id="js" label="JavaScript"> ```js import { createClient } from '@supabase/supabase-js' const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut--- const roomOne = supabase.channel('room_01')
const userStatus = { user: 'user-1', online_at: new Date().toISOString(), }
roomOne.subscribe(async (status) => { if (status !== 'SUBSCRIBED') { return }
const presenceTrackStatus = await roomOne.track(userStatus) console.log(presenceTrackStatus) })
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
final roomOne = supabase.channel('room_01');
final userStatus = {
'user': 'user-1',
'online_at': DateTime.now().toIso8601String(),
};
roomOne.subscribe((status, error) async {
if (status != RealtimeSubscribeStatus.subscribed) return;
final presenceTrackStatus = await roomOne.track(userStatus);
print(presenceTrackStatus);
});
let roomOne = await supabase.channel("room_01")
// Using a custom type
let userStatus = UserStatus(
user: "user-1",
onlineAt: Date().timeIntervalSince1970
)
await roomOne.subscribe()
try await roomOne.track(userStatus)
// Or using a raw JSONObject.
await roomOne.track(
[
"user": .string("user-1"),
"onlineAt": .double(Date().timeIntervalSince1970)
]
)
val roomOne = supabase.channel("room_01")
val userStatus = UserStatus( //Your custom class
user = "user-1",
onlineAt = Clock.System.now().toEpochMilliseconds()
)
roomOne.subscribe(blockUntilSubscribed = true) //You can also use the roomOne.status flow instead, but this parameter will block the coroutine until the status is joined.
roomOne.track(userStatus)
room_one = supabase.channel('room_01')
user_status = {
"user": 'user-1',
"online_at": datetime.datetime.now().isoformat(),
}
def on_subscribe(status, err):
if status != RealtimeSubscribeStates.SUBSCRIBED:
return
room_one.track(user_status)
room_one.subscribe(on_subscribe)
var roomOne = supabase.Realtime.Channel("room_01");
var presence = roomOne.Register<UserStatus>(Guid.NewGuid().ToString());
await roomOne.Subscribe();
await presence.Track(new UserStatus
{
User = "user-1",
OnlineAt = DateTime.UtcNow.ToString("o")
});
A client will receive state from any other client that is subscribed to the same topic (in this case room_01). It will also automatically trigger its own sync and join event handlers.
You can stop tracking presence using the untrack() method. This will trigger the sync and leave event handlers.
<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"
<TabPanel id="js" label="JavaScript">
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')
const roomOne = supabase.channel('room_01')
// ---cut---
const untrackPresence = async () => {
const presenceUntrackStatus = await roomOne.untrack()
console.log(presenceUntrackStatus)
}
untrackPresence()
final roomOne = supabase.channel('room_01');
untrackPresence() async {
final presenceUntrackStatus = await roomOne.untrack();
print(presenceUntrackStatus);
}
untrackPresence();
await roomOne.untrack()
suspend fun untrackPresence() {
roomOne.untrack()
}
untrackPresence()
room_one.untrack()
await presence.Untrack();
You can pass configuration options while initializing the Supabase Client.
By default, Presence will generate a unique UUIDv1 key on the server to track a client channel's state. If you prefer, you can provide a custom key when creating the channel. This key should be unique among clients.
<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"
<TabPanel id="js" label="JavaScript">
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('SUPABASE_URL', 'SUPABASE_PUBLISHABLE_KEY')
const channelC = supabase.channel('test', {
config: {
presence: {
key: 'userId-123',
},
},
})
final channelC = supabase.channel(
'test',
opts: const RealtimeChannelConfig(key: 'userId-123'),
);
let channelC = await supabase.channel("test") {
$0.presence.key = "userId-123"
}
val channelC = supabase.channel("test") {
presence {
key = "userId-123"
}
}
channel_c = supabase.channel('test', {
"config": {
"presence": {
"key": 'userId-123',
},
},
})
var channelC = supabase.Realtime.Channel("test");
var presence = channelC.Register<UserStatus>("userId-123");