From 34d3ab82375303386628915a8db298161774beba Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 20:15:03 +0200 Subject: [PATCH 01/15] Adds some updates for the self-hosted guides --- apps/docs/pages/guides/self-hosting.mdx | 136 +++++------------- .../docs/pages/guides/self-hosting/docker.mdx | 101 ++++++++++++- 2 files changed, 128 insertions(+), 109 deletions(-) diff --git a/apps/docs/pages/guides/self-hosting.mdx b/apps/docs/pages/guides/self-hosting.mdx index 1ad426b229b..4b631683686 100644 --- a/apps/docs/pages/guides/self-hosting.mdx +++ b/apps/docs/pages/guides/self-hosting.mdx @@ -2,15 +2,43 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { title: 'Self-Hosting', - description: 'Getting started with self-hosting Supabase.', + description: 'Host Supabase on your own infrastructure.', + subtitle: 'Host Supabase on your own infrastructure.', } -There are several ways to use Supabase: +There are several ways to host Supabase on your own computer, server, or cloud. -- [Supabase Cloud](https://supabase.com/dashboard): you don't need to deploy anything. We will manage and scale your infrastructure. -- [Docker](/docs/guides/self-hosting/docker): deploy to your own infrastructure. +## Officially Supported -### Community +
+ {official.map((x) => ( +
+ + + {x.description} + + +
+ ))} +
+ +export const official = [ + { + name: 'Docker', + description: 'Deploy Supabase within your own infrastructure using Docker Compose.', + href: '/docs/guides/self-hosting/docker', + }, + { + name: 'BYO Cloud', + description: + 'Contact our Enterprise sales team if you need Supabase managed in your own cloud.', + href: '/pricing', + }, +] + +Supabase is also a hosted platform. If you want to get started for free, visit [supabase.com/dashboard](https://supabase.com/dashboard). + +## Community Supported There are several community-driven projects to help you deploy Supabase. We encourage you to try them out and contribute back to the community. @@ -49,7 +77,7 @@ export const community = [ }, ] -### Third-party +## Third-party Guides The following third-party providers have shown consistent support for the self-hosted version of Supabase:. @@ -78,102 +106,6 @@ export const external = [ }, ] -## Architecture - -Supabase is a combination of open source tools, each specifically chosen for Enterprise-readiness. - -If the tools and communities already exist, with an MIT, Apache 2, or equivalent open license, we will use and support that tool. -If the tool doesn't exist, we build and open source it ourselves. - -![Supabase Architecture](/docs/img/supabase-architecture.png) - -- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway. -- [GoTrue](https://github.com/supabase/gotrue) is an SWT based API for managing users and issuing SWT tokens. -- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API -- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients. -- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions. -- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc. -- [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. - -## Configuration - -Each system has a number of configuration options which can be found in the relevant product documentation. - -- [Postgres](https://hub.docker.com/_/postgres/) -- [PostgREST](https://postgrest.org/en/stable/configuration.html) -- [Realtime](https://github.com/supabase/realtime#server) -- [GoTrue](https://github.com/supabase/gotrue) -- [Storage](https://github.com/supabase/storage-api) -- [Kong](https://docs.konghq.com/gateway/latest/install/docker/) - -## Managing your database - -It is recommended that you decouple your database from the middleware so that you can upgrade the middleware without any downtime. -The "middleware" is everything except Postgres, and it should work with any Postgres provider (such as AWS RDS), or your own Postgres cluster. - -### Extensions - -Supabase requires some Postgres extensions to be enabled by default for the API and Auth system to work. You can find the extensions inside the -[schema migration scripts](https://github.com/supabase/postgres/tree/develop/migrations). These are mounted at `/docker-entrypoint-initdb.d` -to run automatically when starting the database container. - -We recommend installing all extensions into an `extensions` schema. This will keep your API clean, -since all tables in the `public` schema are exposed via the API. - -{/* prettier-ignore */} -```sql -create schema if not exists extensions; -create extension if not exists "uuid-ossp" with schema extensions; -create extension if not exists pgcrypto with schema extensions; -create extension if not exists pgjwt with schema extensions; -``` - -##### `uuid-ossp` - -For UUID functions, required for PostgreSQL `<13`. - -##### `pgcrypto` and `pgjwt` - -For working with JWT and Auth functions. - -### Roles - -Supabase creates several [default roles](/docs/guides/database/postgres-roles) in your Postgres database. To restore defaults at any time you can run the commands inside the [schema initialization scripts](https://github.com/supabase/postgres/tree/develop/migrations/db/init-scripts). Remember to change your [role passwords](/docs/guides/self-hosting/docker#securing-your-setup) before deploying to production environments. - -### Realtime Logs - -Set your database's `log_min_messages` configuration to `fatal` to prevent redundant database logs generated by Realtime. However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs. - -## API Keys - -The API Gateway (Kong) uses JWT to authenticate access through to the database. The JWT should correspond to a relevant Postgres Role, -and Supabase is designed to work with 2 roles: an `ANON_KEY` for unauthenticated access and a `SERVICE_KEY` for elevated access. - -Use this tool to generate keys: - - - -## Managing your secrets - -Many components inside Supabase use secure secrets and passwords. These are listed in the self-hosting -[env file](https://github.com/supabase/supabase/blob/master/docker/.env.example), but we strongly recommend using a -secrets manager when deploying to production. Plain text files like dotenv lead to accidental costly leaks. - -Some suggested systems include: - -- [Doppler](https://www.doppler.com/) -- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) by Azure (Microsoft) -- [Secrets Manager](https://aws.amazon.com/secrets-manager/) by AWS -- [Secrets Manager](https://cloud.google.com/secret-manager) by GCP -- [Vault](https://www.hashicorp.com/products/vault) by Hashicorp - -## Migrating and Upgrading - -If you have decoupled your database from the middleware, then you should be able to redeploy the latest middleware at any time as long as it has no breaking changes. -Supabase is evolving fast, and we'll continue to improve the migration strategy as part of our core offering. - -We realize that database migrations are difficult, and this is one of the problems we plan to make easy for developers. - export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/self-hosting/docker.mdx b/apps/docs/pages/guides/self-hosting/docker.mdx index 649bb5e2d83..68cb792dd88 100644 --- a/apps/docs/pages/guides/self-hosting/docker.mdx +++ b/apps/docs/pages/guides/self-hosting/docker.mdx @@ -159,11 +159,80 @@ rm -rf volumes/db/data/ This will destroy all data in the database and storage volumes, so be careful! -## Common configuration +## Managing your secrets + +Many components inside Supabase use secure secrets and passwords. These are listed in the self-hosting [env file](https://github.com/supabase/supabase/blob/master/docker/.env.example), but we strongly recommend using a secrets manager when deploying to production. Plain text files like dotenv lead to accidental costly leaks. + +Some suggested systems include: + +- [Doppler](https://www.doppler.com/) +- [Infisical](https://infisical.com/) +- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) by Azure (Microsoft) +- [Secrets Manager](https://aws.amazon.com/secrets-manager/) by AWS +- [Secrets Manager](https://cloud.google.com/secret-manager) by GCP +- [Vault](https://www.hashicorp.com/products/vault) by Hashicorp + +## Advanced + +Everything beyond this point in the guide helps you understand how the system works and how you can modify it to suit your needs. + +### Architecture + +Supabase is a combination of open source tools, each specifically chosen for Enterprise-readiness. + +If the tools and communities already exist, with an MIT, Apache 2, or equivalent open license, we will use and support that tool. +If the tool doesn't exist, we build and open source it ourselves. + +![Supabase Architecture](/docs/img/supabase-architecture.png) + +- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway. +- [GoTrue](https://github.com/supabase/gotrue) is an SWT based API for managing users and issuing SWT tokens. +- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API +- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients. +- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions. +- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc. +- [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. + +For the system to work cohesively, some services require additional configuration within the Postgres database. For example, the APIs and Auth system require several [default roles](/docs/guides/database/postgres-roles) amd the `pgjwt` Postgres extension. + +You can find all the default extensions inside the [schema migration scripts repo](https://github.com/supabase/postgres/tree/develop/migrations). These scripts are mounted at `/docker-entrypoint-initdb.d` to run automatically when starting the database container. + +### Configuring services + +Each system has a number of configuration options which can be found in the relevant product documentation. + +- [Postgres](https://hub.docker.com/_/postgres/) +- [PostgREST](https://postgrest.org/en/stable/configuration.html) +- [Realtime](https://github.com/supabase/realtime#server) +- [GoTrue](https://github.com/supabase/gotrue) +- [Storage](https://github.com/supabase/storage-api) +- [Kong](https://docs.konghq.com/gateway/latest/install/docker/) + + These configuration items are generally added to the `env` section of each service, inside the `docker-compose.yml` section. If these configuration items are sensitive, they should be stored in a [secret manager](/docs/guides/self-hosting#managing-your-secrets) or using an `.env` file and then referenced using the `${}` syntax. + + + + + ```yml docker-compose.yml +services: + rest: + image: postgrest/postgrest + environment: + PGRST_JWT_SECRET: ${JWT_SECRET} + ``` + + ```bash .env + ## Never check your secrets into version control +`${JWT_SECRET}` + ``` + + + +### Common configuration Each system can be [configured](../self-hosting#configuration) independently. Some of the most common configuration options are listed below. -### Configuring an email server +#### Configuring an email server You will need to use a production-ready SMTP server for sending emails. You can configure the SMTP server by updating the the following environment variables: @@ -178,7 +247,7 @@ SMTP_SENDER_NAME= We recommend using [AWS SES](https://aws.amazon.com/ses/). It's extremely cheap and reliable. Restart all services to pick up the new configuration. -### Configuring S3 Storage +#### Configuring S3 Storage By default all files are stored locally on the server. You can connfigure the Storage service to use S3 by updating the following environment variables: @@ -192,11 +261,11 @@ By default all files are stored locally on the server. You can connfigure the St You can find all the available options in the [storage repository](https://github.com/supabase/storage-api/blob/master/.env.sample). Restart the `storage` service to pick up the changes: `docker compose restart storage --no-deps` -### Setting database's `log_min_messages` +#### Setting database's `log_min_messages` By default, `docker compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime. You can configure `log_min_messages` using any of the Postgres [Severity Levels](https://www.postgresql.org/docs/current/runtime-config-logging.html#RUNTIME-CONFIG-SEVERITY-LEVELS). -### Exposing your Postgres database +#### Exposing your Postgres database By default, the Postgres database is only accessible locally. If you want to expose it to the outside world, you can update the `docker-compose.yml` file and remove the `127.0.0.1:` prefix from the `ports` section: @@ -208,15 +277,33 @@ By default, the Postgres database is only accessible locally. If you want to exp This is less-secure, so please make sure you are running a firewall in front of your server. -### File storage backend on macOS +#### File storage backend on macOS By default, Storage backend is set to `file`, which is to use local files as the storage backend. For macOS compatibility, you need to choose `VirtioFS` as the Docker container file sharing implementation (in Docker Desktop -> Preferences -> General). -### Setting up logging with the Analytics server +#### Setting up logging with the Analytics server Additional configuration is required for self-hosting the Analytics server. For the full setup instructions, see [Self Hosting Analytics](https://supabase.com/docs/reference/self-hosting-analytics/introduction#getting-started). +--- + +{/* Finish with a video. This also appears in the Sidebar via the "tocVideo" metadata */} + +## Demo + +A minimal setup working on Ubuntu, hosted on Digital Ocean. + + +
+ +
+ export const Page = ({ children }) => export default Page From 1ac2ff93390089044ee25eb16336b741d48f7ec8 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 20:38:58 +0200 Subject: [PATCH 02/15] realtime: fix heading --- apps/docs/pages/guides/realtime.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/realtime.mdx b/apps/docs/pages/guides/realtime.mdx index b11c3a56049..676e911b0fd 100644 --- a/apps/docs/pages/guides/realtime.mdx +++ b/apps/docs/pages/guides/realtime.mdx @@ -3,7 +3,8 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'realtime', title: 'Realtime', - description: 'Supabase Realtime with Broadcast, Presence, and Postgres Changes.', + description: 'Send and receive messages to connected clients.', + subtitle: 'Send and receive messages to connected clients.', sidebar_label: 'Overview', } From d8bbb79e27c8d60b0febd8cb6924ceb2098c9dfe Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 20:57:27 +0200 Subject: [PATCH 03/15] realtime: add page on throttling --- .../NavigationMenu.constants.ts | 4 ++ apps/docs/pages/guides/realtime/broadcast.mdx | 20 -------- .../guides/client-side-throttling.mdx | 47 +++++++++++++++++++ apps/docs/pages/guides/realtime/presence.mdx | 20 -------- 4 files changed, 51 insertions(+), 40 deletions(-) create mode 100644 apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 81f0c44369e..5c9d864ce0c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -886,6 +886,10 @@ export const realtime: NavMenuConstant = { name: 'Guides', url: undefined, items: [ + { + name: 'Throttling messages', + url: '/guides/realtime/guides/client-side-throttling', + }, { name: 'Subscribing to Database Changes', url: '/guides/realtime/subscribing-to-database-changes', diff --git a/apps/docs/pages/guides/realtime/broadcast.mdx b/apps/docs/pages/guides/realtime/broadcast.mdx index 90055d2c2ae..04496b4f52c 100644 --- a/apps/docs/pages/guides/realtime/broadcast.mdx +++ b/apps/docs/pages/guides/realtime/broadcast.mdx @@ -245,26 +245,6 @@ channelD.subscribe(async (status) => { Use this to guarantee that the server has received the message before resolving `channelD.send`'s promise. If the `ack` config is not set to `true` when creating the channel, the promise returned by `channelD.send` will resolve immediately. -## Client-side rate limit - -By default the client will rate limit itself at 10 messages per second (1 message every 100 milliseconds). You can customize this when creating the client: - -```js -import { createClient } from '@supabase/supabase-js' - -const clientE = createClient('https://.supabase.co', '', { - 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 [Quotas](/docs/guides/realtime/quotas) section. - ## More Realtime Quickstarts - [Presence Quickstart](/docs/guides/realtime/presence) diff --git a/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx b/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx new file mode 100644 index 00000000000..bc1624568a9 --- /dev/null +++ b/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx @@ -0,0 +1,47 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'realtime', + title: 'Throttling messages', + description: 'Use client-side throttling to manage message frequency.', + subtitle: 'Use client-side throttling to manage message frequency.', +} + +You should always consider optimizing the performance of you realtime system. + +## Project Quotas + +Each Broadcast and Presence message counts towards your [Project Quotas](/docs/guides/realtime/quotas). + +It's common to unintentionally flood the Realtime service with messages. For example, tracking mouse movents without throttling would send hundreds of events per second. It's rare that you need so many messages. Updating a mouse movement even a few times per second is usually enough for the human eye. + +The Supabase client includes a configurable throttling parameter to protect against these unintended floods. + +## Default client throttling + +By default the Supabase clients throttles messages to 10 messages per-second (1 message every 100 milliseconds). This is a soft-limit provided as a safe-guard when you're getting started. You'll rarely need to send more messages than this. + +Each client has their own throttling behavior. For example, if you instantiate two clients, by default you would send 20 messages per-second to your project. + +## Managing client-side throttling + +You can customize the client-side throttling when creating the client: + +```js +import { createClient } from '@supabase/supabase-js' + +const SUPABASE_URL = 'https://.supabase.co' +const SUPABASE_ANON_KEY = '' + +const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + realtime: { + params: { + eventsPerSecond: 2, + }, + }, +}) +``` + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/realtime/presence.mdx b/apps/docs/pages/guides/realtime/presence.mdx index 311e6aec849..ae645a42922 100644 --- a/apps/docs/pages/guides/realtime/presence.mdx +++ b/apps/docs/pages/guides/realtime/presence.mdx @@ -245,26 +245,6 @@ const channelC = supabase.channel('test', { }) ``` -## Client-Side Rate Limit - -By default the client will rate limit itself at 10 messages per second (1 message every 100 milliseconds). You can customize this when creating the client: - -```js -import { createClient } from '@supabase/supabase-js' - -const supabase = createClient('https://.supabase.co', '', { - 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 [Quotas](/docs/guides/realtime/quotas) section. - ## More Realtime Quickstarts - [Broadcast Quickstart](/docs/guides/realtime/broadcast) From 08596fc87e51c3d4a5b9d1a71b0b789e4ef96eaf Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:56:54 +0200 Subject: [PATCH 04/15] simplify/clarify --- .../Navigation/NavigationMenu/NavigationMenu.constants.ts | 2 +- apps/docs/pages/guides/realtime.mdx | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 5c9d864ce0c..9cf59275895 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -871,7 +871,7 @@ export const realtime: NavMenuConstant = { url: '/guides/realtime/concepts', }, { - name: 'Features', + name: 'Usage', url: undefined, items: [ { name: 'Broadcast', url: '/guides/realtime/broadcast' }, diff --git a/apps/docs/pages/guides/realtime.mdx b/apps/docs/pages/guides/realtime.mdx index 676e911b0fd..4b33c2567de 100644 --- a/apps/docs/pages/guides/realtime.mdx +++ b/apps/docs/pages/guides/realtime.mdx @@ -14,10 +14,6 @@ Supabase provides a globally distributed cluster of [Realtime](https://github.co - [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. - ## See Also - [Realtime: Multiplayer Edition](https://supabase.com/blog/supabase-realtime-multiplayer-general-availability) blog post From 248b3a42be8b17968639ee963cd5e8e8eaa07660 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:57:12 +0200 Subject: [PATCH 05/15] extend concepts with more details --- apps/docs/pages/guides/realtime/concepts.mdx | 68 ++++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/apps/docs/pages/guides/realtime/concepts.mdx b/apps/docs/pages/guides/realtime/concepts.mdx index af4dee031bd..eae39be7c4d 100644 --- a/apps/docs/pages/guides/realtime/concepts.mdx +++ b/apps/docs/pages/guides/realtime/concepts.mdx @@ -4,10 +4,11 @@ export const meta = { id: 'channels', title: 'Realtime Concepts', description: 'Learn about channels and other features in Supabase Realtime', + subtitle: 'Learn about channels and other features in Supabase Realtime', sidebar_label: 'Concepts', } -Supabase Realtime lets you to build real-time applications with collaborative/multiplayer functionality. It includes 3 core features: +You can use Supabase Realtime to build real-time applications with collaborative/multiplayer functionality. It includes 3 core features: - [Broadcast](/docs/guides/realtime/broadcast): sends rapid, ephemeral messages to other connected clients. You can use it to track mouse movements, for example. - [Presence](/docs/guides/realtime/presence): sends user state between connected clients. You can use it to show an "online" status, which disappears when a user is disconnected. @@ -15,23 +16,78 @@ Supabase Realtime lets you to build real-time applications with collaborative/mu ## Channels -When you initialize your Supabase Realtime client, you define a `topic` that uniquely references a channel. Everyone connected to the same Channel `topic` receives the same messages. +A Channel is the basic building block of Realtime. You can think of a Channel as a chatroom, similar to a Discord or Slack channel, where participants are able to see who's online and send and receive messages. + +When you initialize your Supabase Realtime client, you define a `topic` that uniquely references a channel. Clients can bi-directionally send and receive messages over a Channel. ```js import { createClient } from '@supabase/supabase-js' const client = createClient('https://.supabase.co', '') -const channel = client.channel('my-topic') // set your topic here +const roomOne = client.channel('room-one') // set your topic here ``` -Clients can bi-directionally send and receive messages over a Channel. The Realtime backend can also push messages to all clients connected to the same Channel. +## Broadcast -A single client can receive change records from Postgres, Broadcast messages from other clients, and Presence updates all over the same Channel. +Realtime Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel based on a unique topic. For example, a user could send a message to a channel with topic `room-one`. + +```js +roomOne.send({ + type: 'broadcast', + event: 'test', + payload: { message: 'hello, world' }, +}) +``` + +Other clients can receive the message in real-time by subscribing to the Channel with topic `room-one`. These clients continue to receive messages as long as they are subscribed and connected to the same Channel topic. + +An example use-case is sharing a user's cursor position with other clients in an online game. + +## Presence + +Presence can be used to share and invidual's state with others within a Channel. + +```js +const presenceTrackStatus = await roomOne.track({ + user: 'user-1', + online_at: new Date().toISOString(), +}) +``` + +Each client maintains their own state, and this is then combined into a "shared state" for that Channel topic. It's commonly used for sharing statuses (eg: "online" or "inactive"). The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state is 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. + +When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message because the state is held by the Realtime server. ## Choosing between Broadcast and Presence -We recommend using Broadcast by default, and then Presence when required. Presence merges all changes into a shared state for every client connected to the same channel. If you use Presence, it's best to throttle your changes so that you are sending updates less frequently. +We recommend using Broadcast by default, and then Presence when required. 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. This is computationally heavy, so you should use it sparingly. If you use Presence, it's best to throttle your changes so that you are sending updates less frequently. + +## Reatlime extensions + +Channels provide a generic networking solution. Supabase Realtime is designed to leverage this networking primitive with "extensions". We currently support one extension: Postgres changes. + +### Postgres changes + +The Postgres Changes extension listens for database changes and sends them to clients. Clients are required to subscribe with a JWT dictating which changes they are allowed to receive based on the database's [Row Level Security](/docs/guides/auth/row-level-security). + +```js +const allChanges = client + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +Anyone with access to a valid JWT signed with the project's JWT secret is able to listen to your database's changes, unless tables have [Row Level Security](/docs/guides/auth/row-level-security) enabled and policies in place. + +Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the `public` schema and you must first enable the tables you want your clients to listen to. export const Page = ({ children }) => From 869369b2a3f699ce3899487f8f0ec1c76d877506 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:57:27 +0200 Subject: [PATCH 06/15] convert Broadcast to full guide --- apps/docs/pages/guides/realtime/broadcast.mdx | 277 +++++------------- 1 file changed, 81 insertions(+), 196 deletions(-) diff --git a/apps/docs/pages/guides/realtime/broadcast.mdx b/apps/docs/pages/guides/realtime/broadcast.mdx index 04496b4f52c..37c4d263ed3 100644 --- a/apps/docs/pages/guides/realtime/broadcast.mdx +++ b/apps/docs/pages/guides/realtime/broadcast.mdx @@ -1,191 +1,80 @@ import Layout from '~/layouts/DefaultGuideLayout' -import StepHikeCompact from '~/components/StepHikeCompact' export const meta = { title: 'Broadcast', - subtitle: "Get up and running with Realtime's Broadcast feature", - breadcrumb: 'Realtime Broadcast Quickstart', + subtitle: 'Send and receive messages using Realtime Broadcast', + description: 'Send and receive messages using Realtime Broadcast', + // breadcrumb: 'Realtime Broadcast Quickstart', } -Realtime Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel based on a unique topic. For example, a user could send a message to a channel with topic `room-1`. +Let's explore how to implement Realtime Broadcast to send messages between clients. -Other clients can receive the message in real-time by subscribing to the channel with topic `room-1`. These clients can continue to receive messages as long as they continue to be online and subscribed to the same channel topic. +## Usage -An example use-case is sharing a user's cursor position with other clients in an online tool or game. +You can use the Supabase client libraries to send and receive Broadcast messages. -## Quick start +### Initialize the client -Let's explore how to implement Realtime Broadcast so you can integrate it into your use case. +Go to your Supabase project's [API Settings](https://supabase.com/dashboard/project/_/settings/api) and grab the `URL` and `anon` public API key. - +```js +import { createClient } from '@supabase/supabase-js' - - - +const SUPABASE_URL = 'https://.supabase.co' +const SUPABASE_KEY = '' - Install the Supabase JavaScript client. +const client = createClient(SUPABASE_URL, SUPABASE_KEY) +``` - +### Listening to Broadcast messages - +You can provide a callback for the `broadcast` channel to receive message. In this example we will receive any `broadcast` messages in `room-1`: - ```bash - npm install @supabase/supabase-js - ``` +{/* prettier-ignore */} +```js +// Join a room/topic. Can be anything except for 'realtime'. +const channelA = clientA.channel('room-1') - +// Simple function to log any messages we receive +function messageReceived(payload) { + console.log(payload) +} - +// Subscribe to the Channel +channelA + .on( + 'broadcast', + { event: 'test' }, + (payload) => messageReceived(payload) + ) + .subscribe() +``` - - - +### Sending Broadcast messages - This client will be used to listen for messages. +We can send Broadcast messages using `channelB.send()`. Let's set up another client to send messages. - Go to your Supabase project's [API Settings](https://supabase.com/dashboard/project/_/settings/api) and grab the `URL` and `anon` public API key. +{/* prettier-ignore */} +```js +// Join a room/topic. Can be anything except for 'realtime'. +const channelB = clientA.channel('room-1') - +channelB.subscribe((status) => { + // Wait for successful connection + if (status !== 'SUBSCRIBED') { + return null + } - + // Send a message once the client is subscribed + channelB.send({ + type: 'broadcast', + event: 'test', + payload: { message: 'hello, world' }, + }) +}) +``` - ```js - import { - createClient - } from '@supabase/supabase-js' - - const clientA = createClient( - 'https://.supabase.co', - '' - ) - ``` - - - - - - - - - - A channel's topic can be anything except for `'realtime'`. - - - - - - ```js - const channelA = clientA.channel('room-1') - ``` - - - - - - - - - - Specify the Broadcast event you want the `on` handler to listen for. This event name can be anything you want. We'll send a broadcast message with this event name later on. - - - - - - ```js - channelA - .on( - 'broadcast', - { event: 'test' }, - (payload) => console.log(payload) - ) - .subscribe() - ``` - - - - - - - - - - This client will be used to send a message. - - - - - - ```js - const clientB = createClient( - 'https://.supabase.co', - '' - ) - ``` - - - - - - - - - - This channel's topic must match `channelA`'s. - - - - - - ```js - const channelB = clientB.channel('room-1') - ``` - - - - - - - - - - Subscribe to channel and send a message. - - The payload's `event` must match channelA's `event` in the `on` handler. - - - - - - ```js - channelB.subscribe((status) => { - if (status === 'SUBSCRIBED') { - channelB.send({ - type: 'broadcast', - event: 'test', - payload: { - message: 'hello, world' - }, - }) - } - }) - ``` - - - - - - - - - - `clientA` receives the message `clientB` sent. - - - - - - +Before sending messages we need to ensure the client is connected, which we have done within the `subscribe()` callback. ## Broadcast options @@ -193,27 +82,29 @@ There are additional Broadcast functionality that you can enable when creating a ### Self-send messages -You can have a client broadcast a message and then receive the same message by setting Broadcast's `self` config to `true`. Without this, broadcast messages are only sent to other clients. +By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `true`. +{/* prettier-ignore */} ```js -const channelC = clientC.channel('room-2', { +const myChannel = supabase.channel('room-2', { config: { - broadcast: { - self: true, - }, + broadcast: { self: true }, }, }) -channelC.on('broadcast', { event: 'test-my-messages' }, (payload) => console.log(payload)) +myChannel.on( + 'broadcast', + { event: 'test-my-messages' }, + (payload) => console.log(payload) +) -channelC.subscribe((status) => { - if (status === 'SUBSCRIBED') { - channelC.send({ - type: 'broadcast', - event: 'test-my-messages', - payload: { message: 'talking to myself' }, - }) - } +myChannel.subscribe((status) => { + if (status !== 'SUBSCRIBED') { return } + channelC.send({ + type: 'broadcast', + event: 'test-my-messages', + payload: { message: 'talking to myself' }, + }) }) ``` @@ -221,35 +112,29 @@ channelC.subscribe((status) => { You can confirm that Realtime received your message by setting Broadcast's `ack` config to `true`. +{/* prettier-ignore */} ```js -const channelD = clientD.channel('room-3', { +const myChannel = clientD.channel('room-3', { config: { - broadcast: { - ack: true, - }, + broadcast: { ack: true }, }, }) -channelD.subscribe(async (status) => { - if (status === 'SUBSCRIBED') { - const resp = await channelD.send({ - type: 'broadcast', - event: 'acknowledge', - payload: {}, - }) +myChannel.subscribe(async (status) => { + if (status !== 'SUBSCRIBED') { return } - console.log('resp', resp) - } + const serverResponse = await myChannel.send({ + type: 'broadcast', + event: 'acknowledge', + payload: {}, + }) + + console.log('serverResponse', serverResponse) }) ``` Use this to guarantee that the server has received the message before resolving `channelD.send`'s promise. If the `ack` config is not set to `true` when creating the channel, the promise returned by `channelD.send` will resolve immediately. -## More Realtime Quickstarts - -- [Presence Quickstart](/docs/guides/realtime/presence) -- [Postgres Changes Quickstart](/docs/guides/realtime/postgres-changes) - export const Page = ({ children }) => export default Page From da308f6e0bf9dc0fd48fb5ca80ce5bc2b2e607f6 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:26:20 +0200 Subject: [PATCH 07/15] presence usage --- apps/docs/pages/guides/realtime/broadcast.mdx | 2 +- apps/docs/pages/guides/realtime/presence.mdx | 262 ++++-------------- 2 files changed, 59 insertions(+), 205 deletions(-) diff --git a/apps/docs/pages/guides/realtime/broadcast.mdx b/apps/docs/pages/guides/realtime/broadcast.mdx index 37c4d263ed3..b4fddcce73e 100644 --- a/apps/docs/pages/guides/realtime/broadcast.mdx +++ b/apps/docs/pages/guides/realtime/broadcast.mdx @@ -78,7 +78,7 @@ Before sending messages we need to ensure the client is connected, which we have ## Broadcast options -There are additional Broadcast functionality that you can enable when creating a channel. +You can pass configuration options while initializing the Supabase Client. ### Self-send messages diff --git a/apps/docs/pages/guides/realtime/presence.mdx b/apps/docs/pages/guides/realtime/presence.mdx index ae645a42922..0e2f5c565b4 100644 --- a/apps/docs/pages/guides/realtime/presence.mdx +++ b/apps/docs/pages/guides/realtime/presence.mdx @@ -3,233 +3,92 @@ import StepHikeCompact from '~/components/StepHikeCompact' export const meta = { title: 'Presence', - subtitle: "Get up and running with Realtime's Presence feature", - breadcrumb: 'Realtime Presence Quickstart', + description: 'Share state between users wtih Realtime Presence.', + subtitle: 'Share state between users wtih Realtime Presence.', + // breadcrumb: 'Realtime Presence Quickstart', } -Presence can be used to share state between clients. Each client maintains their own piece of state within the shared state. - -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. - -## Quick start - Let's explore how to implement Realtime Presence so you can integrate it into your use case. - +## Usage - - - +You can use the Supabase client libraries to track Presence state between users. - Install the Supabase JavaScript client. +### Initialize the client - +Go to your Supabase project's [API Settings](https://supabase.com/dashboard/project/_/settings/api) and grab the `URL` and `anon` public API key. - +```js +import { createClient } from '@supabase/supabase-js' - ```bash - npm install @supabase/supabase-js - ``` +const SUPABASE_URL = 'https://.supabase.co' +const SUPABASE_KEY = '' - +const supabase = createClient(SUPABASE_URL, SUPABASE_KEY) +``` - +### Sync and track state - - - +Listen to the `sync`, `join`, and `leave` events triggered whenever any client joins or leaves the channel or changes their slice of state: - This client will be used to track Presence state as new clients join and leave the channel. +```js +const roomOne = supabase.channel('room_01') - Go to your Supabase project's [API Settings](https://supabase.com/dashboard/project/_/settings/api) and grab the `URL` and `anon` public API key. +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() +``` - +### Sending state - +You can send state to all subscribers using `track()`: - ```js - import { - createClient - } from '@supabase/supabase-js' +{/* prettier-ignore */} +```js +const roomOne = supabase.channel('room_01') - const clientA = createClient( - 'https://.supabase.co', - '' - ) - ``` +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) +}) +``` - - - +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. - A channel's topic can be anything except for `'realtime'`. +### Stop tracking - +You can stop tracking precense using the `untrack()` method. This will trigger the `sync` and `leave` event handlers. - +```js +const untrackPresence = async () => { + const presenceUntrackStatus = await roomOne.untrack() + console.log(presenceUntrackStatus) +} - ```js - const channelA = clientA.channel('room-1') - ``` +untrackPresence() +``` - +## Presence options - +You can pass configuration options while initializing the Supabase Client. - - - - - Listen to the `sync`, `join`, and `leave` events triggered whenever any client joins or leaves the channel or changes their slice of state. - - To begin tracking state, `clientA` calls `channelA.track()`, passing in the desired state to share. Once `clientA` successfully tracks its state, it will automatically trigger its own `sync` and `join` event handlers. - - - - - - ```js - channelA - .on( - 'presence', - { event: 'sync' }, - () => { - const newState = channelA.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(async (status) => { - if (status === 'SUBSCRIBED') { - const presenceTrackStatus = await channelA.track({ - user: 'user-1', - online_at: new Date().toISOString(), - }) - console.log(presenceTrackStatus) - } - }) - ``` - - - - - - - - - - This client will add to and remove from shared state so other clients can be notified of changes to Presence state. - - - - - - ```js - const clientB = createClient( - 'https://.supabase.co', - '' - ) - ``` - - - - - - - - - - This channel's topic must match `channelA`'s. - - - - - - ```js - const channelB = clientB.channel('room-1') - ``` - - - - - - - - - - Subscribe to channel and add to state. - - This will trigger `clientA`'s `sync` and `join` event handlers. - - - - - - ```js - channelB.subscribe(async (status) => { - if (status === 'SUBSCRIBED') { - const presenceTrackStatus = await channelA.track({ - user: 'user-2', - online_at: new Date().toISOString(), - }) - console.log(presenceTrackStatus) - } - }) - ``` - - - - - - - - - - This will trigger `clientA`'s `sync` and `leave` event handlers. - - - - - - ```js - const untrackPresence = async () => { - const presenceUntrackStatus = await channelB.untrack() - console.log(presenceUntrackStatus) - } - - untrackPresence() - ``` - - - - - - - -## Presence Key +### 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. @@ -245,11 +104,6 @@ const channelC = supabase.channel('test', { }) ``` -## More Realtime Quickstarts - -- [Broadcast Quickstart](/docs/guides/realtime/broadcast) -- [Postgres Changes Quickstart](/docs/guides/realtime/postgres-changes) - export const Page = ({ children }) => export default Page From 40e680d53ee2dc27c78e1c3131e5435e5b4a9e2b Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:14:31 +0200 Subject: [PATCH 08/15] Updates to postgres changes --- .../guides/realtime/postgres-changes.mdx | 298 +++++++++++------- apps/docs/pages/guides/realtime/presence.mdx | 2 +- 2 files changed, 178 insertions(+), 122 deletions(-) diff --git a/apps/docs/pages/guides/realtime/postgres-changes.mdx b/apps/docs/pages/guides/realtime/postgres-changes.mdx index c776b9d706c..ca04ce1d423 100644 --- a/apps/docs/pages/guides/realtime/postgres-changes.mdx +++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx @@ -3,19 +3,16 @@ import StepHikeCompact from '~/components/StepHikeCompact' export const meta = { title: 'Postgres Changes', - subtitle: "Get up and running with Realtime's Postgres Changes feature", - breadcrumb: 'Realtime Postgres Changes Quickstart', + subtitle: 'Listen to Postgres changes using Supabase Realtime.', + description: 'Listen to Postgres changes using Supabase Realtime.', + // breadcrumb: 'Realtime Postgres Changes Quickstart', } -Realtime's Postgres Changes feature listens for database changes and sends them to clients. Clients are required to subscribe with a JWT dictating which changes they are allowed to receive based on the database's [Row Level Security](/docs/guides/auth/row-level-security). - -Anyone with access to a valid JWT signed with the project's JWT secret is able to listen to your database's changes, unless tables have [Row Level Security](/docs/guides/auth/row-level-security) enabled and policies in place. - -Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the `public` schema and you must first enable the tables you want your clients to listen to. +Let's explore how to use Realtime's Postgres Changes feature to listen to database events. ## Quick start -Let's explore how to implement Realtime Postgres Changes so you can integrate it into your use case. +In this example we'll set up a database table, secure it with Row Level Security, and subscribe to all changes using the Supabase client libraries. @@ -81,10 +78,10 @@ Let's explore how to implement Realtime Postgres Changes so you can integrate it -- Allow anonymous access create policy "Allow anonymous access" - on todos - for select - to anon - using (true); + on todos + for select + to anon + using (true); ``` @@ -130,9 +127,7 @@ Let's explore how to implement Realtime Postgres Changes so you can integrate it ```js - import { - createClient - } from '@supabase/supabase-js' + import { createClient } from '@supabase/supabase-js' const client = createClient( 'https://.supabase.co', @@ -175,63 +170,6 @@ Let's explore how to implement Realtime Postgres Changes so you can integrate it - - - Listen to just inserts in the `todos` table by setting the `table` property to 'todos' and event name to `INSERT`. - - - - - - ```js - const channelB = client - .channel('table-db-changes') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'todos', - }, - (payload) => console.log(payload) - ) - .subscribe() - ``` - - - - - - Listen to changes in the `todos` table when a column's value equals a specified value. In this example, we only listen to inserts on `todos` where the row `id` is 1. - - - - - - ```js - const channelC = client - .channel('table-filter-changes') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'todos', - filter: 'id=eq.1', - }, - (payload) => console.log(payload) - ) - .subscribe() - ``` - - - - - - Check out the [full list of available filters](/docs/guides/realtime/postgres-changes#available-filters). - - - @@ -255,11 +193,139 @@ Let's explore how to implement Realtime Postgres Changes so you can integrate it +## Usage + +You can use the Supabase client libraries to subscribe to database changes. + +### Listeniing to specific schemas + +Subscribe to specific schema events using the `schema` parameter: + +{/* prettier-ignore */} +```js +const changes = client + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + schema: 'public', // Subscribes to the "public" schema in Postgres + event: '*', // Listen to all changes + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +The channel name can be any string except 'realtime'. + +### Listening to INSERT events + +Use the `event` parameter to listen only to database `INSERT`s: + +```js +const changes = client + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: 'INSERT', // Listen only to INSERTs + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +The channel name can be any string except 'realtime'. + +### Listening to UPDATE events + +Use the `event` parameter to listen only to database `UPDATE`s: + +```js +const changes = client + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', // Listen only to UPDATEs + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +The channel name can be any string except 'realtime'. + +### Listening to DELETE events + +Use the `event` parameter to listen only to database `UPDATE`s: + +```js +const changes = client + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: 'DELETE', // Listen only to DELETEs + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +The channel name can be any string except 'realtime'. + +### Listening to specific tables + +Subscribe to specific table events using the `table` parameter: + +```js +const changes = client + .channel('table-db-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'todos', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +The channel name can be any string except 'realtime'. + +### Filtering for specific changes + +Use the `filter` paramater for granular changes: + +```js +const changes = client + .channel('table-filter-changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'todos', + filter: 'id=eq.1', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + +The channel name can be any string except 'realtime'. + ## Available filters Realtime offers filters so you can specify the data your client receives at a more granular level. -### eq +### Equal to (eq) To listen to changes when a column's value in a table equals a client-specified value: @@ -279,9 +345,9 @@ const channel = supabase .subscribe() ``` -This filter uses Postgres' `=`. +This filter uses Postgres's `=` filter. -### neq +### Not equal to (neq) To listen to changes when a column's value in a table does not equal a client-specified value: @@ -301,9 +367,9 @@ const channel = supabase .subscribe() ``` -This filter uses Postgres' `!=`. +This filter uses Postgres's `!=` filter. -### lt +### Less than (lt) To listen to changes when a column's value in a table is less than a client-specified value: @@ -323,11 +389,9 @@ const channel = supabase .subscribe() ``` - - This filter uses Postgres' `<` so it works for non-numeric types but make sure to check the expected behavior of the compared data's type. - +This filter uses Postgres's `<` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. -### lte +### Less than or equal to (lte) To listen to changes when a column's value in a table is less than or equal to a client-specified value: @@ -347,11 +411,9 @@ const channel = supabase .subscribe() ``` - - This filter uses Postgres' `<=` so it works for non-numeric types but make sure to check the expected behavior of the compared data's type. - +This filter uses Postgres' `<=` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. -### gt +### Greater thank (gt) To listen to changes when a column's value in a table is greater than a client-specified value: @@ -371,12 +433,9 @@ const channel = supabase .subscribe() ``` - - This filter uses Postgres' `>` so it works for non-numeric types but make sure to check the - expected behavior of the compared data's type. - +This filter uses Postgres's `>` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. -### gte +### Greater than or equal to (gte) To listen to changes when a column's value in a table is greater than or equal to a client-specified value: @@ -396,12 +455,9 @@ const channel = supabase .subscribe() ``` - - This filter uses Postgres' `>=` so it works for non-numeric types but make sure to check the - expected behavior of the compared data's type. - +This filter uses Postgres's `>=` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. -### in +### Contained in list (in) To listen to changes when a column's value in a table equals any client-specified values: @@ -421,9 +477,7 @@ const channel = supabase .subscribe() ``` - - This filter uses Postgres' `= ANY`. Realtime allows a maximum of 100 values for this filter. - +This filter uses Postgres's `= ANY`. Realtime allows a maximum of 100 values for this filter. ## Combination changes @@ -489,9 +543,9 @@ You may choose to sign your own tokens to customize claims that can be checked i Your project JWT secret is found with your [Project API keys](https://app.supabase.com/project/_/settings/api) in your dashboard. +{/* prettier-ignore */} - Do not expose the `service_role` token on the client because the role is authorized to bypass - row-level security. + Do not expose the `service_role` token on the client because the role is authorized to bypass row-level security. To use your own JWT with Realtime make sure to set the token after instantiating the Supabase client and before connecting to a Channel. @@ -533,35 +587,37 @@ supabase.realtime.setAuth('fresh-token') ## Limitations -The current version of Realtime's Postgres Changes feature has some performance limitations to keep in mind. There can be a bottleneck on the database that limits the number of messages streamed to subscribed clients. +Realtime systems usually require forethought, because of their scaling dynamics. For the `Postgres Changes` feature, every change event must be checked to see if the subscribed user has access. This means that if you have 100 users subscribed to a table where you make a single insert, it will then trigger 100 "reads": one for each user. -The polling query that fetches the changes is currently single-threaded. If you are frequently changing your database, the polling query may not be able to fetch the changes rapidly enough. The delivery of the changes will be delayed until you will get timeout. Additionally, there is a polling query per subscribed client, which means that each unique pair of `filters` and `JWT token` (logged-in user) affects the performance of the polling query. In other words, RLS and filters can further limit the number of messages that can be streamed to subscribed clients. +There can be a bottleneck on the database that limits the number of messages streamed to subscribed clients. If you are frequently changing your database, the database query may not be verify the authorization rapidly enough and the delivery of the changes will be delayed until you will get timeout. -From our testing and observation of the performance of the polling query, we have found that the following limits are safe to use: +If you are listening to Postgres Changes at scale, you should consider using separate table without RLS and filters. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast. -| Database Add-on | Filters | RLS Usage | Concurrent Clients | Records per second per client | Messages per second (total) | Latency p95 (ms) | -| ---------------- | ------- | --------- | ------------------ | ----------------------------- | --------------------------- | ---------------- | -| micro < > medium | 🚫 | 🚫 | 500 | 10 | 5,000 | 257 | -| micro < > medium | 🚫 | 🚫 | 1,000 | 10 | 10,000 | 800 | -| micro < > medium | 🚫 | 🚫 | 5,000 | 2 | 10,000 | 1,120 | -| large and above | 🚫 | 🚫 | 1,000 | 10 | 10,000 | 261 | -| large and above | 🚫 | 🚫 | 5,000 | 2 | 10,000 | 952 | -| large and above | 🚫 | 🚫 | 10,000 | 1 | 10,000 | 949 | -| large and above | 🚫 | 🚫 | 200,000 | 0.04 (1 in 25 seconds) | 8,000 | 15,709 | -| large and above | βœ… | βœ… | 500 | 2 | 1,000 | 262 | -| large and above | βœ… | βœ… | 1,000 | 1 | 1,000 | 431 | -| large and above | βœ… | βœ… | 5,000 | 0.2 (1 in 5 seconds) | 1,000 | 702 | +From our observations, we recommend the following limits depending on your database size: -If you want to use Realtime's Postgres Changes feature you should be aware of these limitations. And if you are using this feature at scale, you should consider using separate table without RLS and filters for the purpose of streaming changes to subscribed clients. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast. Don't forget to run your own benchmarks to make sure that the performance is acceptable for your use case. +### Micro to medium -We are making many improvements to Realtime's Postgres Changes. +| Filters | RLS Usage | Concurrent Clients | Records per second per client | Messages per second (total) | Latency p95 (ms) | +| ------- | --------- | ------------------ | ----------------------------- | --------------------------- | ---------------- | +| 🚫 | 🚫 | 500 | 10 | 5,000 | 257 | +| 🚫 | 🚫 | 1,000 | 10 | 10,000 | 800 | +| 🚫 | 🚫 | 5,000 | 2 | 10,000 | 1,120 | -If you are uncertain about the performance of your use case, please reach out using [Support Form](https://supabase.com/dashboard/support/new) and we will be happy to help you. We have a team of engineers that can advise you on the best solution for your use-case. +### Large and above -## More Realtime Quickstarts +| Filters | RLS Usage | Concurrent Clients | Records per second per client | Messages per second (total) | Latency p95 (ms) | +| ------- | --------- | ------------------ | ----------------------------- | --------------------------- | ---------------- | +| 🚫 | 🚫 | 1,000 | 10 | 10,000 | 261 | +| 🚫 | 🚫 | 5,000 | 2 | 10,000 | 952 | +| 🚫 | 🚫 | 10,000 | 1 | 10,000 | 949 | +| 🚫 | 🚫 | 200,000 | 0.04 (1 in 25 seconds) | 8,000 | 15,709 | +| βœ… | βœ… | 500 | 2 | 1,000 | 262 | +| βœ… | βœ… | 1,000 | 1 | 1,000 | 431 | +| βœ… | βœ… | 5,000 | 0.2 (1 in 5 seconds) | 1,000 | 702 | -- [Broadcast Quickstart](/docs/guides/realtime/broadcast) -- [Presence Quickstart](/docs/guides/realtime/presence) +Don't forget to run your own benchmarks to make sure that the performance is acceptable for your use case. + +We are making many improvements to Realtime's Postgres Changes. If you are uncertain about the performance of your use case, please reach out using [Support Form](https://supabase.com/dashboard/support/new) and we will be happy to help you. We have a team of engineers that can advise you on the best solution for your use-case. export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/realtime/presence.mdx b/apps/docs/pages/guides/realtime/presence.mdx index 0e2f5c565b4..b0d34165ce7 100644 --- a/apps/docs/pages/guides/realtime/presence.mdx +++ b/apps/docs/pages/guides/realtime/presence.mdx @@ -8,7 +8,7 @@ export const meta = { // breadcrumb: 'Realtime Presence Quickstart', } -Let's explore how to implement Realtime Presence so you can integrate it into your use case. +Let's explore how to implement Realtime Presence to track state between multiple users. ## Usage From e418a6092fd2f3386e54173456604894b6f877b0 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:29:40 +0200 Subject: [PATCH 09/15] more clarity --- .../guides/realtime/postgres-changes.mdx | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/apps/docs/pages/guides/realtime/postgres-changes.mdx b/apps/docs/pages/guides/realtime/postgres-changes.mdx index ca04ce1d423..3481a690bbc 100644 --- a/apps/docs/pages/guides/realtime/postgres-changes.mdx +++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx @@ -299,6 +299,34 @@ const changes = client The channel name can be any string except 'realtime'. +### Listening to multiple changes + +To listen to different events and schema/tables/filters combinations with the same channel: + +```js +const channel = supabase + .channel('db-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'messages', + }, + (payload) => console.log(payload) + ) + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'users', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + ### Filtering for specific changes Use the `filter` paramater for granular changes: @@ -319,8 +347,6 @@ const changes = client .subscribe() ``` -The channel name can be any string except 'realtime'. - ## Available filters Realtime offers filters so you can specify the data your client receives at a more granular level. @@ -479,36 +505,7 @@ const channel = supabase This filter uses Postgres's `= ANY`. Realtime allows a maximum of 100 values for this filter. -## Combination changes - -To listen to different events and schema/tables/filters combinations with the same channel: - -```js -const channel = supabase - .channel('db-changes') - .on( - 'postgres_changes', - { - event: '*', - schema: 'public', - table: 'messages', - filter: 'body=eq.bye', - }, - (payload) => console.log(payload) - ) - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'users', - }, - (payload) => console.log(payload) - ) - .subscribe() -``` - -## Full `old` record +## Receiveing `old` records 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`: @@ -519,7 +516,7 @@ alter table -RLS policies are not applied to `DELETE` statements. When RLS is enabled and `replica identity` is set to `full` on a table, the `old` record contains only the primary key(s). +RLS policies are not applied to `DELETE` statements, because there is no way for Postgres to verify that a user has access to a deleted record. When RLS is enabled and `replica identity` is set to `full` on a table, the `old` record contains only the primary key(s). @@ -587,11 +584,11 @@ supabase.realtime.setAuth('fresh-token') ## Limitations -Realtime systems usually require forethought, because of their scaling dynamics. For the `Postgres Changes` feature, every change event must be checked to see if the subscribed user has access. This means that if you have 100 users subscribed to a table where you make a single insert, it will then trigger 100 "reads": one for each user. +Realtime systems usually require forethought because of their scaling dynamics. For the `Postgres Changes` feature, every change event must be checked to see if the subscribed user has access. For instance, if you have 100 users subscribed to a table where you make a single insert, it will then trigger 100 "reads": one for each user. -There can be a bottleneck on the database that limits the number of messages streamed to subscribed clients. If you are frequently changing your database, the database query may not be verify the authorization rapidly enough and the delivery of the changes will be delayed until you will get timeout. +There can be a database bottleneck which limits message throughput. If your database cannot authorize the changes rapidly enough, the changes will be delayed until you receive a timeout. -If you are listening to Postgres Changes at scale, you should consider using separate table without RLS and filters. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast. +If you are using Postgres Changes at scale, you should consider using separate "public" table without RLS and filters. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast. From our observations, we recommend the following limits depending on your database size: From 2477b89ef78be111260f4bf18f78fcadc90cf156 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:38:25 +0200 Subject: [PATCH 10/15] reads better like this --- .../guides/client-side-throttling.mdx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx b/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx index bc1624568a9..55ff58726de 100644 --- a/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx +++ b/apps/docs/pages/guides/realtime/guides/client-side-throttling.mdx @@ -7,21 +7,7 @@ export const meta = { subtitle: 'Use client-side throttling to manage message frequency.', } -You should always consider optimizing the performance of you realtime system. - -## Project Quotas - -Each Broadcast and Presence message counts towards your [Project Quotas](/docs/guides/realtime/quotas). - -It's common to unintentionally flood the Realtime service with messages. For example, tracking mouse movents without throttling would send hundreds of events per second. It's rare that you need so many messages. Updating a mouse movement even a few times per second is usually enough for the human eye. - -The Supabase client includes a configurable throttling parameter to protect against these unintended floods. - -## Default client throttling - -By default the Supabase clients throttles messages to 10 messages per-second (1 message every 100 milliseconds). This is a soft-limit provided as a safe-guard when you're getting started. You'll rarely need to send more messages than this. - -Each client has their own throttling behavior. For example, if you instantiate two clients, by default you would send 20 messages per-second to your project. +The Supabase clients include functionality for throttling messages. ## Managing client-side throttling @@ -42,6 +28,20 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { }) ``` +## Default client throttling + +By default the Supabase clients throttles messages to 10 messages per-second (1 message every 100 milliseconds). This is a soft-limit provided as a safe-guard when you're getting started. You'll rarely need to send more messages than this. + +Each client has their own throttling behavior. If you instantiate two clients, by default you would send 20 messages per-second to your project. + +## Project Quotas + +Each Broadcast and Presence message counts towards your [Project Quotas](/docs/guides/realtime/quotas). + +It's common to unintentionally flood the Realtime service with messages. For example, tracking mouse movents without throttling would send hundreds of events per second. It's rare that you need so many messages. Updating a mouse movement even a few times per second is usually enough for the human eye. + +The throttling parameter protects against these unintended floods. + export const Page = ({ children }) => export default Page From 1a4b0f64fe194459d59a26924f46e69c9f4c7501 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:16:05 -0700 Subject: [PATCH 11/15] clean up realtime landing page --- apps/docs/pages/guides/realtime.mdx | 53 +++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/apps/docs/pages/guides/realtime.mdx b/apps/docs/pages/guides/realtime.mdx index 4b33c2567de..9014147b0d4 100644 --- a/apps/docs/pages/guides/realtime.mdx +++ b/apps/docs/pages/guides/realtime.mdx @@ -14,10 +14,57 @@ Supabase provides a globally distributed cluster of [Realtime](https://github.co - [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. -## See Also +## Examples -- [Realtime: Multiplayer Edition](https://supabase.com/blog/supabase-realtime-multiplayer-general-availability) blog post +
+ {examples.map((x) => ( + + ))} +
-export const Page = ({ children }) => +export const examples = [ + { + name: 'Multiplayer.dev', + description: 'Mouse movements and chat messages.', + href: 'https://multiplayer.dev', + }, +] + +## Resources + +Find the source code and documentation in the Supabase GitHub repository. + +
+ {resources.map((x) => ( + + ))} +
+ +export const resources = [ + { + name: 'Supabase Realtime', + description: 'View the source code.', + href: 'https://github.com/supabase/realtime', + }, + { + name: 'Realtime: Multiplayer Edition', + description: 'Read more about Supabase Realtime.', + href: 'https://supabase.com/blog/supabase-realtime-multiplayer-general-availability', + }, +] + +export const Page = ({ children }) => export default Page From 660cc3cf7f510e46b6bc448ae46cfb3255a9af3e Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:16:17 -0700 Subject: [PATCH 12/15] clean up deep dive --- .../NavigationMenu.constants.ts | 12 ++--- apps/docs/pages/guides/realtime/quotas.mdx | 52 ++++++++----------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 9cf59275895..7ed98e74b22 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -894,11 +894,6 @@ export const realtime: NavMenuConstant = { name: 'Subscribing to Database Changes', url: '/guides/realtime/subscribing-to-database-changes', }, - { - name: 'Bring Your Own Database', - url: '/guides/realtime/bring-your-own-database', - items: [], - }, { name: 'Using Realtime with Next.js', url: '/guides/realtime/realtime-with-nextjs', @@ -911,7 +906,12 @@ export const realtime: NavMenuConstant = { items: [ { name: 'Quotas', url: '/guides/realtime/quotas' }, { name: 'Architecture', url: '/guides/realtime/architecture' }, - { name: 'Protocol', url: '/guides/realtime/protocol' }, + { name: 'Message Protocol', url: '/guides/realtime/protocol' }, + { + name: 'Bring Your Own Database', + url: '/guides/realtime/bring-your-own-database', + items: [], + }, ], }, ], diff --git a/apps/docs/pages/guides/realtime/quotas.mdx b/apps/docs/pages/guides/realtime/quotas.mdx index 8f9ec1e6121..c26e1ad65cf 100644 --- a/apps/docs/pages/guides/realtime/quotas.mdx +++ b/apps/docs/pages/guides/realtime/quotas.mdx @@ -7,9 +7,11 @@ export const meta = { sidebar_label: 'Quotas', } +Our cluster supports millions of concurrent connections and message throughput for production workloads. + -Upgrade your plan to increase your quotas. Without a spend cap, or on an Enterprise plan, some quotas are still in place to protect budgets. All quotas are configurable per project. [Contact support](https://supabase.com/dashboard/support/new) if you need your quotas increased. Our cluster supports millions of concurrent connections and message throughput for production workloads. +Upgrade your plan to increase your quotas. Without a spend cap, or on an Enterprise plan, some quotas are still in place to protect budgets. All quotas are configurable per project. [Contact support](https://supabase.com/dashboard/support/new) if you need your quotas increased. @@ -28,47 +30,39 @@ Upgrade your plan to increase your quotas. Without a spend cap, or on an Enterpr Beyond the Free and Pro plan you can customize your quotas by [contacting support](https://supabase.com/dashboard/support/new). -## Client-Side Limiting +## Client-Side throttling -Some basic WebSocket message rate limiting is implemented client-side. - -For example, the [multiplayer.dev](https://multiplayer.dev) instantiates the Supabase client with an `eventsPerSecond` parameter. +Some basic WebSocket message throttling is implemented client-side. See the [Throttling](/docs/guides/realtime/guides/client-side-throttling) guide for more details. ## Quota Errors -When you reach a quota errors can appear in backend logs and messages in the WebSocket connection. +When you exceed a quota, errors will appear in the backend logs and client-side messages in the WebSocket connection. - +- **Logs**: check the [Realtime logs](https://supabase.com/dashboard/project/_/database/realtime-logs) inside your project Dashboard. +- **Websocket errors**: Use your browser's developer tools to find the WebSocket initiation request and view individual messages. -Use the [Realtime Inspector](https://realtime.supabase.com/inspector/new) to reproduce an error and share those connection details with Supabase support. + + +You can use the [Realtime Inspector](https://realtime.supabase.com/inspector/new) to reproduce an error and share those connection details with Supabase support. - -### Backend Logs - -If your project is being limited by a quota, check your [Realtime logs](https://supabase.com/dashboard/project/_/database/realtime-logs). - -### WebSocket Errors - -- `tenant_events`: Clients will be disconnected if your project is generating too many messages per second. `supabase-js` should reconnect automatically when the message throughput decreases below your plan quota. - - - -An `event` is a WebSocket message delivered to, or sent from a client. - - - Some quotas can cause a Channel join to be refused. Realtime will reply with one of the following WebSocket messages: -- `too_many_channels`: Too many channels currently joined for a single client. -- `too_many_connections`: Too many total concurrent connections for a project. -- `too_many_joins`: Too many Channel joins per second. +### `too_many_channels` - +Too many channels currently joined for a single client. -Use your browser's developer tools to find the WebSocket initiation request and view individual messages. +### `too_many_connections` - +Too many total concurrent connections for a project. + +### `too_many_joins` + +Too many Channel joins per second. + +### `tenant_events` + +Clients will be disconnected if your project is generating too many messages per second. `supabase-js` will reconnect automatically when the message throughput decreases below your plan quota. An `event` is a WebSocket message delivered to, or sent from a client. ## Postgres Changes Payload Quota From 7578e10fb777961976354535db61f62e2024212d Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:34:55 -0700 Subject: [PATCH 13/15] remove unused channels --- apps/docs/pages/guides/realtime/postgres-changes.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/realtime/postgres-changes.mdx b/apps/docs/pages/guides/realtime/postgres-changes.mdx index 3481a690bbc..1d14bd7cae0 100644 --- a/apps/docs/pages/guides/realtime/postgres-changes.mdx +++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx @@ -175,7 +175,7 @@ In this example we'll set up a database table, secure it with Row Level Security - Now we can add some data to our table which will trigger `channelA`, `channelB`, and `channelC` event handlers. + Now we can add some data to our table which will trigger the `channelA` event handler. From 4019855f460bc1c9dec61dabd731928269c2f569 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Wed, 13 Sep 2023 02:29:30 +0200 Subject: [PATCH 14/15] Apply suggestions from code review --- apps/docs/pages/guides/realtime/presence.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/realtime/presence.mdx b/apps/docs/pages/guides/realtime/presence.mdx index b0d34165ce7..2b9c271feec 100644 --- a/apps/docs/pages/guides/realtime/presence.mdx +++ b/apps/docs/pages/guides/realtime/presence.mdx @@ -3,8 +3,8 @@ import StepHikeCompact from '~/components/StepHikeCompact' export const meta = { title: 'Presence', - description: 'Share state between users wtih Realtime Presence.', - subtitle: 'Share state between users wtih Realtime Presence.', + description: 'Share state between users with Realtime Presence.', + subtitle: 'Share state between users with Realtime Presence.', // breadcrumb: 'Realtime Presence Quickstart', } From 73461bb0933a927b769abf3006a12dc1defb949e Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Wed, 13 Sep 2023 09:40:48 -0700 Subject: [PATCH 15/15] spelling --- apps/docs/pages/guides/database/connecting-to-postgres.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/database/connecting-to-postgres.mdx b/apps/docs/pages/guides/database/connecting-to-postgres.mdx index d19cb3c126a..5c25b92b16e 100644 --- a/apps/docs/pages/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/pages/guides/database/connecting-to-postgres.mdx @@ -15,7 +15,7 @@ Supabase provides several options for programmatically connecting to your Postgr ## Serverless APIs -Supabase provides auto-updating [APIs](/docs/guides/database/api). This is the easiest way to get started if you are managing data (fetching, inserting, updating). We provides several types of API to suit your preferences: +Supabase provides auto-updating [APIs](/docs/guides/database/api). This is the easiest way to get started if you are managing data (fetching, inserting, updating). We provide several types of API to suit your preferences: - [REST](/docs/guides/api#rest-api-overview): interact with your database through a REST interface. - [GraphQL](/docs/guides/api#graphql-api-overview): interact with your database through a GraphQL interface.