# Presence

Share state between users with Realtime Presence.

Use Realtime Presence to track state between multiple users.

## Usage

You can use the Supabase client libraries to track Presence state between users.

### How Presence works

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 updated
- **`join`** — a new client has started tracking presence
- **`leave`** — a client has stopped tracking presence

Caution: Presence 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](/docs/guides/realtime/broadcast) instead. Presence is best suited for slow-changing state such as online/offline status, active document, or current page.

Note: 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:

```json
{
  "client_key_1": [{ "userId": 1, "typing": false }],
  "client_key_2": [{ "userId": 2, "typing": true }]
}
```

### Initialize the client

Get the Project URL and key from [the project's **Connect** dialog](/dashboard/project/_?showConnect=true).

Deprecation: Supabase is deprecating the `anon` and `service_role` keys by the end of 2026. Use the publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys instead. For the reasoning behind the change, see [the announcement on GitHub](https://github.com/orgs/supabase/discussions/29260).

In most cases you can get keys from your project's [**Connect** dialog](/dashboard/project/_?showConnect=true\&connectTab=\&framework=). For every way to retrieve a key, including the CLI and the Management API, refer to [Find your keys](/docs/guides/getting-started/api-keys#find-your-keys).

**JavaScript**

```js
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)
```

**Dart**

```dart
void main() {
  Supabase.initialize(
    url: 'https://<project>.supabase.co',
    publishableKey: '<sb_publishable_... key>',
  );

  runApp(MyApp());
}

final supabase = Supabase.instance.client;
```

**Swift**

```swift
let supabaseURL = "https://<project>.supabase.co"
let supabaseKey = "<sb_publishable_... key>"
let supabase = SupabaseClient(supabaseURL: URL(string: supabaseURL)!, supabaseKey: supabaseKey)

let realtime = supabase.realtime
```

**Kotlin**

```kotlin
val supabaseUrl = "https://<project>.supabase.co"
val supabaseKey = "<sb_publishable_... key>"
val supabase = createSupabaseClient(supabaseUrl, supabaseKey) {
    install(Realtime)
}
```

**Python**

```python
from supabase import create_client

SUPABASE_URL = 'https://<project>.supabase.co'
SUPABASE_KEY = '<sb_publishable_... key>'

supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
```

**C#**

```c#
var supabase = new Supabase.Client(
    "https://<project>.supabase.co",
    "<sb_publishable_... key>"
);
await supabase.InitializeAsync();
```

### Sync and track state

**JavaScript**

Listen to the `sync`, `join`, and `leave` events triggered whenever any client joins or leaves the channel or changes their slice of state:

```js
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()
```

**Dart**

```dart
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();
```

**Swift**

Listen to the presence change stream, emitting a new `PresenceAction` whenever someone joins or leaves:

```swift
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)
}
```

**Kotlin**

Listen to the presence change flow, emitting new a new `PresenceAction` whenever someone joins or leaves:

```kotlin
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()
```

**Python**

Listen to the `sync`, `join`, and `leave` events triggered whenever any client joins or leaves the channel or changes their slice of state:

```python
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()
```

**C#**

Listen to the `Sync`, `Join`, and `Leave` events triggered whenever any client joins or leaves the channel or changes their slice of state:

```c#
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();
```

### Sending state

You can send state to all subscribers using `track()`:

**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)
})
```

**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);
});
```

**Swift**

```swift
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)
  ]
)
```

**Kotlin**

```kotlin
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)
```

**Python**

```python
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)
```

**C#**

```c#
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.

### Stop tracking

You can stop tracking presence using the `untrack()` method. This will trigger the `sync` and `leave` event handlers.

**JavaScript**

```js
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()
```

**Dart**

```dart
final roomOne = supabase.channel('room_01');

untrackPresence() async {
  final presenceUntrackStatus = await roomOne.untrack();
  print(presenceUntrackStatus);
}

untrackPresence();
```

**Swift**

```swift
await roomOne.untrack()
```

**Kotlin**

```kotlin
suspend fun untrackPresence() {
	roomOne.untrack()
}

untrackPresence()
```

**Python**

```python
room_one.untrack()
```

**C#**

```c#
await presence.Untrack();
```

## Presence options

You can pass configuration options while initializing the Supabase Client.

### Presence key

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.

**JavaScript**

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

const supabase = createClient('SUPABASE_URL', 'SUPABASE_PUBLISHABLE_KEY')

const channelC = supabase.channel('test', {
  config: {
    presence: {
      key: 'userId-123',
    },
  },
})
```

**Dart**

```dart
final channelC = supabase.channel(
  'test',
  opts: const RealtimeChannelConfig(key: 'userId-123'),
);
```

**Swift**

```swift
let channelC = await supabase.channel("test") {
  $0.presence.key = "userId-123"
}
```

**Kotlin**

```kotlin
val channelC = supabase.channel("test") {
    presence {
        key = "userId-123"
    }
}
```

**Python**

```python
channel_c = supabase.channel('test', {
  "config": {
    "presence": {
      "key": 'userId-123',
    },
  },
})
```

**C#**

```c#
var channelC = supabase.Realtime.Channel("test");
var presence = channelC.Register<UserStatus>("userId-123");
```
