diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index 1845aa29ca5..5487e76808a 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -136,3 +136,9 @@ Create a new entry in the [`redirects.js`](https://github.com/supabase/supabase/
## Community channels
Stuck somewhere? Have any questions? Join the [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
+
+## Contributors
+
+
+
+
diff --git a/apps/docs/components/Navigation/Navigation.constants.ts b/apps/docs/components/Navigation/Navigation.constants.ts
index 94f40fce1ee..beb5bdc962d 100644
--- a/apps/docs/components/Navigation/Navigation.constants.ts
+++ b/apps/docs/components/Navigation/Navigation.constants.ts
@@ -241,12 +241,57 @@ export const menuItems: NavMenu = {
{
label: 'Realtime',
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: [] },
+ {
+ name: 'Overview',
+ url: '/guides/realtime',
+ items: [],
+ },
+ {
+ name: 'Quickstart',
+ url: '/guides/realtime/quickstart',
+ items: [],
+ },
+ {
+ name: 'Features',
+ url: undefined,
+ items: [
+ { name: 'Channels', url: '/guides/realtime/channels', items: [] },
+ {
+ name: 'Extensions',
+ url: '/guides/realtime/extensions',
+ 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: 'Guides',
+ url: undefined,
+ items: [
+ {
+ name: 'Subscribing to Database Changes',
+ url: '/guides/realtime/subscribing-to-database-changes',
+ items: [],
+ },
+ {
+ name: 'Using Realtime with Next.js',
+ url: '/guides/realtime/realtime-with-nextjs',
+ items: [],
+ },
+ ],
+ },
+ {
+ name: 'Deep dive',
+ url: undefined,
+ items: [
+ { name: 'Rate Limits', url: '/guides/realtime/rate-limits', items: [] },
+ { name: 'Architecture', url: '/guides/realtime/architecture', items: [] },
+ { name: 'Protocol', url: '/guides/realtime/protocol', items: [] },
+ ],
+ },
],
},
{
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index f30bfec7b43..f677f5ebc76 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -604,16 +604,53 @@ export const realtime = {
label: 'Realtime',
url: '/guides/realtime',
items: [
- { name: 'Overview', url: '/guides/realtime', items: [] },
- { name: 'Quickstart', url: '/guides/realtime/quickstart', items: [] },
{
- name: 'Channels',
+ name: 'Overview',
+ url: '/guides/realtime',
+ },
+ {
+ name: 'Quickstart',
+ url: '/guides/realtime/quickstart',
+ },
+ {
+ name: 'Features',
+ url: undefined,
+ items: [
+ { name: 'Channels', url: '/guides/realtime/channels', items: [] },
+ {
+ name: 'Extensions',
+ url: '/guides/realtime/extensions',
+ 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: 'Guides',
+ url: undefined,
+ items: [
+ {
+ name: 'Subscribing to Database Changes',
+ url: '/guides/realtime/subscribing-to-database-changes',
+ items: [],
+ },
+ {
+ name: 'Using Realtime with Next.js',
+ url: '/guides/realtime/realtime-with-nextjs',
+ items: [],
+ },
+ ],
+ },
+ {
+ name: 'Deep dive',
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: [] },
+ { name: 'Architecture', url: '/guides/realtime/architecture', items: [] },
+ { name: 'Protocol', url: '/guides/realtime/protocol', items: [] },
],
},
],
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx
index 7b6aaf26a64..13b70ac1e2e 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx
@@ -205,7 +205,7 @@ const NavigationMenu = () => {
-
+
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
index c55ec3b6f0d..c08f6d9f061 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
@@ -6,8 +6,9 @@ import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
interface Props {
id: string
active: boolean
+ collapsible?: boolean
}
-const NavigationMenuGuideList: React.FC = ({ id, active }) => {
+const NavigationMenuGuideList: React.FC = ({ id, active, collapsible = true }) => {
const router = useRouter()
// get url
@@ -26,7 +27,7 @@ const NavigationMenuGuideList: React.FC = ({ id, active }) => {
return (
+
+
+```sql
+create function title_description(books) returns text as $$
+ select $1.title || ' ' || $1.description;
+$$ language sql immutable;
+```
+
+```js
+const { data, error } = await supabase
+ .from('books')
+ .select()
+ .textSearch('title_description', `little`)
+```
+
+
+
+
+```sql
+create function title_description(books) returns text as $$
+ select $1.title || ' ' || $1.description;
+$$ language sql immutable;
+```
+
+```dart
+final result = await client
+ .from('books')
+ .select()
+ .textSearch('title_description', "little")
+```
+
diff --git a/apps/docs/pages/guides/platform/backups.mdx b/apps/docs/pages/guides/platform/backups.mdx
index 83d606cd856..bf942d9923f 100644
--- a/apps/docs/pages/guides/platform/backups.mdx
+++ b/apps/docs/pages/guides/platform/backups.mdx
@@ -9,7 +9,9 @@ Database backups are an integral part of any disaster recovery plan. Disasters c
## Frequency of Backups
-When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery. The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
+When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery (PITR). The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
+
+Daily Backups and PITR are mutually exclusive. If your project opts into using PITR, Daily Backups will no longer be taken. They're also unnecessary, as PITR supports a superset of functionality, in terms of the granular recovery that can be performed.
Database backups do not include objects stored via the Storage API, as the database only includes
diff --git a/apps/docs/pages/guides/platform/logs.mdx b/apps/docs/pages/guides/platform/logs.mdx
index a748b241fbc..897f22cebda 100644
--- a/apps/docs/pages/guides/platform/logs.mdx
+++ b/apps/docs/pages/guides/platform/logs.mdx
@@ -60,6 +60,12 @@ Supabase provides a logging interface specific to each product. You can use simp
[Realtime logs](https://app.supabase.com/project/_/logs/realtime-logs) show all server logs for your [Realtime API usage](../../guides/realtime).
+
+
+Realtime connections are not logged by default. Turn on [Realtime connection logs per client](#logging-realtime-connections) with the `log_level` parameter.
+
+
+

@@ -126,6 +132,21 @@ If any permission errors are encountered when executing `alter role postgres ...
+## Logging Realtime Connections
+
+Realtime doesn't log new WebSocket connections or Channel joins by default. Enable connection logging per client by including an `info` `log_level` parameter when instantiating the Supabase client.
+
+```javascript
+import { createClient } from '@supabase/supabase-js'
+
+const options = {
+ realtime: {
+ log_level: 'info',
+ },
+}
+const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key', options)
+```
+
## Logs Explorer
The [Logs Explorer](https://app.supabase.com/project/_/logs-explorer) exposes logs from each part of the Supabase stack as a separate table that can be queried and joined using SQL.
diff --git a/apps/docs/pages/guides/realtime/architecture.mdx b/apps/docs/pages/guides/realtime/architecture.mdx
new file mode 100644
index 00000000000..5a4173bdb88
--- /dev/null
+++ b/apps/docs/pages/guides/realtime/architecture.mdx
@@ -0,0 +1,58 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'architecture',
+ title: 'Realtime Architecture',
+ description: 'Architecture of the Supabase Realtime service',
+ sidebar_label: 'Architecture',
+}
+
+Realtime is a globally distributed Elixir cluster. Clients can connect to any node in the cluster via WebSockets and send messages to any other client connected to the cluster.
+
+Realtime is written in [Elixir](https://elixir-lang.org/), which compiles to [Erlang](https://www.erlang.org/), and utilizes many tools the [Phoenix Framework](https://www.phoenixframework.org/) provides out of the box.
+
+
+
+## Elixir & Phoenix
+
+Phoenix is fast and able to handle millions of concurrent connections.
+
+Phoenix can handle many concurrent connections because Elixir provides lightweight processes (not OS processes) to work with.
+
+Client-facing WebSocket servers need to handle many concurrent connections. Elixir & Phoenix let the Supabase Realtime cluster do this easily.
+
+## Global Cluster
+
+Presence is an in-memory key-value store backed by a CRDT. When a user is connected to the cluster the state of that user is sent to all connected Realtime nodes.
+
+Broadcast lets you send a message from any connected client to a Channel. Any other client connected to that same Channel will receive that message.
+
+This works globally. A client connected to a Realtime node in the United States can send a message to another client connected to a node in Singapore. Simply connect two clients to the same Realtime Channel and they'll all receive the same messages.
+
+Broadcast is useful for getting messages to users in the same location very quickly. If a group of clients are connected to a node in Singapore, the message only needs to go to that Realtime node in Singapore and back down. If users are close to a Realtime node they'll get Broadcast messages in the time it takes to ping the cluster.
+
+Thanks to the Realtime cluster, you (an amazing Supabase user) don't have to think about which regions your clients are connected to.
+
+If you're using Broadcast, Presence, or streaming database changes, messages will always get to your users via the shortest path possible.
+
+## Connecting to a Database
+
+Realtime allows you to listen to changes from your Postgres database. When a new client connects to Realtime and initializes the `postgres_changes` Realtime Extension the cluster will connect to your Postgres database and start streaming changes from a replication slot.
+
+Realtime knows the region your database is in, and connects to it from the closest region possible.
+
+Every Realtime region has at least two nodes so if one node goes offline the other node should reconnect and start streaming changes again.
+
+## Streaming the Write-Ahead Log
+
+A Postgres logical replication slot is acquired when connecting to your database.
+
+Realtime delivers changes by polling the replication slot and appending channel subscription IDs to each wal record.
+
+Subscription IDs are Erlang processes representing underlying sockets on the cluster. These IDs are globally unique and messages to processes are routed automatically by the Erlang virtual machine.
+
+After receiving results from the polling query, with subscription IDs appended, Realtime delivers records to those clients.
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/pages/guides/realtime/channels.mdx b/apps/docs/pages/guides/realtime/channels.mdx
new file mode 100644
index 00000000000..b016f4bf6f6
--- /dev/null
+++ b/apps/docs/pages/guides/realtime/channels.mdx
@@ -0,0 +1,22 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'channels',
+ title: 'Realtime Channels',
+ description: 'WebSockets are Channels on Supabase Realtime',
+ sidebar_label: 'Channels',
+}
+
+You can uniquely reference a channel by its `topic` you define when you initialize your Supabase Realtime client. Everyone connected to the same Channel topic will receive the same messages.
+
+Clients can send and receive messages bi-directionally over a Channel. The Realtime backend can also push messages to all clients connected to the same Channel.
+
+A single client can receive change records from Postgres, Broadcast messages from other clients and Presence updates all over the same Channel.
+
+Channels are implemented over [Phoenix Channels](https://hexdocs.pm/phoenix/channels.html) which uses [Phoenix.PubSub](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) with the default `Phoenix.PubSub.PG2` adapter.
+
+The PG2 adapter utilizes Erlang [process groups](https://www.erlang.org/docs/18/man/pg2.html) to implement the PubSub model where a publisher can send messages to many subscribers.
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/pages/guides/realtime/extensions.mdx b/apps/docs/pages/guides/realtime/extensions.mdx
new file mode 100644
index 00000000000..bad8b1daf66
--- /dev/null
+++ b/apps/docs/pages/guides/realtime/extensions.mdx
@@ -0,0 +1,27 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'extensions',
+ title: 'Realtime Extensions',
+ description:
+ 'Extensions enable powerful Realtime features like Presence, Broadcast and database change streaming',
+ sidebar_label: 'Extensions',
+}
+
+Supabase Realtime provides multiplayer features so that you can build real-time applications. The server is has several extensions which are built to solve a specific task.
+
+- [Broadcast](/docs/guides/realtime/broadcast): for sending rapid, ephemeral messages to other connected clients. A typical use-case is tracking mouse movements.
+- [Presence](/docs/guides/realtime/presence): for sending user state between connected clients. A typical use-case is simply an "online" status, which will disappear when a user is disconnected.
+- [Postgres Changes](/docs/guides/realtime/postgres-changes): for receiving database changes in real-time.
+
+## Choosing between Broadcast and Presence
+
+Defaulting to using Broadcast is a good idea, and then use Presence sparingly. The Presence extension merges all changes into a shared state for every client connected to the same "channel". If you _do_ use Presence, it's best to throttle your changes so that you are sending updates less frequently.
+
+## Future possibilities
+
+Realtime extensions are still under development. We have many more extensions planned. You can follow our progress by watching the [Realtime](https://github.com/supabase/realtime) repo on GitHub.
+
+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 0f3b15e6bac..07b819990ac 100644
--- a/apps/docs/pages/guides/realtime/postgres-changes.mdx
+++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx
@@ -15,7 +15,9 @@ Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes
Postgres Changes works out of the box for tables in the `public` schema. You can listen to tables in your private schemas by granting table `SELECT` permissions to the database role found in your access token. You can run a query similar to the following:
```sql
-GRANT SELECT ON "private_schema"."table" TO authenticated;
+grant
+select
+ on "private_schema"."table" to authenticated;
```
@@ -29,15 +31,19 @@ You can do this in the [Replication](https://app.supabase.com/project/_/database
```sql
begin;
- -- remove the supabase_realtime publication
- drop publication if exists supabase_realtime;
- -- re-create the supabase_realtime publication with no tables
- create publication supabase_realtime;
+-- remove the supabase_realtime publication
+drop
+ publication if exists supabase_realtime;
+
+-- re-create the supabase_realtime publication with no tables
+create publication supabase_realtime;
+
commit;
-- add a table to the publication
-alter publication supabase_realtime add table messages;
+alter
+ publication supabase_realtime add table messages;
```
### Full `old` Record
@@ -46,9 +52,16 @@ By default, only `new` record changes are sent but if you want to receive the `o
you can set the `replica identity` of your table to `full`:
```sql
-alter table messages replica identity full;
+alter table
+ messages replica identity full;
```
+
+
+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).
+
+
+
## Schema Changes
To listen to all changes in the `public` schema:
diff --git a/apps/docs/pages/guides/realtime/protocol.mdx b/apps/docs/pages/guides/realtime/protocol.mdx
new file mode 100644
index 00000000000..488d2975612
--- /dev/null
+++ b/apps/docs/pages/guides/realtime/protocol.mdx
@@ -0,0 +1,194 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'protocol',
+ title: 'Realtime Protocol',
+ description: 'Understanding Realtime Protocol',
+}
+
+The Realtime Protocol is a set of message formats used for communication over a WebSocket connection between a Realtime client and server. These messages are used to initiate a connection, update access tokens, receive system status updates, and receive real-time updates from the Postgres database.
+
+## Connection
+
+In the initial message, the client sends a message specifying the features they want to use (Broadcast, Presence, Postgres Changes).
+
+```ts
+{
+ "event": "phx_join",
+ "topic": string,
+ "payload": {
+ "config": {
+ "broadcast": {
+ "self": boolean
+ },
+ "presence": {
+ "key": string
+ },
+ "postgres_changes": [
+ {
+ "event": "*" | "INSERT" | "UPDATE" | "DELETE",
+ "schema": string,
+ "table": string,
+ "filter": string + '=' + "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" + '.' + string
+ }
+ ]
+ }
+ },
+ "ref": string
+}
+```
+
+
+The `in` filter has the format `COLUMN_NAME=in.(value1,value2,value3)`. However, other filters use the format `COLUMN_NAME=FILTER_NAME.value`.
+
+
+In response, the server sends the Postgres configuration with a unique ID. With this ID, the client should route incoming changes to the appropriate callback.
+
+```ts
+{
+ "event": "phx_reply",
+ "topic": string,
+ "payload": {
+ "response": {
+ "postgres_changes": [
+ {
+ "id": number,
+ "event": "*" | "INSERT" | "UPDATE" | "DELETE",
+ "schema": string,
+ "table": string,
+ "filter": string + '=' + "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" + '.' + string
+ }
+ ]
+ },
+ "status": "ok" | "error"
+ },
+ "ref": string
+}
+```
+
+## System messages
+
+System message are used to inform a client about the status of the Postgres subscription. The `payload.status` indicates if the subscription successful or not.
+The body of the `payload.message` can be "Subscribed to PostgreSQL" or "Subscribing to PostgreSQL failed" with subscription params.
+
+```ts
+{
+ "event": "system",
+ "topic": string,
+ "payload":{
+ "channel": string,
+ "extension": "postgres_changes",
+ "message": "Subscribed to PostgreSQL" | "Subscribing to PostgreSQL failed",
+ "status": "ok" | "error"
+ },
+ "ref": null,
+}
+```
+
+## Heartbeat
+
+The heartbeat message should be sent every 30 seconds to avoid a connection timeout.
+
+```ts
+{
+ "event": "heartbeat",
+ "topic": "phoenix",
+ "payload": {},
+ "ref": string
+}
+```
+
+## Access Token
+
+To update the access token, you need to send to the server a message specifying a new token in the `payload.access_token` value.
+
+```ts
+{
+ "event": "access_token",
+ "topic": string,
+ "payload":{
+ "access_token": string
+ },
+ "ref": string
+}
+```
+
+## Postgres CDC message
+
+Realtime sends a message with the following structure
+
+```ts
+{
+ "event": "postgres_changes",
+ "topic": string,
+ "payload": {
+ "data": {
+ "columns": Array<{name: string, type: string}>,
+ "commit_timestamp": string,
+ "errors": null | string,
+ "old_record": {"id": number | string},
+ "record": {[key: string]: boolean | number | string | null},
+ "type": "*" | "INSERT" | "UPDATE" | "DELETE",
+ "schema": string,
+ "table": string
+ },
+ "ids": Array
+ },
+ "ref": null
+}
+```
+
+## Broadcast message
+
+Structure of the broadcast event
+
+```ts
+{
+ "event": "broadcast",
+ "topic": string,
+ "payload": {
+ "event": string,
+ "payload": {[key: string]: boolean | number | string | null | undefined},
+ "type": "broadcast"
+ },
+ "ref": null
+}
+```
+
+## Presence message
+
+The Presence events allow clients to monitor the online status of other clients in real-time.
+
+### State Update
+
+After joining, the server sends a `presence_state` message to a client with presence information. The payload field contains keys in UUID format, where each key represents a client and its value is a JSON object containing information about that client.
+
+```ts
+{
+ "event": "presence_state",
+ "topic": string,
+ "payload": {
+ [key: string]: {metas: Array<{phx_ref: string, name: string, t: float}>}
+ },
+ "ref": null
+}
+```
+
+### Diff Update
+
+After a change to the presence state, such as a client joining or leaving, the server sends a presence_diff message to update the client's view of the presence state. The payload field contains two keys, `joins` and `leaves`, which represent clients that have joined and left, respectively. The values associated with each key are UUIDs of the clients.
+
+```ts
+{
+ "event": "presence_diff",
+ "topic": string,
+ "payload": {
+ "joins": {metas: Array<{phx_ref: string, name: string, t: float}>},
+ "leaves": {metas: Array<{phx_ref: string, name: string, t: float}>}
+ },
+ "ref": null
+}
+```
+
+export const Page = ({ children }) =>
+export default Page
diff --git a/apps/docs/pages/guides/realtime/realtime-with-nextjs.mdx b/apps/docs/pages/guides/realtime/realtime-with-nextjs.mdx
new file mode 100644
index 00000000000..e60afb4f74a
--- /dev/null
+++ b/apps/docs/pages/guides/realtime/realtime-with-nextjs.mdx
@@ -0,0 +1,24 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'realtime-with-nextjs',
+ title: 'Using Realtime with Next.js',
+ description: 'Client & Server Components in Next.js with Realtime Updates',
+ sidebar_label: 'Videos',
+}
+
+In this guide we explore the best ways to receive realtime Postgres changes with your Next.js application.
+We'll show both client and serverside updates, and explore the which option is best.
+
+
+
+
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/pages/guides/realtime/subscribing-to-database-changes.mdx b/apps/docs/pages/guides/realtime/subscribing-to-database-changes.mdx
new file mode 100644
index 00000000000..f22764ac7c8
--- /dev/null
+++ b/apps/docs/pages/guides/realtime/subscribing-to-database-changes.mdx
@@ -0,0 +1,80 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'subscribing-to-database-changes',
+ title: 'Subscribing to Database Changes',
+ description: 'Listen to database changes in real-time from your website or application.',
+ sidebar_label: 'Videos',
+}
+
+Supabase allows you to subscribe to real-time changes on your database from your client application.
+
+You can listen to database changes using the [Postgres Changes](/docs/guides/realtime/postgres-changes) extension.
+
+## Demo
+
+
+
+
+
+## Streaming inserts
+
+You can use the `INSERT` event to stream all new rows.
+
+```js
+const { createClient } = require('@supabase/supabase-js')
+
+const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
+
+const channel = supabase
+ .channel('schema-db-changes')
+ .on(
+ 'postgres_changes',
+ {
+ event: 'INSERT',
+ schema: 'public',
+ },
+ (payload) => console.log(payload)
+ )
+ .subscribe()
+```
+
+## Streaming updates
+
+You can use the `UPDATE` event to stream all updated rows.
+
+```js
+const { createClient } = require('@supabase/supabase-js')
+
+const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
+
+const channel = supabase
+ .channel('schema-db-changes')
+ .on(
+ 'postgres_changes',
+ {
+ event: 'UPDATE',
+ schema: 'public',
+ },
+ (payload) => console.log(payload)
+ )
+ .subscribe()
+```
+
+## More resources
+
+- Learn more about the [Postgres Changes](/docs/guides/realtime/postgres-changes) extension.
+- Client Libraries:
+ - [JavaScript](/docs/reference/javascript/subscribe)
+ - [Flutter](/docs/reference/dart/stream)
+ - [Python](/docs/reference/python/subscribe)
+ - [C#](/docs/reference/csharp/subscribe)
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/public/img/guides/realtime/realtime-arch.png b/apps/docs/public/img/guides/realtime/realtime-arch.png
new file mode 100644
index 00000000000..fca216142b1
Binary files /dev/null and b/apps/docs/public/img/guides/realtime/realtime-arch.png differ
diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx
index bb359df65a1..013b34c1752 100644
--- a/apps/www/pages/pricing/index.tsx
+++ b/apps/www/pages/pricing/index.tsx
@@ -449,7 +449,8 @@ export default function IndexPage() {
The Pro tier has a usage quota included and a spend cap turned on by default. If you
need to go beyond the inclusive limits, simply switch off your spend cap to pay for
- additional usage.
+ additional usage and scale seamlessly. Note that your project will run into
+ restrictions if you have the spend cap enabled and exhaust your quota.
diff --git a/packages/ui/src/components/Toggle/Toggle.tsx b/packages/ui/src/components/Toggle/Toggle.tsx
index 7339c862879..aa2b62c30f1 100644
--- a/packages/ui/src/components/Toggle/Toggle.tsx
+++ b/packages/ui/src/components/Toggle/Toggle.tsx
@@ -9,7 +9,7 @@ interface Props extends Omit, 'size'> {
disabled?: boolean
layout?: 'horizontal' | 'vertical' | 'flex'
error?: string
- descriptionText?: string
+ descriptionText?: string | React.ReactNode
label?: string | React.ReactNode
afterLabel?: string
beforeLabel?: string
diff --git a/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx b/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx
index 387d2616a30..8b458b02882 100644
--- a/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx
+++ b/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx
@@ -34,7 +34,7 @@ export const ForeignKeyFormatter = (props: PropsWithChildren
{value === null ? : value}
- {relationship !== undefined && targetTable !== undefined && (
+ {relationship !== undefined && targetTable !== undefined && value !== null && (
= ({
const isChangingCustomDomains =
currentAddons.customDomains?.id !== selectedAddons.customDomains?.id
+ const togglingOnSpendCap =
+ currentPlan.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PAYG &&
+ selectedPlan &&
+ selectedPlan.metadata.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
+
// If it's enterprise we only only changing of add-ons
const hasChangesToPlan = isEnterprise
? isChangingComputeSize || isChangingPITRDuration
@@ -316,6 +321,16 @@ const PaymentSummaryPanel: FC = ({
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
)}
+
+ {togglingOnSpendCap && (
+ }
+ title="Enabling spend cap"
+ description="Exceeding your plan's quota will result in service restrictions. With the spend cap disabled, you'll be charged for usage beyond the quota."
+ />
+ )}
)}
diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx
index 2745c65dbcc..44637d482b9 100644
--- a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx
+++ b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx
@@ -14,7 +14,8 @@ interface Props {
const PlanCard: FC = ({ plan, currentPlan, onSelectPlan }) => {
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
- const teamTierEnabled = currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM || useFlag('teamTier')
+ const teamTierEnabled =
+ currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM || useFlag('teamTier')
const planMeta = PRICING_META[plan.id]
const isEnterprise = plan.id === 'Enterprise'
@@ -118,8 +119,29 @@ const PlanCard: FC = ({ plan, currentPlan, onSelectPlan }) => {
{!isEnterprise && (
+ You will be charged for usage beyond the plan limits
+
{' '}
+
+ )}
diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
index 025e54ea8a7..c31d72eac83 100644
--- a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
+++ b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
@@ -49,8 +49,6 @@ export const PRICING_META = {
'No project pausing',
'Email support',
],
- additional: 'Need more? Turn off your spend cap to Pay As You Grow ',
- scale: 'Additional fees apply for usage and storage beyond the limits above.',
},
[STRIPE_PRODUCT_IDS.TEAM]: {
new: true,
diff --git a/studio/components/interfaces/Billing/ProUpgrade.tsx b/studio/components/interfaces/Billing/ProUpgrade.tsx
index 33919b8a218..897ee2e0005 100644
--- a/studio/components/interfaces/Billing/ProUpgrade.tsx
+++ b/studio/components/interfaces/Billing/ProUpgrade.tsx
@@ -1,8 +1,7 @@
import { FC, useEffect, useRef, useState } from 'react'
import { Transition } from '@headlessui/react'
import { useRouter } from 'next/router'
-import * as Tooltip from '@radix-ui/react-tooltip'
-import { Button, IconHelpCircle, Toggle, Modal } from 'ui'
+import { IconHelpCircle, Toggle } from 'ui'
import { useStore } from 'hooks'
import { post, patch } from 'lib/common/fetch'
@@ -30,6 +29,7 @@ import {
import BackButton from 'components/ui/BackButton'
import SupportPlan from './AddOns/SupportPlan'
import HCaptcha from '@hcaptcha/react-hcaptcha'
+import SpendCapModal from './SpendCapModal'
// Do not allow compute size changes for af-south-1
@@ -263,7 +263,7 @@ const ProUpgrade: FC = ({
-
Enable spend cap
+
Spend cap
= ({
/>
- If enabled, additional resources will not be charged on a per-usage basis
+ By default, Pro projects have a spend cap enabled to control costs and prevent
+ exceeding limits. This restricts usage upon exhausting the quota. To scale
+ without restrictions, disable the spend cap and be charged for overusage beyond
+ the quota.
- A spend cap allows you to restrict your project's resource usage within the limits
- of the Pro tier. Disabling the spend cap will then remove those limits and any
- additional resources consumed beyond the Pro tier limits will be charged on a
- per-usage basis
-
-
- The table below shows an overview of which resources are chargeable, and how they
- are charged:
-
- {/* Maybe instead of a table, show something more interactive like a spend cap playground */}
- {/* Maybe ideate this in Figma first but this is good enough for now */}
-
-
-
Item
-
Limit
-
Rate
-
-
-
-
Database size
-
8GB
-
$0.125/GB
-
-
-
Data transfer
-
50GB
-
$0.09/GB
-
-
-
-
-
-
Auth MAUs
-
-
-
-
-
-
-
-
- Monthly Active Users: A user that has made an API request in the last
- month
-
-
-
-
-
-
100,000
-
$0.00325/user
-
-
-
-
-
Storage size
-
100GB
-
$0.021/GB
-
-
-
Data Transfer
-
50GB
-
$0.09/GB
-
-
-
-
-
-
-
-
-
-
-
-
-
+ onHide={() => setShowSpendCapHelperModal(false)}
+ />
>
)
}
diff --git a/studio/components/interfaces/Billing/SpendCapModal.tsx b/studio/components/interfaces/Billing/SpendCapModal.tsx
new file mode 100644
index 00000000000..1b809b51ddb
--- /dev/null
+++ b/studio/components/interfaces/Billing/SpendCapModal.tsx
@@ -0,0 +1,149 @@
+import { FC } from 'react'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { Button, IconHelpCircle, Modal } from 'ui'
+import Link from 'next/link'
+
+interface Props {
+ visible: boolean
+ onHide: () => void
+}
+
+const SpendCapModal: FC = ({ visible, onHide }) => {
+ return (
+ onHide()}>
+
+
+
+
+ Enabling the spend cap sets a limit on your usage to stay within your plan's quota,
+ which controls costs but can limit service. Disabling the spend cap removes these
+ limits, but any extra usage beyond the plan's limit will be charged per usage.
+
+
+ Take a look at the following table to see which resources are chargeable and how they
+ are charged:
+
+ {/* Maybe instead of a table, show something more interactive like a spend cap playground */}
+ {/* Maybe ideate this in Figma first but this is good enough for now */}
+
+
+
Item
+
Limit
+
Rate
+
+
+
+
Database size
+
8GB
+
$0.125/GB
+
+
+
Database egress
+
50GB
+
$0.09/GB
+
+
+
+
+
+
Auth MAUs
+
+
+
+
+
+
+
+
+ Monthly Active Users: A user that has made an API request in the last
+ month
+
+
+ Your project is currently on the Free tier - upgrade to the Pro tier for
+ a greatly increased quota and continue to scale.
+
+
+ See{' '}
+
+ pricing page
+ {' '}
+ for a full breakdown of available plans.
+
+
+
+
+
+ ) : (
+
+
+ By default, Pro projects have spend caps to control costs. When enabled,
+ usage is limited to the plan's quota, with restrictions when limits are
+ exceeded. To scale beyond Pro limits without restrictions, disable the
+ spend cap and pay for over-usage beyond the quota.
+
+ By default, Pro projects have spend caps to control costs. When enabled,
+ usage is limited to the plan's quota, with restrictions when limits are
+ exceeded. To scale beyond Pro limits without restrictions, disable the
+ spend cap and pay for over-usage beyond the quota.
+