diff --git a/apps/docs/components/Navigation/Navigation.constants.ts b/apps/docs/components/Navigation/Navigation.constants.ts index 16acfdafa4..1d0f16fd39 100644 --- a/apps/docs/components/Navigation/Navigation.constants.ts +++ b/apps/docs/components/Navigation/Navigation.constants.ts @@ -243,6 +243,8 @@ export const menuItems: NavMenu = { items: [ { name: 'Overview', url: '/guides/realtime', items: [] }, { name: 'Quickstart', url: '/guides/realtime/quickstart', items: [] }, + { name: 'Broadcast', url: '/guides/realtime/broadcast', items: [] }, + { name: 'Presence', url: '/guides/realtime/presence', items: [] }, { name: 'Postgres Changes', url: '/guides/realtime/postgres-changes', items: [] }, { name: 'Rate Limits', url: '/guides/realtime/rate-limits', items: [] }, ], diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 4c3ca6b0c7..9b522d61a6 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -509,6 +509,8 @@ export const realtime = { name: 'Channels', url: undefined, items: [ + { name: 'Broadcast', url: '/guides/realtime/broadcast', items: [] }, + { name: 'Presence', url: '/guides/realtime/presence', items: [] }, { name: 'Postgres Changes', url: '/guides/realtime/postgres-changes', items: [] }, { name: 'Rate Limits', url: '/guides/realtime/rate-limits', items: [] }, ], diff --git a/apps/docs/pages/guides/getting-started/features.mdx b/apps/docs/pages/guides/getting-started/features.mdx index c1e1a0dcfc..71fc2953aa 100755 --- a/apps/docs/pages/guides/getting-started/features.mdx +++ b/apps/docs/pages/guides/getting-started/features.mdx @@ -96,11 +96,11 @@ Receive your database changes through websockets. [Docs](/docs/guides/realtime/p ### User Broadcasting -Send messages between connected users through websockets. [Docs](/docs/guides/realtime#broadcast). +Send messages between connected users through websockets. [Docs](/docs/guides/realtime/broadcast). ### User Presence -Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime#presence). +Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime/presence). ### Client libraries diff --git a/apps/docs/pages/guides/realtime.mdx b/apps/docs/pages/guides/realtime.mdx index 4619b58ec1..164d8f27de 100644 --- a/apps/docs/pages/guides/realtime.mdx +++ b/apps/docs/pages/guides/realtime.mdx @@ -9,47 +9,17 @@ export const meta = { Supabase provides a globally distributed cluster of [Realtime](https://github.com/supabase/realtime) servers that enable the following functionality: -- [Broadcast](#broadcast): Send ephemeral messages from client to clients with low latency. -- [Presence](#presence): Track and synchronize shared state between clients. -- [Postgres Changes](#postgres-changes): Listen to Postgres database changes and send them to authorized clients. +- [Broadcast](/docs/guides/realtime/broadcast): Send ephemeral messages from client to clients with low latency. +- [Presence](/docs/guides/realtime/presence): Track and synchronize shared state between clients. +- [Postgres Changes](/docs/guides/realtime/postgres-changes): Listen to Postgres database changes and send them to authorized clients. A [channel](https://hexdocs.pm/phoenix/channels.html) is the basic building block of Realtime and narrows the scope of data flow to subscribed clients. You can think of a channel as a chatroom where participants are able to see who's online and send and receive messages; similar to a Discord or Slack channel. All clients can connect to a channel and take advantage of the built-in features, Broadcast and Presence, while extensions, like Postgres Changes, must be enabled prior to use. -## Broadcast - -Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel with a unique identifier. For example, a user could send a message to a channel with id `room-1`. - -Other clients can elect to receive the message in real-time by subscribing to the channel with id `room-1`. If these clients are online and subscribed then they will receive the message. - -Broadcast works by connecting your client to the nearest Realtime server, which will communicate with other servers to relay messages to other clients. - -A common use-case is sharing a user's cursor position with other clients in an online game. - -## Presence - -Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast. - -When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message instead of waiting for all other clients to send their individual states. - -Clients are free to come-and-go as they please, and as long as they are all subscribed to the same channel then they will all have the same Presence state as each other. - -The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state will be automatically removed from the shared state. If you've ever tried to build an “I'm online” feature which handles unexpected disconnects, you'll appreciate how useful this is. - -## Postgres Changes - -Postgres Changes enable you to listen to database changes and have them broadcast to authorized clients based on [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies. - -This works by Realtime polling your database's logical replication slot for changes, passing those changes to the [apply_rls](https://github.com/supabase/walrus#reading-wal) SQL function to determine which clients have permission, and then using Broadcast to send those changes to clients. - -Realtime requires a publication called `supabase_realtime` to determine which tables to poll. You must add tables to this publication prior to clients subscribing to channels that want to listen for database changes. - -We strongly encourage you to enable RLS on your database tables and have RLS policies in place to prevent unauthorized parties from accessing your data. - ## See Also -- [Realtime quickstart](/docs/guides/realtime/quickstart) +- [Quickstart](/docs/guides/realtime/quickstart) - [Realtime: Multiplayer Edition](https://supabase.com/blog/supabase-realtime-multiplayer-general-availability) blog post export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/realtime/broadcast.mdx b/apps/docs/pages/guides/realtime/broadcast.mdx new file mode 100644 index 0000000000..fe52723801 --- /dev/null +++ b/apps/docs/pages/guides/realtime/broadcast.mdx @@ -0,0 +1,135 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'broadcast', + title: 'Broadcast', + description: "Getting started with Realtime's Broadcast feature", +} + +Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel with a unique identifier. For example, a user could send a message to a channel with id `room-1`. + +Other clients can elect to receive the message in real-time by subscribing to the channel with id `room-1`. If these clients are online and subscribed then they will receive the message. + +Broadcast works by connecting your client to the nearest Realtime server, which will communicate with other servers to relay messages to other clients. + +A common use-case is sharing a user's cursor position with other clients in an online game. + +## Listen to Messages + +You can get started with Broadcast by creating a client and listening to a channel's messages: + +```js +const { createClient } = require('@supabase/supabase-js') + +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) + +supabase + .channel('test') + .on('broadcast', { event: 'supa' }, (payload) => console.log(payload)) + .subscribe() +``` + +## Send Messages + +You can create another client and send messages to other clients: + +```js +const { createClient } = require('@supabase/supabase-js') + +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) + +supabase.channel('test').subscribe((status) => { + if (status === 'SUBSCRIBED') { + channel.send({ + type: 'broadcast', + event: 'supa', + payload: { org: 'supabase' }, + }) + } +}) +``` + +In order for clients to successfully send and receive mesages to one another, they must both specify the same `event`. + +We recommend that the client has successfully subscribed to the channel prior to sending messages. + +### Self-Send Messages + +You can also choose for a client to receive messages that it sent: + +```js +// Supabase client setup + +supabase + .channel('test', { + config: { + broadcast: { + self: true, + }, + }, + }) + .on('broadcast', { event: 'supa' }, (payload) => console.log(payload)) + .subscribe((status) => { + if (status === 'SUBSCRIBED') { + channel.send({ + type: 'broadcast', + event: 'supa', + payload: { org: 'supabase' }, + }) + } + }) +``` + +### Acknowledge Messages + +You can ensure that Realtime's servers received your message by: + +```js +// Supabase client setup + +const channel = supabase.channel('receipt', { + config: { + broadcast: { ack: true }, + }, +}) + +channel.subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + const resp = await channel.send({ + type: 'broadcast', + event: 'latency', + payload: {}, + }) + console.log(resp) + } +}) +``` + +If `ack` is not set to `true`, Realtime servers will not acknowledge that it received the sent message and `send` promise resolves immediately. + +## Client-Side Rate Limit + +There is a default client-side rate limit that enables you to send 10 messages per second, or one message every 100 milliseconds. You can customize this when creating the client: + +```js +const { createClient } = require('@supabase/supabase-js') + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY, + { + realtime: { + params: { + eventsPerSecond: 20 + } + } + } +``` + +By setting `eventsPerSecond` to 20, you can send one message every 50 milliseconds on a per client basis. + +Learn more by visiting the [Rate Limits](/docs/guides/realtime/rate-limits) section. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/realtime/postgres-changes.mdx b/apps/docs/pages/guides/realtime/postgres-changes.mdx index 0d68f25c2e..205af17987 100644 --- a/apps/docs/pages/guides/realtime/postgres-changes.mdx +++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx @@ -24,6 +24,8 @@ We strongly encourage you to enable RLS and create policies for tables in privat +## Replication Setup + You can do this in the [Replication](https://app.supabase.com/project/_/database/replication) section in the Dashboard or with the [SQL editor](https://app.supabase.com/project/_/sql): ```sql @@ -39,6 +41,8 @@ commit; alter publication supabase_realtime add table messages; ``` +### Full `old` Record + By default, only `new` record changes are sent but if you want to receive the `old` record (previous values) whenever you `UPDATE` or `DELETE` a record, you can set the `replica identity` of your table to `full`: @@ -46,6 +50,8 @@ you can set the `replica identity` of your table to `full`: alter table messages replica identity full; ``` +## Schema Changes + To listen to all changes in the `public` schema: ```js @@ -74,6 +80,8 @@ const channel = supabase .subscribe() ``` +## Table Changes + To listen to changes on a table in the `public` schema: ```js @@ -93,6 +101,8 @@ const channel = supabase .subscribe() ``` +## Filter Changes + To listen to changes when a column's value in a table matches a client-specified value: ```js @@ -113,6 +123,8 @@ const channel = supabase .subscribe() ``` +## Combination Changes + To listen to different events and schema/tables/filters combinations with the same channel: ```js diff --git a/apps/docs/pages/guides/realtime/presence.mdx b/apps/docs/pages/guides/realtime/presence.mdx new file mode 100644 index 0000000000..3ae8d56526 --- /dev/null +++ b/apps/docs/pages/guides/realtime/presence.mdx @@ -0,0 +1,165 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'presence', + title: 'Presence', + description: "Getting started with Realtime's Presence feature", +} + +Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast. + +When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message instead of waiting for all other clients to send their individual states. + +Clients are free to come-and-go as they please, and as long as they are all subscribed to the same channel then they will all have the same Presence state as each other. + +The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state will be automatically removed from the shared state. If you've ever tried to build an “I'm online” feature which handles unexpected disconnects, you'll appreciate how useful this is. + +## Presence State + +You can get started by listening to `sync` event messages notifying the client that a channel's state has been synchronized on the server. You can get the state by calling the channel's `presenceState` helper: + +```js +const { createClient } = require('@supabase/supabase-js') + +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) + +const channel = supabase.channel('test') + +channel + .on('presence', { event: 'sync' }, () => { + const state = channel.presenceState() + console.log(state) + }) + .subscribe() +``` + +Whenever there's Presence activity on the `'test'` channel, this `sync` event will be broadcast to all clients subscribed to the channel. + +## Listen to Joins + +You can create a client and listen to new state joining the channel's Presence: + +```js +// Supabase client setup + +const channel = supabase.channel('test') + +channel + .on('presence', { event: 'join' }, ({ key, newPresences }) => { + console.log(key, newPresences) + }) + .subscribe() +``` + +## Track Presence + +On another client, subscribe to the channel and insert state to be tracked by Presence: + +```js +// Supabase client setup + +const channel = supabase.channel('test') + +channel.subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + const presenceTrackStatus = await channel.track({ + user: 'user-1', + online_at: new Date().toISOString(), + }) + console.log(presenceTrackStatus) + } +}) +``` + +### Presence Key + +By default, Presence will generate an `UUIDv1` key on the server to uniquely track a client channel's state but you may pass Presence a custom key when creating the channel. + +```js +// Supabase client setup + +const channel = supabase.channel('test', { + config: { + presence: { + key: 'userId-1', + }, + }, +}) + +channel.subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + const presenceTrackStatus = await channel.track({ + user: 'user-1', + online_at: new Date().toISOString(), + }) + console.log(presenceTrackStatus) + } +}) +``` + +## Listen to Leaves + +You can create a client and listen to a client channel's state leaving: + +```js +// Supabase client setup + +const channel = supabase.channel('test') + +channel + .on('presence', { event: 'leave' }, ({ key, leftPresences }) => { + console.log(key, leftPresences) + }) + .subscribe() +``` + +## Untrack Presence + +On another client, subscribe to the channel, and remove tracked state from Presence: + +```js +// Supabase client setup + +const channel = supabase.channel('test') + +channel.subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + const presenceTrackStatus = await channel.track({ + user: 'user-1', + online_at: new Date().toISOString(), + }) + + if (presenceTrackStatus === 'ok') { + const presenceUntrackStatus = await channe.untrack() + console.log(presenceUntrackStatus) + } + } +}) +``` + +## Client-Side Rate Limit + +There is a default client-side rate limit that enables you to send 10 messages per second, or one message every 100 milliseconds. You can customize this when creating the client: + +```js +const { createClient } = require('@supabase/supabase-js') + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY, + { + realtime: { + params: { + eventsPerSecond: 5 + } + } + } +``` + +By setting `eventsPerSecond` to 5, you can send one message every 200 milliseconds on a per client basis. + +Learn more by visiting the [Rate Limits](/docs/guides/realtime/rate-limits) section. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/realtime/quickstart.mdx b/apps/docs/pages/guides/realtime/quickstart.mdx index bd3910f024..cd20604b75 100644 --- a/apps/docs/pages/guides/realtime/quickstart.mdx +++ b/apps/docs/pages/guides/realtime/quickstart.mdx @@ -27,7 +27,7 @@ npm install @supabase/supabase-js ## Cursor Positions -[Broadcast](/docs/guides/realtime#broadcast) allows a client to send messages and multiple clients to receive the messages. The broadcasted messages are ephemeral. They are not persisted to the database and are directly relayed through the Realtime servers. This is ideal for sending information like cursor positions where minimal latency is important, but persisting them is not. +[Broadcast](/docs/guides/realtime/broadcast) allows a client to send messages and multiple clients to receive the messages. The broadcasted messages are ephemeral. They are not persisted to the database and are directly relayed through the Realtime servers. This is ideal for sending information like cursor positions where minimal latency is important, but persisting them is not. In [multiplayer.dev](https://multiplayer.dev), client's cursor positions are sent to other clients in the room. However, cursor positions will be randomly generated for this example. @@ -124,7 +124,7 @@ channel.subscribe(async (status) => { ## Track and Display Which Users Are Online -[Presence](/docs/guides/realtime#presence) stores and synchronize shared state across clients. The `sync` event is triggered whenever the shared state changes. The `join` event is triggered when new clients join the channel and `leave` event is triggered when clients leave. +[Presence](/docs/guides/realtime/presence) stores and synchronize shared state across clients. The `sync` event is triggered whenever the shared state changes. The `join` event is triggered when new clients join the channel and `leave` event is triggered when clients leave. Each client can use the channel's `track` method to store an object in shared state. Each client can only track one object, and if `track` is called again by the same client, then the new object overwrites the previously tracked object in the shared state. You can use one client to track and display users who are online: diff --git a/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx b/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx index 7183c9fb53..e6ecfbe4cd 100644 --- a/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx +++ b/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx @@ -1351,7 +1351,7 @@ We used bloc for our state management solution. One thing we could have done dif We could also explore some cool feature improvement. At the top of the rooms page, we are loading the newest created users to start a conversation. This is fine, but it only allows users to start a conversation with new users. -We can for example update this to a list of users that are online at the same time. We can implement this using the [presence feature](https://supabase.com/docs/guides/realtime#presence) of Supabase. +We can for example update this to a list of users that are online at the same time. We can implement this using the [presence feature](https://supabase.com/docs/guides/realtime/presence) of Supabase. ## More Flutter Resources diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index d5fa09975e..91aff3508f 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1220,13 +1220,13 @@ module.exports = [ }, { permanent: true, - source: '/docs/guides/realtime/broadcast', - destination: '/docs/guides/realtime#broadcast', + source: '/docs/guides/realtime#broadcast', + destination: '/docs/guides/realtime/broadcast', }, { permanent: true, - source: '/docs/guides/realtime/presence', - destination: '/docs/guides/realtime#presence', + source: '/docs/guides/realtime#presence', + destination: '/docs/guides/realtime/presence', }, { permanent: true, @@ -1810,7 +1810,7 @@ module.exports = [ { permanent: true, source: '/docs/reference/realtime', - destination: '/docs/reference/realtime/introduction', + destination: '/docs/reference/self-hosting-realtime/start', }, { permanent: true, diff --git a/apps/www/pages/realtime/Realtime.tsx b/apps/www/pages/realtime/Realtime.tsx index 633b7bc7ba..890332c147 100644 --- a/apps/www/pages/realtime/Realtime.tsx +++ b/apps/www/pages/realtime/Realtime.tsx @@ -238,7 +238,7 @@ function RealtimePage() {

, ]} // [TODO] Point to the correct docs URL - documentation_link={'/docs/guides/realtime#broadcast'} + documentation_link={'/docs/guides/realtime/broadcast'} />