- )
-}
-
-export default GuidesTableOfContents
-export type { TOCHeader }
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index 4cea890356d..0ca56c33284 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -1,8 +1,9 @@
-import type { ComponentProps } from 'react'
// End of third-party imports
import { isFeatureEnabled } from 'common/enabled-features'
+import type { ComponentProps } from 'react'
import type { IconPanel } from 'ui-patterns/IconPanel'
+
import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types'
const {
@@ -2849,10 +2850,19 @@ export const self_hosting: NavMenuConstant = {
name: 'How-to Guides',
items: [
{ name: 'Self-Hosted Functions', url: '/guides/self-hosting/self-hosted-functions' },
- { name: 'Restore from Platform', url: '/guides/self-hosting/restore-from-platform' },
+ {
+ name: 'Add Reverse Proxy with HTTPS',
+ url: '/guides/self-hosting/self-hosted-proxy-https',
+ },
+ {
+ name: 'Restore Project from Platform',
+ url: '/guides/self-hosting/restore-from-platform',
+ },
{ name: 'Configure S3 Storage', url: '/guides/self-hosting/self-hosted-s3' },
{ name: 'Copy Storage from Platform', url: '/guides/self-hosting/copy-from-platform-s3' },
- { name: 'Enabling MCP server', url: '/guides/self-hosting/enable-mcp' },
+ { name: 'Configure Social Login (OAuth)', url: '/guides/self-hosting/self-hosted-oauth' },
+ { name: 'Configure Phone Login & MFA', url: '/guides/self-hosting/self-hosted-phone-mfa' },
+ { name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' },
],
},
{
diff --git a/apps/docs/content/guides/api/rest/auto-generated-docs.mdx b/apps/docs/content/guides/api/rest/auto-generated-docs.mdx
index 7357392b588..dce84addd9c 100644
--- a/apps/docs/content/guides/api/rest/auto-generated-docs.mdx
+++ b/apps/docs/content/guides/api/rest/auto-generated-docs.mdx
@@ -6,13 +6,15 @@ description: 'Supabase provides documentation that updates automatically.'
Supabase generates documentation in the [Dashboard](/dashboard) which updates as you make database changes.
-1. Go to the [API](/dashboard/project/_/api) page in the Dashboard.
-2. Select any table under **Tables and Views** in the sidebar.
-3. Switch between the JavaScript and the cURL docs using the tabs.
+1. Go to the [Project Settings](/dashboard/project/_/settings/general) page in the Dashboard.
+2. Select Data API -> Docs
+3. Select any table under **Tables and Views** in the sidebar.
+4. Switch between the JavaScript and the cURL docs using the tabs.
+5. You may also select the SUPABASE_KEY to use.
diff --git a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx
index b65a671d198..0efdc4068e2 100644
--- a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx
+++ b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx
@@ -495,11 +495,11 @@ Store the client secret securely. It will only be shown once. If you lose it, yo
When a client exchanges an authorization code or refreshes a token, it must authenticate with the token endpoint. The `token_endpoint_auth_method` controls how this authentication happens:
-| Method | Description | Used by |
-| --- | --- | --- |
-| `none` | No client authentication. Only `client_id` is sent in the request body. | Public clients (required) |
-| `client_secret_basic` | Client credentials sent via HTTP Basic auth (`Authorization: Basic `). **This is the default for confidential clients.** | Confidential clients |
-| `client_secret_post` | Client credentials sent in the request body (`client_id` and `client_secret` as form parameters). | Confidential clients |
+| Method | Description | Used by |
+| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
+| `none` | No client authentication. Only `client_id` is sent in the request body. | Public clients (required) |
+| `client_secret_basic` | Client credentials sent via HTTP Basic auth (`Authorization: Basic `). **This is the default for confidential clients.** | Confidential clients |
+| `client_secret_post` | Client credentials sent in the request body (`client_id` and `client_secret` as form parameters). | Confidential clients |
**Defaults:** Public clients default to `none`. Confidential clients default to `client_secret_basic` (per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591#section-2)).
diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx
index b02a37ad8fd..3ba089193d4 100644
--- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx
+++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx
@@ -463,8 +463,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
return parseCookieHeader(request.headers.get('Cookie') ?? '')
},
setAll(cookiesToSet) {
- cookiesToSet.forEach(({ name, value }) =>
- headers.append('Set-Cookie', serializeCookieHeader(name, value))
+ cookiesToSet.forEach(({ name, value, options }) =>
+ headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
)
},
},
@@ -497,8 +497,8 @@ export async function action({ request }: ActionFunctionArgs) {
return parseCookieHeader(request.headers.get('Cookie') ?? '')
},
setAll(cookiesToSet) {
- cookiesToSet.forEach(({ name, value }) =>
- headers.append('Set-Cookie', serializeCookieHeader(name, value))
+ cookiesToSet.forEach(({ name, value, options }) =>
+ headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
)
},
},
diff --git a/apps/docs/content/guides/auth/sessions.mdx b/apps/docs/content/guides/auth/sessions.mdx
index 1b79d9f8d66..c7eac35d2bf 100644
--- a/apps/docs/content/guides/auth/sessions.mdx
+++ b/apps/docs/content/guides/auth/sessions.mdx
@@ -69,7 +69,7 @@ Otherwise sessions are progressively deleted from the database 24 hours after th
### What are recommended values for access token (JWT) expiration?
-Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/settings/jwt) in the Advanced Settings section.
+Most applications should use the default expiration time of 1 hour. This can be customized in your [project's settings](/dashboard/project/_/settings/jwt/legacy) in the JWT Keys > Legacy JWT Secret section.
Setting a value over 1 hour is generally discouraged for security reasons, but it may make sense in certain situations.
diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx
index 55b43ec5041..a08c9585ebf 100644
--- a/apps/docs/content/guides/platform/backups.mdx
+++ b/apps/docs/content/guides/platform/backups.mdx
@@ -1,75 +1,39 @@
---
title: 'Database Backups'
-description: 'Learn about the available backup methods for your Supabase project.'
+description: 'Learn about backups for your Supabase project.'
---
-Database backups are an integral part of any disaster recovery plan. Disasters come in many shapes and sizes. It could be as simple as accidentally deleting a table column, the database crashing, or even a natural calamity wiping out the underlying hardware a database is running on. The risks and impact brought by these scenarios can never be fully eliminated, but only minimized or even mitigated. Having database backups is a form of insurance policy. They are essentially snapshots of the database at various points in time. When disaster strikes, database backups allow the project to be brought back to any of these points in time, therefore averting the crisis.
+We automatically back up all Free, Pro, Team, and Enterprise Plan projects on a daily basis. You can find backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard.
-
+Pro Plan projects can access the last 7 days of daily backups. Team Plan projects can access the last 14 days of daily backups, while Enterprise Plan projects can access up to 30 days of daily backups. If you need more frequent backups, consider enabling [Point-in-Time Recovery](#point-in-time-recovery). We recommend that free tier plan projects regularly export their data using the [Supabase CLI `db dump` command](/docs/reference/cli/supabase-db-dump) and maintain off-site backups.
-The Supabase team regularly monitors the status of backups. In case of any issues, you can [contact support](/dashboard/support/new). Also you can check out our [status page](https://status.supabase.com/) at any time.
+
+
+When you delete a project, we permanently remove all associated data, including any backups stored in S3. This action is irreversible, so consider it carefully before proceeding.
+
+
+
+
+
+For security purposes, daily backups do not store passwords for custom roles, and you will not find them in downloadable files. If you restore from a daily backup and use custom roles, you will need to reset their passwords after the restoration completes.
-Once a project is deleted all associated data will be permanently removed, including any backups stored in S3. This action is irreversible and should be carefully considered before proceeding.
+Database backups do not include objects you store via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects you deleted after that backup.
-## Types of backups
+## Backup and restore process
-Database backups can be categorized into two types: **logical** and **physical**. You can learn more about them [here](/blog/postgresql-physical-logical-backups).
+You can access daily backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard and restore a project to any of the backups.
-
+You can restore your project to any of the backups. To generate a logical backup yourself, use the [Supabase CLI `db dump` command](/docs/reference/cli/supabase-db-dump).
-To enable physical backups, you have three options:
+## Managing backups programmatically
-- Enable [Point-in-Time Recovery (PITR)](#point-in-time-recovery)
-- [Increase your database size](/docs/guides/platform/database-size) to greater than 15GB
-- [Create a read replica](/docs/guides/platform/read-replicas)
-
-Once a project satisfies at least one of the requirements for physical backups then logical backups are no longer made. However, your project may revert back to logical backups if you remove add-ons.
-
-
-
-You can confirm your project's backup type by navigating to [**Database Backups > Scheduled backups**](/dashboard/project/_/database/backups/scheduled) and if you can download a backup then it is logical, otherwise it is physical.
-
-However, if your project has the Point-in-Time Recovery (PITR) add-on then the backups are physical and you can view them in [Database Backups > Point in time](/dashboard/project/_/database/backups/pitr).
-
-## 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 (PITR). The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
-
-
-
-If you enable PITR, Daily Backups will no longer be taken. PITR provides a finer granularity than Daily Backups, so it's unnecessary to run both.
-
-
-
-
-
-Database backups do not include objects stored via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects that have been deleted since then.
-
-
-
-## Daily backups
-
-All Pro, Team and Enterprise Plan Supabase projects are backed up automatically on a daily basis. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data if disaster hits at the most inopportune time. If a lower RPO is required, enabling Point-in-Time Recovery should be considered.
-
-
-
-For security purposes, passwords for custom roles are not stored in daily backups, and will not be found in downloadable files. As such, if you are restoring from a daily backup and are using custom roles, you will need to set their passwords once more following a completed restoration.
-
-
-
-### Backup process [#daily-backups-process]
-
-The Postgres utility [pg_dumpall](https://www.postgresql.org/docs/current/app-pg-dumpall.html) is used to perform daily backups. An SQL file is generated, zipped up, and sent to our storage servers for safe keeping.
-
-You can access daily backups in the [Scheduled backups](/dashboard/project/_/database/backups/scheduled) settings in the Dashboard. Pro Plan projects can access the last 7 days' worth of daily backups. Team Plan projects can access the last 14 days' worth of daily backups, while Enterprise Plan projects can access up to 30 days' worth of daily backups. Users can restore their project to any one of the backups. If you wish to generate a logical backup on your own, you can do so through the [Supabase CLI](/docs/reference/cli/supabase-db-dump).
-
-You can also manage backups programmatically using the Management API:
+You can also manage backups programmatically [using the Management API](/docs/reference/api/v1-list-all-backups):
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
@@ -80,7 +44,7 @@ export PROJECT_REF="your-project-ref"
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups"
-# Restore from a PITR (not logical) backup (replace ISO timestamp with desired restore point)
+# Restore from a PITR backup (replace Unix timestamp with desired restore point)
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups/restore-pitr" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
@@ -89,126 +53,87 @@ curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups
}'
```
-#### Backup process for large databases
+### Restoration process
-Databases larger than 15GB[^1], if they're on a recent build[^2] of the Supabase platform, get automatically transitioned[^3] to use daily physical backups. Physical backups are a more performant backup mechanism that lowers the overhead and impact on the database being backed up, and also avoids holding locks on objects in your database for a long period of time. While restores are unaffected, the backups created using this method cannot be downloaded from the Backups section of the dashboard.
+When selecting a backup to restore to, choose the closest available backup made before your desired restore point. You can always choose earlier backups, but consider how many days of data you might lose.
-This class of physical backups only allows for recovery to a fixed time each day, similar to daily backups. You can upgrade to [PITR](#point-in-time-recovery) for access to more granular recovery options.
+The Dashboard prompts you for confirmation before proceeding with the restoration. The project is inaccessible during this process, so plan for downtime beforehand. Downtime depends on the size of the database—the larger it is, the longer the downtime will be.
-Once a database is transitioned to using physical backups, it continues to use physical backups, even if the database size falls back below the threshold for the transition.
+After you confirm, we trigger the process to restore the desired backup data to your project. The dashboard will display a notification once the restoration completes.
-[^1]: The threshold for transitioning will be slowly lowered over time. Eventually, all projects will be transitioned to using physical backups.
-[^2]: Projects created or upgraded after the 14th of July 2022 are eligible.
-[^3]: The transition to physical backups is handled transparently and does not require any user intervention. It involves a single restart of the database to pick up new configuration that can only be loaded at start; the expected downtime for the restart is a few seconds.
-
-### Restoration process [#daily-backups-restoration-process]
-
-When selecting a backup to restore to, select the closest available one made before the desired point in time to restore to. Earlier backups can always be chosen too but do consider the number of days' worth of data that could be lost.
-
-The Dashboard will then prompt for a confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the underlying SQL of the chosen backup is then run against the project. The Postgres utility [psql](https://www.postgresql.org/docs/current/app-psql.html) is used to facilitate the restoration. The Dashboard will display a notification once the restoration completes.
-
-If your project is using subscriptions or replication slots, you will need to drop them prior to the restoration, and re-create them afterwards. The slot used by Realtime is exempted from this, and will be handled automatically.
+If your project uses subscriptions or replication slots, you need to drop them before the restoration and re-create them afterwards. We exempt the slot used by Realtime from this requirement and handle it automatically.
{/* screenshot of the Dashboard of the project completing restoration */}
## Point-in-Time recovery
-Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. Even with daily backups, a day's worth of data could still be lost. With PITR, backups could be performed up to the point of disaster.
+Point-in-Time Recovery (PITR) allows you to back up a project at shorter intervals, giving you the option to restore to any chosen point with up to seconds of granularity. Even with daily backups, you could still lose a day's worth of data. With PITR, you can back up to the point of disaster.
Pro, Team and Enterprise Plan projects can enable PITR as an add-on.
-Projects interested in PITR will also need to use at least a Small compute add-on, in order to ensure smooth functioning.
+Projects that want to use PITR must also use at least a Small compute add-on to ensure smooth functioning.
+
+
+
+
+ As [covered in this blog post](/blog/postgresql-physical-logical-backups), a combination of physical backups and [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) file archiving makes PITR possible. Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change the database processes.
+
+ We use [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, to handle both aspects of PITR. Daily, we take a snapshot of the database and send it to our storage servers. Throughout the day, as database transactions occur, we generate and upload WAL files.
+
+ By default, we back up WAL files at two-minute intervals. If these files exceed a certain file size threshold, we back them up immediately. During periods of high transaction volume, WAL file backups therefore become more frequent. Conversely, when the database has no activity, we do not make WAL file backups. Overall, in the worst case scenario, PITR achieves a Recovery Point Objective (RPO) of two minutes.
+
+
+
+
+
+
-If you enable PITR, Daily Backups will no longer be taken. PITR provides a finer granularity than Daily Backups, so it's unnecessary to run both.
+If you enable PITR, we will no longer take Daily Backups. PITR provides finer granularity than Daily Backups, so running both is unnecessary.
-When you disable PITR, all new backups will still be taken as physical backups only. Physical backups can still be used for restoration, but they are not available for direct download. If you need to download a backup after PITR is disabled, you’ll need to take a manual [logical backup using the Supabase CLI or pg_dump](/docs/guides/platform/migrating-within-supabase/backup-restore#backup-database-using-the-cli).
-
-
-
-If PITR has been disabled, logical backups remain available until they pass the backup retention period for your plan. After that window passes, only physical backups will be shown.
-
-
-
-### Backup process [#pitr-backup-process]
-
-As discussed [here](/blog/postgresql-physical-logical-backups), PITR is made possible by a combination of taking physical backups of a project, as well as archiving [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) files. Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change made in the database.
-
-Supabase uses [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, to handle both aspects of PITR. On a daily basis, a snapshot of the database is taken and sent to our storage servers. Throughout the day, as database transactions occur, WAL files are generated and uploaded.
-
-By default, WAL files are backed up at two minute intervals. If these files cross a certain file size threshold, they are backed up immediately. As such, during periods of high amount of transactions, WAL file backups become more frequent. Conversely, when there is no activity in the database, WAL file backups are not made. Overall, this would mean that at the worst case scenario or disaster, the PITR achieves a Recovery Point Objective (RPO) of two minutes.
+### Backup process

-You can access PITR in the [Point in Time](/dashboard/project/_/database/backups/pitr) settings in the Dashboard. The recovery period of a project is indicated by the earliest and latest points of recoveries displayed in your preferred timezone. If need be, the maximum amount of this recovery period can be modified accordingly.
+You can access PITR in the [Point in Time](/dashboard/project/_/database/backups/pitr) settings in the Dashboard. The recovery period of a project is shown by the earliest and latest recovery points displayed in your preferred timezone. You can change the maximum recovery period if needed.
-Note that the latest restore point of the project could be significantly far from the current time. This occurs when there has not been any recent activity in the database, and therefore no WAL file backups have been made recently. This is perfectly fine as the state of the database at the latest point of recovery would still be indicative of the state of the database at the current time given that no transactions have been made in between.
+The latest restore point of the project could be significantly behind the current time. This occurs when the database has had no recent activity, and therefore we have not made any recent WAL file backups. However, the state of the database at the latest recovery point still reflects the current state of the database, given that no transactions have occurred in between.
-### Restoration process [#pitr-restoration-process]
+### Restoration process

-A date and time picker will be provided upon pressing the `Start a restore` button. The process will only proceed if the selected date and time fall within the earliest and latest points of recoveries.
+A date and time picker appears when you click the **Start a restore** button. The process only proceeds if the selected date and time fall within the earliest and latest recovery points.

-After locking in the desired point in time to recover to, The Dashboard will then prompt for a review and confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot for downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the latest physical backup available is downloaded to the project and the database is partially restored. WAL files generated after this physical backup up to the specified point-in-time are then downloaded. The underlying records of transactions in these files are replayed against the database to complete the restoration. The Dashboard will display a notification once the restoration completes.
+After selecting your desired recovery point, the Dashboard prompts you to review and confirm before proceeding with the restoration. The project is inaccessible during this process, so plan for downtime beforehand. Downtime depends on the size of the database—the larger it is, the longer the downtime will be. After you confirm, we download the latest available physical backup to the project and partially restore the database. We then download the WAL files generated after this physical backup up to your specified point in time. We replay the underlying transaction records in these files against the database to complete the restoration. The Dashboard will display a notification once the restoration completes.
<$Show if="billing:all">
<$Partial path="billing/pricing/pricing_pitr.mdx" />
$Show>
+### Downloading backups after disabling PITR
+
+When you disable PITR, we still take all new backups as physical backups only. You can still use physical backups for restoration, but they are not available for direct download. If you need to download a backup after disabling PITR, you need to take a manual [legacy logical backup using the Supabase CLI or pg_dump](/docs/guides/platform/migrating-within-supabase/backup-restore#backup-database-using-the-cli).
+
## Restore to a new project
See the [Duplicate Project docs](/docs/guides/platform/clone-project).
-
-## Troubleshooting
-
-### Logical backups
-
-#### `search_path` issues
-
-During the `pg_restore` process, the `search_path` is set to an empty string for predictability, and security. Using unqualified references to functions or relations can cause restorations using logical backups to fail, as the database will not be able to locate the function or relation being referenced. This can happen even if the database functions without issues during normal operations, as the `search_path` is usually set to include several schemas during normal operations. Therefore, you should always use schema-qualified names within your SQL code.
-
-You can refer to [an example PR](https://github.com/supabase/supabase/pull/28393/files) on how to update SQL code to use schema-qualified names.
-
-#### Invalid check constraints
-
-Postgres requires that [check constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS) be:
-
-1. immutable
-1. not reference table data other than the new or updated row being checked
-
-Violating these requirements can result in numerous failure scenarios, including during logical restorations.
-
-Common examples of check constraints that can result in such failures are:
-
-- validating against the current time, e.g. that the row being inserted references a future event
-- validating the contents of a row against the contents of another table
-
-#### Views that reference themselves
-
-Views that directly or indirectly reference themselves will cause logical restores to fail due to cyclic dependency errors. These views are also invalid and unusable in Postgres, and any query against them will result in a runtime error.
-
-**Example:**
-
-```
--- Direct self-reference
-CREATE VIEW my_view AS
- SELECT * FROM my_view;
-
--- Indirect circular reference
-CREATE VIEW v1 AS SELECT * FROM v2;
-CREATE VIEW v2 AS SELECT * FROM v1;
-```
-
--- Drop the offending view from your database, or delete them from the logical backup to make it restorable.
-
-Postgres documentation [views](https://www.postgresql.org/docs/current/sql-createview.html)
diff --git a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx
index dac00f80c61..fef7ebb55fc 100644
--- a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx
+++ b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx
@@ -36,7 +36,7 @@ Usage is shown as "Log Drain Hours" on your invoice.
### Pricing
-Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs per hour ( per month).
+Log Drains are available as a project Add-On for all Pro, Team and Enterprise users. Each Log Drain costs per hour ( per month).
## Log Drain Events
diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx
index 4bdb5850eab..26ad20d965d 100644
--- a/apps/docs/content/guides/realtime/authorization.mdx
+++ b/apps/docs/content/guides/realtime/authorization.mdx
@@ -133,7 +133,7 @@ exists (
rooms_users
where
user_id = (select auth.uid())
- and topic = (select realtime.topic())
+ and room_topic = (select realtime.topic())
and realtime.messages.extension in ('broadcast')
)
);
@@ -266,7 +266,7 @@ with check (
rooms_users
where
user_id = (select auth.uid())
- and topic = (select realtime.topic())
+ and room_topic = (select realtime.topic())
and realtime.messages.extension in ('broadcast')
)
);
@@ -293,7 +293,7 @@ using (
rooms_users
where
user_id = (select auth.uid())
- and topic = (select realtime.topic())
+ and room_topic = (select realtime.topic())
and realtime.messages.extension in ('presence')
)
);
@@ -316,7 +316,7 @@ with check (
rooms_users
where
user_id = (select auth.uid())
- and name = (select realtime.topic())
+ and room_topic = (select realtime.topic())
and realtime.messages.extension in ('presence')
)
);
@@ -343,7 +343,7 @@ using (
rooms_users
where
user_id = (select auth.uid())
- and topic = (select realtime.topic())
+ and room_topic = (select realtime.topic())
and realtime.messages.extension in ('broadcast', 'presence')
)
);
@@ -366,7 +366,7 @@ with check (
rooms_users
where
user_id = (select auth.uid())
- and name = (select realtime.topic())
+ and room_topic = (select realtime.topic())
and realtime.messages.extension in ('broadcast', 'presence')
)
);
diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx
index 74146dcdc8d..a196650d77b 100644
--- a/apps/docs/content/guides/self-hosting/docker.mdx
+++ b/apps/docs/content/guides/self-hosting/docker.mdx
@@ -428,6 +428,20 @@ By default all files are stored locally on the server. You can connect Storage t
See the [Configure S3 Storage](/docs/guides/self-hosting/self-hosted-s3) guide for detailed setup instructions.
+#### Configuring HTTPS
+
+By default, Supabase is accessible over HTTP. For production deployments, especially when using OAuth providers, you need HTTPS with a valid TLS certificate. The recommended approach is to place a reverse proxy (such as Caddy or Nginx) in front of Kong.
+
+See the [Configure HTTPS](/docs/guides/self-hosting/self-hosted-proxy-https) guide for setup instructions.
+
+#### Configuring social login (OAuth) providers
+
+See the [Configure Social Login (OAuth) Providers](/docs/guides/self-hosting/self-hosted-oauth) guide for setup instructions.
+
+#### Configuring phone login, SMS, and MFA
+
+See the [Configure Phone Login & MFA](/docs/guides/self-hosting/self-hosted-phone-mfa) guide for SMS provider setup, OTP settings, and multi-factor authentication configuration.
+
#### Configuring Supabase AI Assistant
Configuring the Supabase AI Assistant is optional. By adding **your own** `OPENAI_API_KEY` to `.env` you can enable AI services, which help with writing SQL queries, statements, and policies.
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-functions.mdx b/apps/docs/content/guides/self-hosting/self-hosted-functions.mdx
index f8707dc48ac..02eaefaa097 100644
--- a/apps/docs/content/guides/self-hosting/self-hosted-functions.mdx
+++ b/apps/docs/content/guides/self-hosting/self-hosted-functions.mdx
@@ -125,14 +125,14 @@ const customVar = Deno.env.get('MY_CUSTOM_VAR')
The functions service is pre-configured with the following environment variables:
-| Variable | Value | Purpose |
-| --- | --- | --- |
-| `SUPABASE_URL` | `http://kong:8000` | Internal API gateway URL |
-| `SUPABASE_PUBLIC_URL` | `http://:8000` | Base URL for accessing Supabase from the Internet |
-| `JWT_SECRET` | Your secret key | Legacy symmetric encryption key used to sign and verify JWTs |
-| `SUPABASE_ANON_KEY` | Your anon key | Client-side API key with limited permissions (`anon` role). |
-| `SUPABASE_SERVICE_ROLE_KEY` | Your service role key | Server-side API key with full database access (`service_role` role) |
-| `SUPABASE_DB_URL` | Postgres connection string | Can be used for direct database access |
+| Variable | Value | Purpose |
+| --------------------------- | --------------------------- | ------------------------------------------------------------------- |
+| `SUPABASE_URL` | `http://kong:8000` | Internal API gateway URL |
+| `SUPABASE_PUBLIC_URL` | `http://:8000` | Base URL for accessing Supabase from the Internet |
+| `JWT_SECRET` | Your secret key | Legacy symmetric encryption key used to sign and verify JWTs |
+| `SUPABASE_ANON_KEY` | Your anon key | Client-side API key with limited permissions (`anon` role). |
+| `SUPABASE_SERVICE_ROLE_KEY` | Your service role key | Server-side API key with full database access (`service_role` role) |
+| `SUPABASE_DB_URL` | Postgres connection string | Can be used for direct database access |
Here's an example function that queries a table using `@supabase/supabase-js`:
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-oauth.mdx b/apps/docs/content/guides/self-hosting/self-hosted-oauth.mdx
new file mode 100644
index 00000000000..4297d06127c
--- /dev/null
+++ b/apps/docs/content/guides/self-hosting/self-hosted-oauth.mdx
@@ -0,0 +1,475 @@
+---
+title: 'Configure Social Login (OAuth) Providers'
+description: 'Set up social login (OAuth/OIDC) providers for self-hosted Supabase with Docker.'
+subtitle: 'Set up social login (OAuth/OIDC) providers for self-hosted Supabase with Docker.'
+---
+
+This guide covers the **server-side configuration** required to enable social login providers on a self-hosted Supabase instance running with Docker Compose. This applies to all OAuth and OIDC-based providers, including third-party identity providers like Keycloak.
+
+## Before you begin
+
+You need:
+
+- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
+- `API_EXTERNAL_URL` set to the publicly reachable URL of your Supabase instance (e.g., `https://`).
+
+
+
+HTTPS is strongly recommended in production. Most OAuth providers reject `http://` callback URLs (except `localhost`).
+
+
+
+Your **OAuth callback URL** is built from `API_EXTERNAL_URL`. For example, if `API_EXTERNAL_URL` is `https://`, the callback URL will become:
+
+```
+https:///auth/v1/callback
+```
+
+You will have to register this URL with each OAuth provider.
+
+## OAuth request flow
+
+When a user signs in with an OAuth provider, the following flow occurs:
+
+1. Your app calls `supabase.auth.signInWithOAuth()` and the browser redirects to the Auth service
+2. API gateway (Kong) routes the request to the Auth container (`/auth/v1/authorize`)
+3. Auth redirects the user to the OAuth provider (e.g., Google) for consent
+4. The provider redirects back to `https:///auth/v1/callback`
+5. Auth exchanges the authorization code for tokens and redirects the user to your `SITE_URL` or an allowed redirect URL
+
+## Auth environment variables
+
+The Auth service (GoTrue) uses the prefix `GOTRUE_EXTERNAL_` followed by a provider name for all OAuth configuration. For example, when using Google:
+
+- `GOTRUE_EXTERNAL_GOOGLE_ENABLED`
+- `GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID`
+- `GOTRUE_EXTERNAL_GOOGLE_SECRET`
+- `GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI`
+
+## Step-by-step configuration
+
+The default `.env.example` and `docker-compose.yml` include commented-out placeholders for Google, GitHub, and Azure.
+
+### Step 1: Register your app with the provider
+
+1. Go to the OAuth provider's developer console and create an application.
+2. Set the **authorized redirect URL**, e.g., `https:///auth/v1/callback`
+3. Copy the **client ID** and **client secret** into your `.env` file.
+
+### Step 2: Configure variables in the `.env` file
+
+Uncomment the lines for your provider in `.env` and add your client ID and secret, e.g., for Google:
+
+```
+GOOGLE_ENABLED=true
+GOOGLE_CLIENT_ID=your-client-id
+GOOGLE_SECRET=your-client-secret
+```
+
+### Step 3: Enable the matching lines in `docker-compose.yml`
+
+Uncomment the corresponding `GOTRUE_EXTERNAL_` lines in the `auth` service's `environment`:
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
+ GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
+ GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
+ GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+For providers **not** pre-configured in the files (see the [full provider list](#other-supported-providers) below), add the lines manually following the same pattern: variables in `.env`, passthrough with `GOTRUE_EXTERNAL_PROVIDER_` in `docker-compose.yml`.
+
+
+
+### Step 4: Restart the auth service
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### Step 5: Verify the configuration
+
+Check that the provider is enabled:
+
+```sh
+curl -H 'apikey: your-anon-key' https:///auth/v1/settings
+```
+
+The response should include your provider under `external`:
+
+```
+{
+ "external": {
+ "google": true
+ }
+}
+```
+
+## Provider-specific setup
+
+
+
+
+
+**Google Cloud Console setup:**
+
+1. Go to [Google Cloud Console](https://console.cloud.google.com/)
+2. Create or select a project
+3. Select **Solutions** > **All products** in the navigation menu on the left
+4. Go to **APIs & services** > **OAuth consent screen** and click **Get started**
+5. Follow the configuration steps and add an **External** app
+6. Go to **APIs & Services** > **Credentials**
+7. Click **Create Credentials** > **OAuth client ID**
+8. Set application type to **Web application**
+9. Under **Authorized redirect URIs**, add: `https:///auth/v1/callback`
+10. Click **Create** and copy the client ID and client secret
+
+**`.env`:**
+
+```
+GOOGLE_ENABLED=true
+GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
+GOOGLE_SECRET=your-google-client-secret
+```
+
+**`docker-compose.yml`:**
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
+ GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
+ GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
+ GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+
+
+**GitHub setup:**
+
+1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
+2. Click **New OAuth app**
+3. Fill in **Homepage URL**, e.g., `https://`
+4. Set **Authorization callback URL** to: `https:///auth/v1/callback`
+5. Click **Register application**
+6. Copy the client ID, generate and copy a client secret
+
+**`.env`:**
+
+```
+GITHUB_ENABLED=true
+GITHUB_CLIENT_ID=your-github-client-id
+GITHUB_SECRET=your-github-client-secret
+```
+
+**`docker-compose.yml`:**
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
+ GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
+ GOTRUE_EXTERNAL_GITHUB_SECRET: ${GITHUB_SECRET}
+ GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+
+
+**Azure Portal setup:**
+
+1. Go to [Azure Portal](https://portal.azure.com/)
+2. Go to **All services** > **Identity** > **App registrations** via the navigation menu on the left
+3. Click **New registration**
+4. Add application **Name**
+5. Under **Redirect URI**, select **Web** and enter: `https:///auth/v1/callback`
+6. Click **Register**
+7. Copy the **Application (client) ID**
+8. Click on **Client credentials** > **Add a certificate or secret**
+9. Click on **New client secret** and add a client secret
+10. Copy the secret value (not "secret ID")
+
+**`.env`:**
+
+```
+AZURE_ENABLED=true
+AZURE_CLIENT_ID=your-azure-application-client-id
+AZURE_SECRET=your-azure-client-secret
+## Optional: restrict to a specific tenant (defaults to 'common')
+# AZURE_URL=https://login.microsoftonline.com/your-tenant-id
+```
+
+**`docker-compose.yml`:**
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED}
+ GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
+ GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
+ GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+ ## Optional: uncomment for tenant-specific Azure login
+ # GOTRUE_EXTERNAL_AZURE_URL: ${AZURE_URL}
+```
+
+
+
+
+
+**Apple Developer setup:**
+
+1. Refer to [Apple Developer documentation](https://developer.apple.com/documentation/signinwithapple/configuring-your-environment-for-sign-in-with-apple) to learn how to enable App ID and create a Services ID
+2. Create a private key for sign in with Apple
+3. Generate a client secret JWT from your private key. See [Apple Developer documentation](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret) for details.
+
+**`.env`:**
+
+```
+APPLE_ENABLED=true
+APPLE_CLIENT_ID=com.example.your-services-id
+APPLE_SECRET=your-generated-jwt-client-secret
+```
+
+**`docker-compose.yml`:**
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_EXTERNAL_APPLE_ENABLED: ${APPLE_ENABLED}
+ GOTRUE_EXTERNAL_APPLE_CLIENT_ID: ${APPLE_CLIENT_ID}
+ GOTRUE_EXTERNAL_APPLE_SECRET: ${APPLE_SECRET}
+ GOTRUE_EXTERNAL_APPLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+Apple uses `response_mode=form_post` for its OAuth flow. The Auth service handles this automatically - no additional configuration is needed.
+
+
+
+
+
+
+
+**Keycloak setup:**
+
+1. Open your Keycloak admin console
+2. Select (or create) the realm you want to use
+3. Go to **Clients** > **Create client**
+4. Set **Client type** to **OpenID Connect**
+5. Set **Client ID** (e.g., `supabase`)
+6. On the next screen, enable **Client authentication**
+7. Under **Valid redirect URIs**, add: `https:///auth/v1/callback`
+8. Save, then go to the **Credentials** tab and copy the **Client secret**
+
+**`.env` variables:**
+
+```
+KEYCLOAK_ENABLED=true
+KEYCLOAK_CLIENT_ID=supabase
+KEYCLOAK_SECRET=your-keycloak-client-secret
+## Required: your Keycloak realm URL
+KEYCLOAK_URL=https://keycloak.example.com/realms/myrealm
+```
+
+**`docker-compose.yml` passthrough:**
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_EXTERNAL_KEYCLOAK_ENABLED: ${KEYCLOAK_ENABLED}
+ GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
+ GOTRUE_EXTERNAL_KEYCLOAK_SECRET: ${KEYCLOAK_SECRET}
+ GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+ GOTRUE_EXTERNAL_KEYCLOAK_URL: ${KEYCLOAK_URL}
+```
+
+
+
+`KEYCLOAK_URL` is **required**. It must be the full realm URL (e.g., `https://keycloak.example.com/realms/myrealm`). The Auth service uses this to discover the OIDC endpoints (`.well-known/openid-configuration`). Without it, Keycloak login will not work.
+
+
+
+
+
+
+
+## Other supported providers
+
+Supabase Auth supports the following OAuth providers:
+
+| Provider | Env prefix | Additional variables | Docs |
+| ----------------- | ---------------- | ------------------------------- | --------------------------------------------------------------------- |
+| Apple | `APPLE_` | - | [Login with Apple](/docs/guides/auth/social-login/auth-apple) |
+| Azure (Microsoft) | `AZURE_` | `URL` (tenant URL) | [Login with Azure](/docs/guides/auth/social-login/auth-azure) |
+| Bitbucket | `BITBUCKET_` | - | [Login with Bitbucket](/docs/guides/auth/social-login/auth-bitbucket) |
+| Discord | `DISCORD_` | - | [Login with Discord](/docs/guides/auth/social-login/auth-discord) |
+| Facebook | `FACEBOOK_` | - | [Login with Facebook](/docs/guides/auth/social-login/auth-facebook) |
+| Figma | `FIGMA_` | - | [Login with Figma](/docs/guides/auth/social-login/auth-figma) |
+| GitHub | `GITHUB_` | `URL` (for GitHub Enterprise) | [Login with GitHub](/docs/guides/auth/social-login/auth-github) |
+| GitLab | `GITLAB_` | `URL` (for self-hosted GitLab) | [Login with GitLab](/docs/guides/auth/social-login/auth-gitlab) |
+| Google | `GOOGLE_` | - | [Login with Google](/docs/guides/auth/social-login/auth-google) |
+| Kakao | `KAKAO_` | - | [Login with Kakao](/docs/guides/auth/social-login/auth-kakao) |
+| Keycloak (OIDC) | `KEYCLOAK_` | `URL` (realm URL, **required**) | [Login with Keycloak](/docs/guides/auth/social-login/auth-keycloak) |
+| LinkedIn (OIDC) | `LINKEDIN_OIDC_` | - | [Login with LinkedIn](/docs/guides/auth/social-login/auth-linkedin) |
+| Notion | `NOTION_` | - | [Login with Notion](/docs/guides/auth/social-login/auth-notion) |
+| Slack (OIDC) | `SLACK_OIDC_` | - | [Login with Slack](/docs/guides/auth/social-login/auth-slack) |
+| Snapchat | `SNAPCHAT_` | - | - |
+| Spotify | `SPOTIFY_` | - | [Login with Spotify](/docs/guides/auth/social-login/auth-spotify) |
+| Twitch | `TWITCH_` | - | [Login with Twitch](/docs/guides/auth/social-login/auth-twitch) |
+| Twitter | `TWITTER_` | - | [Login with Twitter](/docs/guides/auth/social-login/auth-twitter) |
+| WorkOS | `WORKOS_` | - | [Login with WorkOS](/docs/guides/auth/social-login/auth-workos) |
+| Zoom | `ZOOM_` | - | [Login with Zoom](/docs/guides/auth/social-login/auth-zoom) |
+
+For each provider, you need at minimum `ENABLED`, `CLIENT_ID`, `SECRET`, and `REDIRECT_URI` in `.env` and `docker-compose.yml`.
+
+
+
+**LinkedIn (OIDC)** and **Slack (OIDC)** use multi-word env prefixes. The full Docker Compose variables are `GOTRUE_EXTERNAL_LINKEDIN_OIDC_CLIENT_ID` and `GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID` respectively - not `LINKEDIN_CLIENT_ID` or `SLACK_CLIENT_ID`.
+
+
+
+## Test the login flow
+
+You can test OAuth with the following minimal HTML page:
+
+- Save the code below to `index.html`
+- Start `python -m http.server 3000` in the same directory
+- Make sure `SITE_URL` is set to `http://localhost:3000` in your self-hosted Supabase `.env` configuration
+- Open your browser and go to `http://localhost:3000`
+
+```html
+
+
+
+
Supabase OAuth Test
+
+
+
+
+
+
+
+```
+
+For detailed client-side integration, see [Social Login](/docs/guides/auth/social-login).
+
+## Troubleshooting
+
+### "Provider not enabled" or provider shows `false` in `/auth/v1/settings`
+
+- Check that `GOTRUE_EXTERNAL_*_ENABLED` is set to `true` in `docker-compose.yml`
+- Verify the `.env` variable is not empty, e.g., check with `docker compose exec auth env | grep GOOGLE`
+
+### Variables added to `.env` but provider still not working
+
+Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
+
+```
+GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
+```
+
+Run `docker compose exec auth env | grep GOTRUE_EXTERNAL` to verify the variables are reaching the container.
+
+### `SITE_URL` or redirect URL errors after login
+
+After a successful OAuth login, the Auth service redirects to `SITE_URL` or a URL from `ADDITIONAL_REDIRECT_URLS`. Ensure:
+
+- `SITE_URL` in `.env` is set to your application's URL
+- If your app uses a different redirect URL, add it to `ADDITIONAL_REDIRECT_URLS` (comma-separated)
+
+{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
+
+### Nonce check failure on mobile (Google Sign In)
+
+When using Google Sign In on mobile with ID tokens, nonce verification may fail because mobile SDKs don't always support the nonce flow that the Auth service expects.
+
+
+
+`GOTRUE_EXTERNAL_SKIP_NONCE_CHECK` disables nonce validation on ID tokens, which weakens replay-attack protection. Treat it as a **short-lived troubleshooting workaround**, not a permanent fix:
+
+- Enable it only in the environment where you're debugging the issue.
+- Revert it as soon as the issue is resolved.
+- Prefer fixing the client-side nonce handling or switching to an OAuth flow (authorization code with PKCE) that avoids ID-token nonce issues entirely.
+
+
+
+To enable it, uncomment the following line in `docker-compose.yml`:
+
+```
+GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true
+```
+
+### Auth service fails to start
+
+Check the auth container logs:
+
+```sh
+docker compose logs auth
+```
+
+Common causes:
+
+- Missing required environment variable (e.g., `CLIENT_ID` or `SECRET` is empty)
+- Invalid `API_EXTERNAL_URL` (must be a valid URL with protocol)
+
+## Environment variable reference
+
+All OAuth-related environment variables for the `auth` service in `docker-compose.yml`:
+
+| Variable | Description | Required |
+| -------------------------------- | ------------------------------------------------------------------------ | -------- |
+| `GOTRUE_EXTERNAL_*_ENABLED` | Enable the provider (`true`/`false`) | Yes |
+| `GOTRUE_EXTERNAL_*_CLIENT_ID` | OAuth client ID from the provider | Yes |
+| `GOTRUE_EXTERNAL_*_SECRET` | OAuth client secret from the provider | Yes |
+| `GOTRUE_EXTERNAL_*_REDIRECT_URI` | Callback URL: `${API_EXTERNAL_URL}/auth/v1/callback` | Yes |
+| `GOTRUE_SITE_URL` | Default redirect URL after authentication (set via `SITE_URL` in `.env`) | Yes |
+
+## Additional resources
+
+- [Redirect URLs](/docs/guides/auth/redirect-urls)
+- [Auth server on GitHub](https://github.com/supabase/auth) (check README and `example.env`)
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-phone-mfa.mdx b/apps/docs/content/guides/self-hosting/self-hosted-phone-mfa.mdx
new file mode 100644
index 00000000000..30d427e471f
--- /dev/null
+++ b/apps/docs/content/guides/self-hosting/self-hosted-phone-mfa.mdx
@@ -0,0 +1,225 @@
+---
+title: 'Configure Phone Login & MFA'
+description: 'Set up phone login SMS providers, OTP settings, and multi-factor authentication for self-hosted Supabase with Docker.'
+subtitle: 'Set up phone login SMS providers, OTP settings, and multi-factor authentication for self-hosted Supabase with Docker.'
+---
+
+This guide covers the **server-side configuration** for phone login and multi-factor authentication (MFA) on a self-hosted Supabase instance running with Docker Compose.
+
+For client-side implementation, see [Phone Login](/docs/guides/auth/phone-login) and [Multi-Factor Authentication](/docs/guides/auth/auth-mfa).
+
+## Before you begin
+
+You need:
+
+- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
+- An account with an SMS provider (e.g., Twilio)
+
+Phone auth is **enabled by default** in the Docker setup (`ENABLE_PHONE_SIGNUP=true` in `.env`). However, without an SMS provider configured, the Auth service has no way to deliver OTP codes.
+
+## SMS provider configuration
+
+The default `.env.example` and `docker-compose.yml` include commented-out SMS provider placeholders. The example below uses Twilio - you'll need a Twilio account with an account SID, auth token, and message service SID. See [Twilio's documentation](https://www.twilio.com/docs/messaging) for how to obtain these credentials.
+
+To enable SMS delivery:
+
+### Step 1: Uncomment and configure the settings in `.env`
+
+```
+SMS_PROVIDER=twilio
+SMS_OTP_EXP=60
+SMS_OTP_LENGTH=6
+SMS_MAX_FREQUENCY=60s
+SMS_TEMPLATE=Your code is {{ .Code }}
+
+## Twilio credentials
+SMS_TWILIO_ACCOUNT_SID=your-account-sid
+SMS_TWILIO_AUTH_TOKEN=your-auth-token
+SMS_TWILIO_MESSAGE_SERVICE_SID=your-message-service-sid
+```
+
+### Step 2: Uncomment the matching lines in `docker-compose.yml`
+
+Uncomment the `GOTRUE_SMS_*` lines in the `auth` service's `environment` block:
+
+```yaml
+auth:
+ environment:
+ # ... existing variables ...
+ GOTRUE_SMS_PROVIDER: ${SMS_PROVIDER}
+ GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}
+ GOTRUE_SMS_OTP_LENGTH: ${SMS_OTP_LENGTH}
+ GOTRUE_SMS_MAX_FREQUENCY: ${SMS_MAX_FREQUENCY}
+ GOTRUE_SMS_TEMPLATE: ${SMS_TEMPLATE}
+ GOTRUE_SMS_TWILIO_ACCOUNT_SID: ${SMS_TWILIO_ACCOUNT_SID}
+ GOTRUE_SMS_TWILIO_AUTH_TOKEN: ${SMS_TWILIO_AUTH_TOKEN}
+ GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: ${SMS_TWILIO_MESSAGE_SERVICE_SID}
+```
+
+### Step 3: Restart the auth service
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### Step 4: Verify
+
+```sh
+docker compose exec auth env | grep GOTRUE_SMS
+```
+
+Confirm your provider and credentials appear in the output.
+
+
+
+For providers other than Twilio, add the provider-specific `GOTRUE_SMS_*` lines manually to `docker-compose.yml`.
+
+
+
+## OTP settings
+
+### Expiration
+
+
+
+The default OTP expiration is **60 seconds**. This is often too short for production use, consider increasing it.
+
+
+
+Set `SMS_OTP_EXP` in `.env` (value is in seconds):
+
+```
+# Set expiration to 5 minutes
+SMS_OTP_EXP=300
+```
+
+And ensure `GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}` is uncommented in `docker-compose.yml`.
+
+### Length
+
+The default OTP length is 6 digits. You can set it to any value between 6 and 10:
+
+```
+SMS_OTP_LENGTH=8
+```
+
+### Rate limiting
+
+`SMS_MAX_FREQUENCY` controls the minimum interval between SMS sends to the same phone number. The default is 60 seconds:
+
+```
+## Allow one SMS every 30 seconds
+SMS_MAX_FREQUENCY=30s
+```
+
+## Test OTPs for development
+
+To avoid sending real SMS during development, use `SMS_TEST_OTP` to map phone numbers to fixed OTP codes:
+
+```
+SMS_TEST_OTP=16505551234:123456,16505555678:654321
+```
+
+And uncomment `GOTRUE_SMS_TEST_OTP: ${SMS_TEST_OTP}` in `docker-compose.yml`.
+
+When a test phone number requests an OTP, the Auth service skips SMS delivery and accepts only the mapped code. Other phone numbers continue to use the real SMS provider.
+
+
+
+Remove test OTPs before deploying to production. You can also set an expiration with `SMS_TEST_OTP_VALID_UNTIL` (ISO 8601 datetime, e.g., `2026-12-31T23:59:59Z`) so they stop working automatically.
+
+
+
+## Multi-factor authentication (MFA)
+
+The Auth service supports three MFA factor types. Configure them by uncommenting variables in `.env` and the matching `GOTRUE_MFA_*` lines in `docker-compose.yml`.
+
+### App authenticator (TOTP)
+
+TOTP is **enabled by default** - users can enroll with apps like Google Authenticator or Authy without any additional configuration.
+
+To disable TOTP:
+
+```
+MFA_TOTP_ENROLL_ENABLED=false
+MFA_TOTP_VERIFY_ENABLED=false
+```
+
+### Phone MFA
+
+Phone MFA is **disabled by default** (opt-in). It uses the same SMS provider configuration as phone login.
+
+To enable:
+
+```
+MFA_PHONE_ENROLL_ENABLED=true
+MFA_PHONE_VERIFY_ENABLED=true
+```
+
+### Maximum enrolled factors
+
+By default, a user can enroll up to 10 MFA factors. To change this:
+
+```
+MFA_MAX_ENROLLED_FACTORS=5
+```
+
+## Troubleshooting
+
+### OTP expires too quickly
+
+The default `SMS_OTP_EXP` is 60 seconds. Increase it in `.env`:
+
+```
+SMS_OTP_EXP=300
+```
+
+Ensure `GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}` is uncommented in `docker-compose.yml`, then restart:
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### SMS not being delivered
+
+Check the auth container logs for errors:
+
+```sh
+docker compose logs auth --tail 50
+```
+
+Verify provider credentials reach the container:
+
+```sh
+docker compose exec auth env | grep GOTRUE_SMS
+```
+
+Common causes:
+
+- Provider credentials are in `.env` but the matching `GOTRUE_SMS_*` line is still commented out in `docker-compose.yml`
+- Provider credentials are wrong
+- Phone number format is wrong (use E.164 format: `+1234567890`)
+
+### Variables are configured in `.env` but not working
+
+Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
+
+```sh
+docker compose exec auth env | grep -E 'GOTRUE_SMS|GOTRUE_MFA'
+```
+
+After changing the configuration environment variables, recreate the Auth service container:
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### Rate limit errors
+
+If users see "rate limit exceeded" errors, check `SMS_MAX_FREQUENCY` (minimum interval between sends) and the global rate limit `GOTRUE_RATE_LIMIT_SMS_SENT` (default: 30 per hour).
+
+### Additional resources
+
+- [Multi-Factor Authentication (Phone)](/docs/guides/auth/auth-mfa/phone)
+- [Multi-Factor Authentication (TOTP)](/docs/guides/auth/auth-mfa/totp)
+- [Auth server on GitHub](https://github.com/supabase/auth) (check README and `example.env`)
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx b/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx
new file mode 100644
index 00000000000..25723468ba8
--- /dev/null
+++ b/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx
@@ -0,0 +1,228 @@
+---
+title: 'Configure Reverse Proxy and HTTPS'
+description: 'Set up a reverse proxy with HTTPS for self-hosted Supabase.'
+subtitle: 'Set up a reverse proxy with HTTPS for self-hosted Supabase.'
+---
+
+HTTPS is required for production self-hosted Supabase deployments. This guide covers two production approaches using a reverse proxy in front of self-hosted Supabase API gateway, plus a self-signed certificate option for development environment.
+
+## Before you begin
+
+You need:
+
+- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
+- A domain name with DNS pointing to your server's public IP address (to obtain Let's Encrypt certificate).
+- Ports 80 and 443 open.
+
+## Set up HTTPS
+
+Below are two options for adding a reverse proxy with automatic HTTPS in front of your self-hosted Supabase: **Caddy** (simpler, zero-config TLS) and **Nginx + Let's Encrypt** (more control over proxy settings). Both sit in front of Kong and terminate TLS, so internal traffic stays on HTTP.
+
+
+
+If you already run [HAProxy](https://www.haproxy.com/), [Traefik](https://traefik.io/), [Nginx Proxy Manager](https://nginxproxymanager.com/), or another reverse proxy for your infrastructure, you can use it instead of Caddy or Nginx above. The key requirements are:
+
+- Proxy to Kong on port `8000` (or `:8000` if the proxy runs outside the Docker network)
+- Enable WebSocket support (required for Realtime)
+- Proxy traffic to Storage directly to the container, bypassing Kong
+- Add `X-Forwarded` headers to all requests
+- Comment out Kong's host port bindings in `docker-compose.yml` if the proxy runs in the same Docker network
+- Update `SUPABASE_PUBLIC_URL`, `API_EXTERNAL_URL`, and `SITE_URL` in `.env` to your HTTPS URL
+
+
+
+### Step 1: Remove public port bindings for API gateway
+
+Comment out Kong's host port mappings in `docker-compose.yml` so that it's not exposed to the Internet:
+
+```yaml
+kong:
+ # ...
+ ports:
+ # - ${KONG_HTTP_PORT}:8000/tcp
+ # - ${KONG_HTTPS_PORT}:8443/tcp
+```
+
+Kong remains accessible to other containers on the internal Docker network.
+
+### Step 2: Update environment variables
+
+Update the URL configuration in your `.env` file to use your HTTPS domain:
+
+```
+SUPABASE_PUBLIC_URL=https://
+API_EXTERNAL_URL=https://
+SITE_URL=https://
+```
+
+Change the following to your domain name and a **valid** email address:
+
+```
+PROXY_DOMAIN=your-domain.example.com
+CERTBOT_EMAIL=admin@your-domain.example.com
+```
+
+### Step 3: Start the reverse proxy
+
+Pick one of the options below and use the corresponding Docker Compose overlay.
+
+
+
+[Caddy](https://caddyserver.com/) automatically provisions and renews Let's Encrypt TLS certificates with zero configuration. It also handles HTTP-to-HTTPS redirects, WebSocket upgrades, and HTTP/2 and HTTP/3 out of the box.
+
+Start Caddy by using the pre-configured `docker-compose.caddy.yml` overlay:
+
+```sh
+docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
+```
+
+Caddy configuration is in `volumes/proxy/caddy/Caddyfile`.
+
+
+
+
+This option uses a 3rd party Nginx Docker image ([`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot)), which includes Certbot for automatic Let's Encrypt certificate issuance and renewal in a single container.
+
+Start Nginx by using the pre-configured `docker-compose.nginx.yml` overlay:
+
+```sh
+docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
+```
+
+Nginx configuration template is in `volumes/proxy/nginx/supabase-nginx.conf.tpl`. On container startup, `${NGINX_SERVER_NAME}` is substituted using the environment variable from the `.env` file. The [`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot) image reads the resolved `server_name` to determine which domain to request a Let's Encrypt certificate for.
+
+HTTP-to-HTTPS redirects are handled automatically by the `jonasal/nginx-certbot` image.
+
+
+
+
+### Step 4: Verify HTTPS connection
+
+```sh
+curl -I https:///auth/v1/
+```
+
+You should receive a `401` response confirming you could connect to Auth.
+
+## Self-signed certificates (development only)
+
+
+
+Self-signed certificates trigger browser warnings and are rejected by most OAuth providers. Use this approach only in development environment or internal networks.
+
+
+
+For development or internal networks where you cannot use Let's Encrypt, you can configure Kong to serve HTTPS directly using self-signed certificates.
+
+### Step 1: Generate a self-signed certificate
+
+Change `` in the example below, and create certificates with `openssl`:
+
+```sh
+openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout volumes/api/server.key \
+ -out volumes/api/server.crt \
+ -subj "/CN=" && \
+ chmod 640 volumes/api/server.key && \
+ chgrp 65533 volumes/api/server.key
+```
+
+{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
+
+### Step 2: Configure Kong for SSL
+
+Comment out Kong's HTTP port mapping in `docker-compose.yml`:
+
+```yaml
+kong:
+ # ...
+ ports:
+ # - ${KONG_HTTP_PORT}:8000/tcp
+```
+
+Uncomment the certificate volume mounts and SSL environment variables in `docker-compose.yml`:
+
+```yaml
+kong:
+ # ... existing configuration ...
+ volumes:
+ - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
+ - ./volumes/api/server.crt:/home/kong/server.crt:ro
+ - ./volumes/api/server.key:/home/kong/server.key:ro
+ environment:
+ # ... existing environment variables ...
+ KONG_SSL_CERT: /home/kong/server.crt
+ KONG_SSL_CERT_KEY: /home/kong/server.key
+```
+
+### Step 3: Update configuration variables in `.env`
+
+Edit your `.env` file to use HTTPS with the Kong HTTPS port:
+
+```
+SUPABASE_PUBLIC_URL=https://:8443
+API_EXTERNAL_URL=https://:8443
+SITE_URL=https://:8443
+```
+
+### Step 4: Restart and verify
+
+```sh
+docker compose down && docker compose up -d
+```
+
+```sh
+curl -I -k https://:8443/auth/v1/
+```
+
+The `-k` flag tells curl to accept the self-signed certificate.
+
+## Troubleshooting
+
+### Certificate not issued
+
+If Caddy or Certbot fails to obtain a certificate:
+
+- Verify that ports 80 and 443 are open on your firewall
+- Verify that your domain's DNS A record points to your server's public IP
+- Check proxy logs via `docker logs supabase-caddy` or `docker logs supabase-nginx`
+- Let's Encrypt has [rate limits](https://letsencrypt.org/docs/rate-limits/) - if you hit them, wait before retrying
+
+### WebSocket connection failed
+
+If Realtime subscriptions fail to connect:
+
+- **Caddy** handles WebSocket upgrades automatically - check that Kong is healthy
+- **Nginx** requires explicit `Upgrade` and `Connection` headers on the `/realtime/v1/` location. Verify your `nginx.conf` includes these headers as shown above
+
+### OAuth callback URL mismatch
+
+If OAuth redirects fail with a callback URL error:
+
+- Verify `API_EXTERNAL_URL` in `.env` is set to your HTTPS URL
+- Verify the callback URL registered with your OAuth provider matches `API_EXTERNAL_URL` followed by `/auth/v1/callback`
+- After changing `API_EXTERNAL_URL`, restart all services with `docker compose down && docker compose up -d`
+
+### Mixed content warnings
+
+If the browser console shows mixed content errors:
+
+- Verify `SUPABASE_PUBLIC_URL` is set to your HTTPS URL
+- Verify `SITE_URL` is also set to HTTPS
+- Clear your browser cache after making changes
+
+### ERR_CERT_AUTHORITY_INVALID
+
+This is expected when using self-signed certificates. For production, use Caddy or Nginx with Let's Encrypt. If you need to use self-signed certificates, add the certificate to your system's trust store or use a browser flag to bypass the warning.
+
+## Additional resources
+
+- [Caddy documentation](https://caddyserver.com/docs/)
+- [Nginx documentation](https://nginx.org/en/docs/) (on nginx.org)
+- [docker-nginx-certbot on GitHub](https://github.com/JonasAlfredsson/docker-nginx-certbot)
diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/telemetry/log-drains.mdx
index 9bf926f643d..9b5b8adfdd0 100644
--- a/apps/docs/content/guides/telemetry/log-drains.mdx
+++ b/apps/docs/content/guides/telemetry/log-drains.mdx
@@ -4,7 +4,7 @@ title: 'Log Drains'
description: 'Getting started with Supabase Log Drains'
---
-Log drains will send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Team and Enterprise Plans. Log drains is available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains).
+Log drains send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Pro, Team and Enterprise Plans. Log drains are available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains).
You can read about the initial announcement [here](/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top).
diff --git a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx
index ecbb16f8bea..203e64a42dd 100644
--- a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx
+++ b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx
@@ -9,7 +9,7 @@ database_id = "ba593989-12b5-464f-8ede-a525e1ca2ffb"
**What is Egress?**
-Egress (also known as bandwidth) is any amount of network packets/bytes being streamed back to a connected client. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor.
+Egress (also known as bandwidth) is any amount of network packets/bytes being streamed to a connected client from your project. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor.
You can read about Unified egress, included quota, and how to check the egress usage here: https://supabase.com/docs/guides/platform/manage-your-usage/egress. Additionally, the [project reports](/dashboard/project/_/observability) have a few egress related stats. You can create a custom report to look into daily egress.
diff --git a/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx b/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx
index 43284c297b6..fa251d50167 100644
--- a/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx
+++ b/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx
@@ -16,8 +16,12 @@ message = "connect ECONNREFUSED 1.2.3.4:5432"
message = "psql: error: connection to server at \"db.xxxxxxxxxxxxxxxxxxxx.supabase.co\" (1.2.3.4), port 5432 failed: Connection refused Is the server running on that host and accepting TCP/IP connections?"
---
-If you're not able to connect to the Supabase database and see the error `connect ECONNREFUSED 1.2.3.4:5432` or `psql: error: connection to server at "db.xxxxxxxxxxxxxxxxxxxx.supabase.co" (1.2.3.4), port 5432 failed: Connection refused
-Is the server running on that host and accepting TCP/IP connections?`, this could be because there are banned IPs on your project caused by Fail2ban as it kicks in when attempting 2 wrong passwords in a row.
+If you're not able to connect to the Supabase database and see one of the errors below, this could be because there are banned IPs on your project caused by Fail2ban as it kicks in when attempting 2 wrong passwords in a row.
+
+- `connect ECONNREFUSED 1.2.3.4:5432`
+- `psql: error: connection to server at "db.xxxxxxxxxxxxxxxxxxxx.supabase.co" (1.2.3.4), port 5432 failed: Connection refused
+Is the server running on that host and accepting TCP/IP connections?`
+- `Circuit breaker open: Unable to establish connection to upstream database`
These bans will clear after 30mins but you can unban the IPs using the Supabase CLI https://supabase.com/docs/guides/cli following the commands below.
diff --git a/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx b/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx
index c4fdf405942..d666ff718b3 100644
--- a/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx
+++ b/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx
@@ -20,3 +20,45 @@ To overcome these limitations and implement robust folder management with hierar
- **Implement RLS policies on `storage.objects`.** These policies must `JOIN` with your custom metadata table to enforce hierarchical access permissions based on your defined folder structure.
- **Handle batch folder operations via your metadata table.** For operations like moving or renaming folders, update the relevant entries in your custom metadata table. Note that actual file paths in Storage are not directly altered by these operations.
- **Optimize RLS policies for performance.** `JOIN`s in RLS policies can lead to performance degradation, especially with large datasets. Ensure proper indexing on your custom metadata table and consider using `SECURITY DEFINER` functions to optimize policy execution.
+
+## Alternative approach: Using the S3 protocol for bulk operations
+
+Supabase Storage also supports an S3-compatible API. This allows you to use tools like the AWS CLI to perform bulk file operations such as downloading, moving, or reorganizing objects more efficiently.
+
+Install the AWS CLI by following the [AWS CLI installation guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
+
+Create S3 credentials in Supabase using the [Supabase S3 authentication guide](/docs/guides/storage/s3/authentication?queryGroups=language&language=credentials).
+
+Configure an AWS CLI profile using the credentials you generated in Supabase. The profile name can be anything, but it must match the value used in the following commands.
+
+```shell
+aws configure --profile supabase-s3
+```
+
+Download files from a bucket or prefix:
+
+```shell
+aws s3 cp s3://bucket-name/folder-name ./download-target
+--profile supabase-s3
+--endpoint-url https://.supabase.co/storage/v1/s3
+--recursive
+--region
+```
+
+- Replace `bucket-name` with your bucket name.
+- Replace `folder-name` with the prefix you want to download, or omit it to download the entire bucket.
+- Replace `` with your Supabase project reference.
+- Replace `` with your project's region (for example `eu-central-1`).
+- `./download-target` is the local directory where files will be saved.
+
+Move or rename files using the `mv` command. Because folders in Supabase Storage are implemented as prefixes, renaming a folder is effectively moving objects from one prefix to another.
+
+```shell
+aws s3 mv s3://bucket-name-one/folder-name-one s3://bucket-name-two/folder-name-two
+--profile supabase-s3
+--endpoint-url https://.supabase.co/storage/v1/s3
+--recursive
+--region
+```
+
+This method is useful for large-scale downloads, migrations, or reorganizing files within a bucket.
diff --git a/apps/docs/features/docs/GuidesMdx.template.tsx b/apps/docs/features/docs/GuidesMdx.template.tsx
index 76fbe6d397c..97758032965 100644
--- a/apps/docs/features/docs/GuidesMdx.template.tsx
+++ b/apps/docs/features/docs/GuidesMdx.template.tsx
@@ -5,7 +5,7 @@ import ReactMarkdown from 'react-markdown'
import { cn } from 'ui'
import Breadcrumbs from '~/components/Breadcrumbs'
-import GuidesTableOfContents from '~/components/GuidesTableOfContents'
+import GuidesSidebar from '~/components/GuidesSidebar'
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import type { WithRequired } from '~/features/helpers.types'
@@ -71,7 +71,7 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
'relative',
'transition-all ease-out',
'duration-100',
- hideToc ? 'col-span-12' : 'col-span-12 md:col-span-9'
+ 'col-span-12 md:col-span-9'
)}
>
@@ -114,24 +114,23 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
- {!hideToc && (
-
- )}
+
)
diff --git a/apps/docs/features/ui/guide/Guide.tsx b/apps/docs/features/ui/guide/Guide.tsx
index e0179dd7ab3..9fa9b8c31c3 100644
--- a/apps/docs/features/ui/guide/Guide.tsx
+++ b/apps/docs/features/ui/guide/Guide.tsx
@@ -3,7 +3,7 @@
import { createContext, useContext, type ReactNode } from 'react'
import { cn } from 'ui'
-import GuidesTableOfContents from '~/components/GuidesTableOfContents'
+import GuidesTableOfContents from '~/components/GuidesSidebar'
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
import { type GuideFrontmatter } from '~/lib/docs'
diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts
new file mode 100644
index 00000000000..c07cb27bcb2
--- /dev/null
+++ b/apps/docs/internals/generate-guides-markdown.ts
@@ -0,0 +1,165 @@
+import fs from 'fs'
+import path from 'path'
+import { globby } from 'globby'
+import matter from 'gray-matter'
+
+const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials')
+
+/**
+ * Reads <$Partial path="..." /> tags and replaces them with the file contents.
+ * Recurses to handle nested partials.
+ */
+async function inlinePartials(content: string): Promise {
+ const partialRegex = /<\$Partial\s+path="([^"]+)"[^/]*\/>/g
+ const matches = [...content.matchAll(partialRegex)]
+ for (const [fullMatch, partialPath] of matches) {
+ try {
+ const raw = await fs.promises.readFile(path.join(PARTIALS_DIR, partialPath), 'utf8')
+ const { content: partialBody } = matter(raw)
+ const inlined = await inlinePartials(partialBody)
+ content = content.replace(fullMatch, inlined)
+ } catch {
+ content = content.replace(fullMatch, '')
+ }
+ }
+ return content
+}
+
+/** Remove the minimum common leading whitespace from all non-empty lines. */
+function dedentBlock(text: string): string {
+ const lines = text.split('\n')
+ const nonEmpty = lines.filter((l) => /\S/.test(l))
+ if (!nonEmpty.length) return text
+ const minIndent = Math.min(...nonEmpty.map((l) => (l.match(/^([ \t]*)/) ?? ['', ''])[1].length))
+ if (!minIndent) return text
+ return lines.map((l) => l.slice(minIndent)).join('\n')
+}
+
+/**
+ * Converts StepHikeCompact components to markdown ordered lists.
+ * Each step becomes a numbered item: the title (from Details) is bolded on the item
+ * line, and the full step body (Details + Code) is dedented and appended below.
+ * Remaining JSX tags inside the body are later stripped by stripJsxTags.
+ */
+function convertStepHike(content: string): string {
+ return content.replace(/([\s\S]*?)<\/StepHikeCompact>/g, (_, body) => {
+ const items: string[] = []
+ const stepRe = /]*>([\s\S]*?)<\/StepHikeCompact\.Step>/g
+ let stepNum = 1
+ let m: RegExpExecArray | null
+ while ((m = stepRe.exec(body)) !== null) {
+ const stepBody = m[1]
+ const titleMatch = stepBody.match(/]+title="([^"]*)"/)
+ const title = titleMatch ? titleMatch[1] : ''
+ // Dedent the entire step body so nested JSX indentation is removed.
+ // Remaining component tags (Details, Code, Admonition…) are stripped later.
+ const inner = dedentBlock(stepBody).trim()
+ const item = title ? `${stepNum}. **${title}**\n\n${inner}` : `${stepNum}. ${inner}`
+ items.push(item)
+ stepNum++
+ }
+ return items.join('\n\n')
+ })
+}
+
+/**
+ * Strips JSX component tags (capitalized names, dot-notation, or $-prefixed)
+ * while keeping their inner content. Also strips wrapper div and a elements.
+ * Removes MDX JSX comment blocks. Strips unnecessary leading indentation from
+ * non-code-block lines.
+ */
+function stripJsxTags(content: string): string {
+ // Remove MDX/JSX comments {/* ... */}
+ content = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, '')
+
+ // Remove self-closing JSX components: or <$Directive ... />
+ content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*\/>/gs, '')
+
+ // Remove opening JSX component tags (possibly multi-line):
+ content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*>/gs, '')
+
+ // Remove closing JSX component tags:
+ content = content.replace(/<\/[\$A-Z][\w.]*>/g, '')
+
+ // Remove wrapper div and a elements used structurally in MDX (carry JSX props
+ // like className which are not valid HTML; inner content such as img is preserved)
+ content = content.replace(/
- There are unsaved changes. Are you sure you want to close the panel? Your changes will be
- lost.
-
-
-)
-
interface FormFieldConfigParamsProps {
readonly?: boolean
}
diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx
index 9c078d71c88..11646d724e7 100644
--- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx
@@ -8,16 +8,16 @@ import { useEffect, useRef, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { Button, Form_Shadcn_, SidePanel } from 'ui'
-import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormSchema, WebhookFormValues } from './EditHookPanel.constants'
import { FormContents } from './FormContents'
+import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation'
import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-transaction-mutation'
import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query'
import { tableEditorQueryOptions } from '@/data/table-editor/table-editor-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { useConfirmOnClose, type ConfirmOnCloseModalProps } from '@/hooks/ui/useConfirmOnClose'
+import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { uuidv4 } from '@/lib/helpers'
export type HTTPArgument = { id: string; name: string; value: string }
@@ -288,7 +288,7 @@ export const EditHookPanel = () => {
// This is intentionally kept outside of the useConfirmOnClose hook to force RHF to update the isDirty state.
const isDirty = form.formState.isDirty
- const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
+ const { confirmOnClose, modalProps } = useConfirmOnClose({
checkIsDirty: () => isDirty,
onClose: () => onClose(),
})
@@ -340,22 +340,7 @@ export const EditHookPanel = () => {
-
+
>
)
}
-
-const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
-
-
- There are unsaved changes. Are you sure you want to close the panel? Your changes will be
- lost.
-
-
-)
diff --git a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx
index b69e049afc5..8b5203f133e 100644
--- a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx
+++ b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx
@@ -72,7 +72,7 @@ const Migrations = () => {
Try refreshing your browser, but if the issue persists for more than a few
minutes, please reach out to us via support.
-
- Automatically replicate your data to external data warehouses and analytics platforms in
- real-time. No manual exports, no lag.
-
-
- We are currently in private alpha and slowly
- onboarding new customers to ensure stable data pipelines. Request access below to join the
- waitlist. Read replicas are available now.
-
- {unifiedReplication
- ? 'Replication keeps your data in sync across systems'
- : 'Create your first destination'}
-
+
Replication keeps your data in sync across systems
- {unifiedReplication
- ? 'Deploy read replicas for lower latency and better resource management, or capture database changes to external platforms for real-time data pipelines.'
- : 'Destinations are external platforms where your database changes are automatically sent. Connect to various data warehouses and analytics platforms to enable real-time data pipelines.'}
+ Deploy read replicas for lower latency and better resource management, or capture
+ database changes to external platforms for real-time data pipelines.
-
- )
-}
diff --git a/apps/studio/components/interfaces/LocalDropdown.tsx b/apps/studio/components/interfaces/LocalDropdown.tsx
index c4950b00cde..a335bb2b807 100644
--- a/apps/studio/components/interfaces/LocalDropdown.tsx
+++ b/apps/studio/components/interfaces/LocalDropdown.tsx
@@ -36,14 +36,17 @@ export const LocalDropdown = () => {
toggleFeaturePreviewModal(true)}
onSelect={() => toggleFeaturePreviewModal(true)}
>
Feature previews
- setCommandMenuOpen(true)}>
+ setCommandMenuOpen(true)}
+ >
Command menu
@@ -57,7 +60,11 @@ export const LocalDropdown = () => {
}}
>
{singleThemes.map((theme: Theme) => (
-
+
{theme.name}
))}
diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx
index c9f6ab3af98..8765914043b 100644
--- a/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx
+++ b/apps/studio/components/interfaces/LogDrains/LogDrainsEmpty.tsx
@@ -3,6 +3,7 @@ import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { DOCS_URL } from 'lib/constants'
import Link from 'next/link'
import { Button, Card, cn } from 'ui'
+
import { AnimatedLogos } from './AnimatedLogos'
import { VoteLink } from './VoteLink'
@@ -12,7 +13,7 @@ export const LogDrainsEmpty = () => {
step: 1,
title: 'Pricing',
description:
- 'Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs $60 per month.',
+ 'Log Drains are available as a project Add-On for all Pro, Team, and Enterprise users. Each Log Drain costs $60 per month.',
label: 'See our pricing',
link: `${DOCS_URL}/guides/platform/manage-your-usage/log-drains`,
},
@@ -34,10 +35,11 @@ export const LogDrainsEmpty = () => {
Capture your logs, your way
- Upgrade to a Team or Enterprise Plan to send your logs to your preferred platform
+ Upgrade to a Pro, Team or Enterprise Plan to send your logs to your preferred platform
+ {/* This should only be shown to free tier users so upgrade to Pro makes sense */}
diff --git a/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx b/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx
index a41b5d6e045..c8235010cc1 100644
--- a/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx
+++ b/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx
@@ -1,11 +1,10 @@
import { useQueryClient } from '@tanstack/react-query'
-import { useFlag, useParams } from 'common'
+import { useParams } from 'common'
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import { ChartIntervalDropdown } from 'components/ui/Logs/ChartIntervalDropdown'
import { CHART_INTERVALS } from 'components/ui/Logs/logs.utils'
import dayjs from 'dayjs'
-import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { RefreshCw } from 'lucide-react'
@@ -25,14 +24,8 @@ export const ObservabilityOverview = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
- const { plan } = useCurrentOrgPlan()
const queryClient = useQueryClient()
- const authReportEnabled = useFlag('authreportv2')
- const edgeFnReportEnabled = useFlag('edgefunctionreport')
- const realtimeReportEnabled = useFlag('realtimeReport')
- const storageReportEnabled = useFlag('storagereport')
- const postgrestReportEnabled = useFlag('postgrestreport')
const { projectStorageAll: storageSupported } = useIsFeatureEnabled(['project_storage:all'])
const DEFAULT_INTERVAL: ChartIntervalKey = '1day'
@@ -77,7 +70,7 @@ export const ObservabilityOverview = () => {
reportUrl: `/project/${projectRef}/observability/auth`,
logsUrl: `/project/${projectRef}/logs/auth-logs`,
enabled: true,
- hasReport: authReportEnabled,
+ hasReport: true,
},
{
key: 'functions' as const,
@@ -85,7 +78,7 @@ export const ObservabilityOverview = () => {
reportUrl: `/project/${projectRef}/observability/edge-functions`,
logsUrl: `/project/${projectRef}/logs/edge-functions-logs`,
enabled: true,
- hasReport: edgeFnReportEnabled,
+ hasReport: true,
},
{
key: 'realtime' as const,
@@ -93,7 +86,7 @@ export const ObservabilityOverview = () => {
reportUrl: `/project/${projectRef}/observability/realtime`,
logsUrl: `/project/${projectRef}/logs/realtime-logs`,
enabled: true,
- hasReport: realtimeReportEnabled,
+ hasReport: true,
},
{
key: 'storage' as const,
@@ -101,7 +94,7 @@ export const ObservabilityOverview = () => {
reportUrl: `/project/${projectRef}/observability/storage`,
logsUrl: `/project/${projectRef}/logs/storage-logs`,
enabled: storageSupported,
- hasReport: storageReportEnabled,
+ hasReport: true,
},
{
key: 'postgrest' as const,
@@ -109,18 +102,10 @@ export const ObservabilityOverview = () => {
reportUrl: `/project/${projectRef}/observability/postgrest`,
logsUrl: `/project/${projectRef}/logs/postgrest-logs`,
enabled: true,
- hasReport: postgrestReportEnabled,
+ hasReport: true,
},
],
- [
- projectRef,
- authReportEnabled,
- edgeFnReportEnabled,
- realtimeReportEnabled,
- storageReportEnabled,
- storageSupported,
- postgrestReportEnabled,
- ]
+ [projectRef, storageSupported]
)
const enabledServices = serviceBase.filter((s) => s.enabled)
@@ -169,8 +154,6 @@ export const ObservabilityOverview = () => {
setInterval(interval as ChartIntervalKey)}
- planId={plan?.id}
- planName={plan?.name}
organizationSlug={organization?.slug}
dropdownAlign="end"
tooltipSide="left"
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts
index b34098174b3..de562f4824a 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts
@@ -721,13 +721,6 @@ export const TAX_IDS: TaxId[] = [
placeholder: 'MK1234567890123',
countryIso2: 'MK',
},
- {
- name: 'nz_gst',
- type: 'nz_gst',
- country: 'New Zealand',
- placeholder: '123456789',
- countryIso2: 'NZ',
- },
{
name: 'NO VAT',
type: 'no_vat',
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx
index 332e9c7cc8d..f7c23432868 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx
@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { ExternalLink } from 'lucide-react'
-import { useTheme } from 'next-themes'
-import Image from 'next/image'
-import Link from 'next/link'
-
import { useFlag, useParams } from 'common'
import {
ScaffoldSection,
@@ -19,10 +14,15 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { MANAGED_BY } from 'lib/constants/infrastructure'
+import { ExternalLink } from 'lucide-react'
+import { useTheme } from 'next-themes'
+import Image from 'next/image'
+import Link from 'next/link'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
-import { Alert, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
+import { Alert, Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
-import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'
+
+import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
import SpendCapSidePanel from './SpendCapSidePanel'
export interface CostControlProps {}
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx
index 4956a78098a..a02b3300730 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx
@@ -7,29 +7,28 @@ export interface ProjectUpdateDisabledTooltipProps {
tooltip?: string
}
-const ProjectUpdateDisabledTooltip = ({
+export const ProjectUpdateDisabledTooltip = ({
projectUpdateDisabled,
projectNotActive = false,
children,
tooltip,
}: PropsWithChildren) => {
- const showTooltip = projectUpdateDisabled || projectNotActive
+ const tooltipMessage =
+ tooltip ||
+ (projectUpdateDisabled
+ ? 'Subscription changes are currently disabled. Our engineers are working on a fix.'
+ : projectNotActive
+ ? 'Unable to update subscription as project is currently not active'
+ : undefined)
return (
{children}
- {showTooltip && (
-
- {projectUpdateDisabled
- ? tooltip ||
- 'Subscription changes are currently disabled. Our engineers are working on a fix.'
- : projectNotActive
- ? 'Unable to update subscription as project is currently not active'
- : ''}
+ {tooltipMessage !== undefined && (
+
+ {tooltipMessage}
)}
)
}
-
-export default ProjectUpdateDisabledTooltip
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx
index 198c988faa2..a9decb09b3d 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx
@@ -1,7 +1,6 @@
-import { MinusCircle, PauseCircle } from 'lucide-react'
-
import { getComputeSize, OrgProject } from 'data/projects/org-projects-infinite-query'
import type { OrgSubscription, ProjectAddon } from 'data/subscriptions/types'
+import { MinusCircle, PauseCircle } from 'lucide-react'
import { useMemo } from 'react'
import { plans as subscriptionsPlans } from 'shared-data/plans'
import { Modal } from 'ui'
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx
index 2ab3378a058..fec46b51718 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx
@@ -1,13 +1,13 @@
-import { useState } from 'react'
-import { toast } from 'sonner'
-
import { useFlag, useParams } from 'common'
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
import { getComputeSize, OrgProject } from 'data/projects/org-projects-infinite-query'
import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
+import { useState } from 'react'
+import { toast } from 'sonner'
import { Alert, Button, cn, Input, Modal } from 'ui'
-import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'
+
+import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
export interface ExitSurveyModalProps {
visible: boolean
@@ -104,7 +104,7 @@ export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalP
- Share with us why you're downgrading your plan.
+ What made you decide to downgrade your plan?
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx
index 3c6b01b2faa..b8252ec5b81 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx
@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { isArray } from 'lodash'
-import { Check, ExternalLink } from 'lucide-react'
-import { useRouter } from 'next/router'
-import { useEffect, useMemo, useRef, useState } from 'react'
-
import { useParams } from 'common'
import { StudioPricingSidePanelOpenedEvent } from 'common/telemetry-constants'
import { getPlanChangeType } from 'components/interfaces/Billing/Subscription/Subscription.utils'
@@ -22,11 +17,16 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { formatCurrency } from 'lib/helpers'
+import { isArray } from 'lodash'
+import { Check, ExternalLink } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { plans as subscriptionsPlans } from 'shared-data/plans'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Organization } from 'types/base'
-import { Button, SidePanel, cn } from 'ui'
+import { Button, cn, SidePanel } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
import DowngradeModal from './DowngradeModal'
import { EnterpriseCard } from './EnterpriseCard'
import { ExitSurveyModal } from './ExitSurveyModal'
@@ -187,7 +187,8 @@ export const PlanUpdateSidePanel = () => {
const source = Array.isArray(router.query.source)
? router.query.source[0]
: router.query.source
- const shouldHighlight = source === 'log-drains-empty-state' && plan.id === 'tier_team'
+ // TODO this panel should allow direct configuration of the highlighting rather than indirectly via the source param
+ const shouldHighlight = source === 'log-drains-empty-state' && plan.id === 'tier_pro'
if (plan.id === 'tier_enterprise') {
return
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
index 8494343da25..f3f6b37074c 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
@@ -1,6 +1,4 @@
import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
-import Link from 'next/link'
-
import { useFlag, useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import {
@@ -12,11 +10,13 @@ import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import Link from 'next/link'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Alert, Button } from 'ui'
import { Admonition } from 'ui-patterns'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
-import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'
+
+import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
import { Restriction } from '../Restriction'
import { PlanUpdateSidePanel } from './PlanUpdateSidePanel'
diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx
index f09b319cd5d..52d6e954610 100644
--- a/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx
+++ b/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx
@@ -1,9 +1,8 @@
import { Trash } from 'lucide-react'
-import Table from 'components/to-be-cleaned/Table'
import CopyButton from 'components/ui/CopyButton'
import type { AuthorizedApp } from 'data/oauth/authorized-apps-query'
-import { Button } from 'ui'
+import { Button, TableCell, TableRow } from 'ui'
import { TimestampInfo } from 'ui-patterns'
export interface AuthorizedAppRowProps {
@@ -13,35 +12,39 @@ export interface AuthorizedAppRowProps {
export const AuthorizedAppRow = ({ app, onSelectRevoke }: AuthorizedAppRowProps) => {
return (
-
-
+
+
- Direct connections to the database only work if your client is able to resolve IPv6
- addresses. Enabling the dedicated IPv4 add-on allows you to directly connect to your
- database via a IPv4 address.
+ Your project’s direct connection endpoint and dedicated pooler are IPv6-only by default.
+ Enable the dedicated IPv4 address add-on to connect from IPv4-only networks.
+
+
+
+ The shared pooler endpoint accepts IPv4 connections by default and does not require this
+ add-on.
- If you are connecting via the Shared connection pooler, you do not need this add-on as
- our pooler resolves to IPv4 addresses. You can check your connection info in your{' '}
-
- project database settings
-
- .
-
- By default, this is only applied to the Primary database for your project. If{' '}
-
- Read replicas
- {' '}
+ By default, this is only applied to the primary database for your project. If{' '}
+
+ read replicas
+ {' '}
are used, each replica also gets its own IPv4 address, with a corresponding{' '}
{formatCurrency(selectedIPv4?.price)}{' '}
charge.
)}
- There are no immediate charges. The addon is billed at the end of your billing cycle
- based on your usage and prorated to the hour.
+ There are no immediate charges. The add-on is billed at the end of your billing
+ cycle based on your usage and prorated to the hour.
>
)}
{!hasAccessToIPv4 && (
-
Upgrade your plan to enable a IPv4 address for your project
+
Upgrade your plan to enable an IPv4 address for your project
- If your network only supports IPv4, consider purchasing the{' '}
-
- IPv4 add-on
-
- .
-
-
+
+
+ Enable IPv4 add-on
+
+
+ }
+ />
)}
- {organization?.plan?.id === 'free'
+ {hasAccessToDiskSizeConfig === false
? 'Disk size configuration is not available for projects on the Free Plan'
: 'Disk size configuration is only available when the spend cap has been disabled'}
- {organization?.plan?.id === 'free' ? (
+ {hasAccessToDiskSizeConfig === false ? (
If you are intending to use more than 500MB of disk space, then you will need to
upgrade to at least the Pro Plan.
@@ -191,11 +196,11 @@ Read more about [disk management](${DOCS_URL}/guides/platform/database-size#disk
- {organization?.plan?.id === 'free'
+ {hasAccessToDiskSizeConfig === false
? 'Upgrade subscription'
: 'Disable spend cap'}
diff --git a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx
index a964795a204..e896af177e2 100644
--- a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx
@@ -9,6 +9,7 @@ import { number, object } from 'yup'
import { useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation'
+import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -45,7 +46,10 @@ const DiskSizeConfigurationModal = ({
const { data: projectSubscriptionData, isPending: isLoadingSubscription } =
useOrgSubscriptionQuery({ orgSlug: organization?.slug }, { enabled: visible })
- const isLoading = isLoadingProject || isLoadingSubscription
+ const { hasAccess: hasAccessToDiskModifications, isLoading: isLoadingDiskEntitlement } =
+ useCheckEntitlements('instances.disk_modifications')
+
+ const isLoading = isLoadingProject || isLoadingSubscription || isLoadingDiskEntitlement
const timeTillNextAvailableDatabaseResize =
lastDatabaseResizeAt === null ? 0 : 6 * 60 - dayjs().diff(lastDatabaseResizeAt, 'minutes')
@@ -101,7 +105,8 @@ const DiskSizeConfigurationModal = ({
- {projectSubscriptionData?.plan?.id === 'free'
+ {hasAccessToDiskModifications === false
? 'Disk size configuration is not available for projects on the Free Plan'
: 'Disk size configuration is only available when the spend cap has been disabled'}
- {projectSubscriptionData?.plan?.id === 'free' ? (
+ {hasAccessToDiskModifications === false ? (
If you are intending to use more than 500MB of disk space, then you will need to
upgrade to at least the Pro Plan.
@@ -205,11 +210,11 @@ const DiskSizeConfigurationModal = ({
- {projectSubscriptionData?.plan?.id === 'free'
+ {hasAccessToDiskModifications === false
? 'Upgrade subscription'
: 'Disable spend cap'}
diff --git a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx
index 32b76175d8a..3a54b3e8baa 100644
--- a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx
+++ b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx
@@ -1,17 +1,16 @@
-import { useRouter } from 'next/router'
-import { useEffect, useState } from 'react'
-import { toast } from 'sonner'
-
import { LOCAL_STORAGE_KEYS } from 'common'
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
import { TextConfirmModal } from 'components/ui/TextConfirmModalWrapper'
import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
-import { useProjectDeleteMutation } from 'data/projects/project-delete-mutation'
import type { OrgProject } from 'data/projects/org-projects-infinite-query'
+import { useProjectDeleteMutation } from 'data/projects/project-delete-mutation'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
import type { Organization } from 'types'
import { Input } from 'ui'
@@ -140,9 +139,7 @@ export const DeleteProjectModal = ({
{!isFree && (
<>
-
- Help us improve by sharing why you're deleting your project.
-
-
-
- ) : !isAWSProvider ? (
-
-
-
- Read replicas are only supported for projects provisioned via AWS
-
-
-
- Projects provisioned by other cloud providers currently will not be able to use read
- replicas
-
-
-
-
- ) : isAwsK8s ? (
-
-
-
- Read replicas are not supported for AWS (Revamped) projects
-
-
-
- Projects provisioned by other cloud providers currently will not be able to use read
- replicas
-
-
-
- ) : currentPgVersion < 15 ? (
-
-
-
- Read replicas can only be deployed with projects on Postgres version 15 and above
-
-
- If you'd like to use read replicas, please contact us via support
-
-
-
-
- Contact support
-
-
-
-
- ) : !isMinimallyOnSmallCompute ? (
-
-
-
- Project required to at least be on a Small compute
-
-
-
- This is to ensure that read replicas can keep up with the primary databases'
- activities.
-
-
-
-
- ) : !isWalgEnabled ? (
-
-
-
- {refetchInterval !== false
- ? 'Physical backups are currently being enabled'
- : 'Physical backups are required to deploy replicas'}
-
- {refetchInterval === false && (
-
- Physical backups are used under the hood to spin up read replicas for your project.
-
- )}
-
- {refetchInterval !== false
- ? 'This warning will go away once physical backups have been enabled - check back in a few minutes!'
- : 'Enabling physical backups will take a few minutes, after which you will be able to deploy read replicas.'}
-
- {refetchInterval !== false ? (
-
- You may start deploying read replicas thereafter once this is completed.
-
- ) : (
-
- {
- if (projectRef) enablePhysicalBackups({ ref: projectRef })
- }}
- >
- Enable physical backups
-
-
-
- )}
-
- ) : isProWithSpendCapEnabled ? (
-
-
-
- Spend cap needs to be disabled to deploy replicas
-
-
-
- Launching a replica incurs additional disk size that will exceed the plan's quota.
- Disable the spend cap first to allow overages before launching a replica.
-
-
-
-
- Disable spend cap
-
-
-
-
-
- ) : reachedMaxReplicas ? (
-
-
-
- You can only deploy up to {maxNumberOfReplicas} read replicas at once
-
-
- If you'd like to spin up another read replica, please drop an existing replica first.
-
- {maxNumberOfReplicas === MAX_REPLICAS_BELOW_XL && (
- <>
-
-
- Alternatively, you may deploy up to{' '}
- {MAX_REPLICAS_ABOVE_XL} replicas if
- your project is on an XL compute or higher.
-
-
- New replica will cost an additional{' '}
-
- {formatCurrency(
- estComputeMonthlyCost +
- additionalCostDiskSize +
- Number(additionalCostIOPS) +
- Number(additionalCostThroughput)
- )}
- /month
-
-
-
-
-
-
- Read replicas will match the compute size of your primary database and will
- include 25% more disk size than the primary database to accommodate WAL files.
-
-
- The additional cost for the replica breaks down to:
-
- Read replicas will be on the same compute size as your primary database. Deploying a
- read replica on the{' '}
- {selectedComputeMeta?.name} size incurs
- additional{' '}
-
- {selectedComputeMeta?.price_description}
-
- .
-
- )}
-
-
- Read more about{' '}
-
- billing
- {' '}
- for read replicas.
-
Create a Supabase account with the same email where you got our post-event note
+
Load data into a Supabase database
+
Complete these steps by Monday, May 4, 2026 at 12:00 PM PST
+
+
+ Create your account
+
+
+ No purchase necessary. Void where prohibited.{' '}
+
+ Official rules
+
+ .
+
+
+ ),
+ },
+ ],
+}
+
+export default page
diff --git a/apps/www/_go/events/postgresconf-sjc-2026/contest-thank-you.tsx b/apps/www/_go/events/postgresconf-sjc-2026/contest-thank-you.tsx
new file mode 100644
index 00000000000..bee266b16c6
--- /dev/null
+++ b/apps/www/_go/events/postgresconf-sjc-2026/contest-thank-you.tsx
@@ -0,0 +1,37 @@
+import type { GoPageInput } from 'marketing'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+const page: GoPageInput = {
+ template: 'thank-you',
+ slug: 'postgresconf-sjc-2026/contest/thank-you',
+ metadata: {
+ title: "You're entered | Supabase at PostgresConf San Jose 2026",
+ description:
+ 'Thanks for entering the Supabase contest at PostgresConf San Jose 2026. Good luck!',
+ },
+ hero: {
+ title: 'Thanks for entering',
+ description:
+ "Your contest entry is confirmed. Make sure you've created a Supabase account and loaded data before Monday, May 4, 2026 at 12:00 PM PST. We'll reach out to the winner by email.",
+ },
+ sections: [
+ {
+ type: 'single-column',
+ title: 'Get started with Supabase',
+ description: "If you haven't already, create your account and start building.",
+ children: (
+
+
+ Go to dashboard
+
+
+ Visit supabase.com
+
+
+ ),
+ },
+ ],
+}
+
+export default page
diff --git a/apps/www/_go/events/postgresconf-sjc-2026/contest.tsx b/apps/www/_go/events/postgresconf-sjc-2026/contest.tsx
new file mode 100644
index 00000000000..db27a18902d
--- /dev/null
+++ b/apps/www/_go/events/postgresconf-sjc-2026/contest.tsx
@@ -0,0 +1,128 @@
+import type { GoPageInput } from 'marketing'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+const page: GoPageInput = {
+ template: 'lead-gen',
+ slug: 'postgresconf-sjc-2026/contest',
+ metadata: {
+ title: 'Win a Mac Mini | Supabase at PostgresConf San Jose 2026',
+ description:
+ 'Sign up for Supabase and enter the contest for a chance to win a Mac Mini. PostgresConf San Jose 2026.',
+ },
+ hero: {
+ title: 'Win a Mac Mini',
+ subtitle: 'Supabase at PostgresConf San Jose 2026',
+ description:
+ 'Supabase is Postgres with batteries included -- auth, storage, edge functions, vectors, and real-time, all built on top of the database you already know. Sign up, load some data, and enter below for a chance to win a Mac Mini.',
+ image: {
+ src: '/images/landing-pages/postgresconf-sjc-2026/mac-mini.png',
+ alt: 'Apple Mac Mini',
+ width: 400,
+ height: 400,
+ },
+ ctas: [
+ {
+ label: 'Get started',
+ href: '#how-to-enter',
+ variant: 'primary',
+ },
+ ],
+ },
+ sections: [
+ {
+ type: 'single-column',
+ id: 'how-to-enter',
+ title: 'How to enter',
+ children: (
+
+
+
Create a Supabase account and note the email address you used
+
Load data into a Supabase database
+
Fill out the entry form below
+
Complete these steps by Monday, May 4, 2026 at 12:00 PM PST
+
+
+ Create your account
+
+
+ No purchase necessary. Void where prohibited.{' '}
+
+ Official rules
+
+ .
+
+
+ ),
+ },
+ {
+ type: 'form',
+ id: 'enter-contest',
+ title: 'Enter the contest',
+ description: 'Fill out the form below to complete your entry.',
+ fields: [
+ {
+ type: 'text',
+ name: 'first_name',
+ label: 'First Name',
+ placeholder: 'First Name',
+ required: true,
+ half: true,
+ },
+ {
+ type: 'text',
+ name: 'last_name',
+ label: 'Last Name',
+ placeholder: 'Last Name',
+ required: true,
+ half: true,
+ },
+ {
+ type: 'email',
+ name: 'email_address',
+ label: 'Email',
+ placeholder: 'Email address',
+ required: true,
+ },
+ {
+ type: 'text',
+ name: 'company_name',
+ label: 'Company',
+ placeholder: 'Company name',
+ required: true,
+ },
+ ],
+ submitLabel: 'Enter contest',
+ successRedirect: '/go/postgresconf-sjc-2026/contest/thank-you',
+ disclaimer:
+ 'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy) and the [Official Rules](/go/contest-rules).',
+ crm: {
+ hubspot: {
+ formGuid: '1f508323-bd39-497d-b4bf-4978a50d9248',
+ fieldMap: {
+ first_name: 'firstname',
+ last_name: 'lastname',
+ email_address: 'email',
+ company_name: 'company',
+ },
+ consent:
+ 'By submitting this form, I confirm that I have read and understood the Privacy Policy.',
+ },
+ customerio: {
+ event: 'event_attended',
+ profileMap: {
+ email_address: 'email',
+ first_name: 'first_name',
+ last_name: 'last_name',
+ company_name: 'company_name',
+ },
+ staticProperties: {
+ event_name: 'PostgresConf San Jose 2026',
+ },
+ },
+ },
+ },
+ ],
+}
+
+export default page
diff --git a/apps/www/_go/events/startup-grind-2026/contest.tsx b/apps/www/_go/events/startup-grind-2026/contest.tsx
new file mode 100644
index 00000000000..09bb47c022d
--- /dev/null
+++ b/apps/www/_go/events/startup-grind-2026/contest.tsx
@@ -0,0 +1,63 @@
+import type { GoPageInput } from 'marketing'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+const page: GoPageInput = {
+ template: 'lead-gen',
+ slug: 'startup-grind-2026/contest',
+ metadata: {
+ title: 'Win an iPhone 17 Pro Max | Supabase at Startup Grind 2026',
+ description:
+ 'Create a Supabase account and load data for a chance to win an iPhone 17 Pro Max. Startup Grind 2026.',
+ },
+ hero: {
+ title: 'Win an iPhone 17 Pro Max',
+ subtitle: 'Supabase at Startup Grind 2026',
+ description:
+ 'Great meeting you at Startup Grind. Supabase gives you Postgres with auth, storage, edge functions, and real-time -- everything you need to ship your product faster. Try it out and you could win an iPhone 17 Pro Max.',
+ image: {
+ src: '/images/landing-pages/stripe-sessions/iphone17-pro-max.png',
+ alt: 'Orange iPhone 17 Pro Max',
+ width: 400,
+ height: 500,
+ },
+ ctas: [
+ {
+ label: 'Get started',
+ href: '#how-to-enter',
+ variant: 'primary',
+ },
+ ],
+ },
+ sections: [
+ {
+ type: 'single-column',
+ id: 'how-to-enter',
+ title: 'How to enter',
+ children: (
+
+
+
+ Create a Supabase account with the same email address where you got our post-event
+ note
+
+
Load data into a Supabase database
+
Complete these steps by Monday, May 11, 2026 at 12:00 PM PST
+
+
+ Create your account
+
+
+ No purchase necessary. Void where prohibited.{' '}
+
+ Official rules
+
+ .
+
+
+ ),
+ },
+ ],
+}
+
+export default page
diff --git a/apps/www/_go/events/stripe-sessions-2026/contest.tsx b/apps/www/_go/events/stripe-sessions-2026/contest.tsx
new file mode 100644
index 00000000000..21432184a0c
--- /dev/null
+++ b/apps/www/_go/events/stripe-sessions-2026/contest.tsx
@@ -0,0 +1,63 @@
+import type { GoPageInput } from 'marketing'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+const page: GoPageInput = {
+ template: 'lead-gen',
+ slug: 'stripe/contest',
+ metadata: {
+ title: 'Win an iPhone 17 Pro Max | Supabase at Stripe Sessions',
+ description:
+ 'Create a Supabase account and load data for a 1-in-10 chance to win an iPhone 17 Pro Max. Stripe Sessions 2026.',
+ },
+ hero: {
+ title: 'Win an iPhone 17 Pro Max',
+ subtitle: 'Supabase at Stripe Sessions 2026',
+ description:
+ "Great meeting you at Stripe Sessions. Try Supabase if you haven't already -- it's Postgres with all the tools you need to build AI-native applications. We're running a sweepstakes and you have a 1-in-10 chance of winning. Those are better odds than anywhere else!",
+ image: {
+ src: '/images/landing-pages/stripe-sessions/iphone17-pro-max.png',
+ alt: 'Orange iPhone 17 Pro Max',
+ width: 400,
+ height: 500,
+ },
+ ctas: [
+ {
+ label: 'Get started',
+ href: '#how-to-enter',
+ variant: 'primary',
+ },
+ ],
+ },
+ sections: [
+ {
+ type: 'single-column',
+ id: 'how-to-enter',
+ title: 'How to enter',
+ children: (
+
+
+
+ Create a Supabase account with the same email address where you got our post-event
+ note
+
+
Load data into a Supabase database
+
Complete these steps by Monday, May 11, 2026 at 12:00 PM PST
+
+
+ Create your account
+
+
+ No purchase necessary. Void where prohibited.{' '}
+
+ Official rules
+
+ .
+
+
+ ),
+ },
+ ],
+}
+
+export default page
diff --git a/apps/www/_go/events/stripe-sessions-2026/exec-dinner-thank-you.tsx b/apps/www/_go/events/stripe-sessions-2026/exec-dinner-thank-you.tsx
new file mode 100644
index 00000000000..073bfb5ee40
--- /dev/null
+++ b/apps/www/_go/events/stripe-sessions-2026/exec-dinner-thank-you.tsx
@@ -0,0 +1,34 @@
+import type { GoPageInput } from 'marketing'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+const page: GoPageInput = {
+ template: 'thank-you',
+ slug: 'stripe/exec-dinner/thank-you',
+ metadata: {
+ title: "You're confirmed | Supabase Executive Dinner",
+ description:
+ 'Your RSVP for the Supabase executive dinner at Spruce on April 29, 2026 has been confirmed.',
+ },
+ hero: {
+ title: "You're confirmed",
+ description:
+ "We'll send details and directions closer to the date. We look forward to seeing you at Spruce on April 29.",
+ },
+ sections: [
+ {
+ type: 'single-column',
+ title: 'In the meantime',
+ description: 'Learn more about what we are building at Supabase.',
+ children: (
+
+
+ Visit supabase.com
+
+
+ ),
+ },
+ ],
+}
+
+export default page
diff --git a/apps/www/_go/events/stripe-sessions-2026/exec-dinner.tsx b/apps/www/_go/events/stripe-sessions-2026/exec-dinner.tsx
new file mode 100644
index 00000000000..a0a2a22a0c0
--- /dev/null
+++ b/apps/www/_go/events/stripe-sessions-2026/exec-dinner.tsx
@@ -0,0 +1,134 @@
+import type { GoPageInput } from 'marketing'
+
+const page: GoPageInput = {
+ template: 'lead-gen',
+ slug: 'stripe/exec-dinner',
+ metadata: {
+ title: 'Executive Dinner: The Future of Scalable Databases | Supabase',
+ description:
+ 'Join Supabase leaders for an intimate dinner exploring what comes next for Postgres at scale. April 29, 2026 at Spruce, San Francisco.',
+ },
+ hero: {
+ title: 'The future of scalable databases',
+ subtitle: 'An intimate executive dinner hosted by Supabase',
+ description:
+ 'Join Supabase product and engineering leaders for a dinner conversation about where Postgres is headed -- from scaling beyond single-node limits to managing globally distributed workloads. Expect sharp perspectives, good food, and the opportunity to connect with other engineering leaders.',
+ ctas: [
+ {
+ label: 'Reserve your seat',
+ href: '#rsvp',
+ variant: 'primary',
+ },
+ ],
+ },
+ sections: [
+ {
+ type: 'single-column',
+ title: 'Details',
+ children: (
+