diff --git a/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx b/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx index f0c72e677a785..ff4b76fe86e1f 100644 --- a/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx +++ b/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx @@ -38,8 +38,8 @@ We'll be saving the Postgres documentation in Postgres, and ChatGPT will be retr dark: '/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--dark.png', }} - width={1196} - height={1194} +width={1196} +height={1194} /> ### Step 1: Fork the ChatGPT Retrieval Plugin repository 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 95ecc5a595ec1..b65a671d19888 100644 --- a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx +++ b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx @@ -491,6 +491,22 @@ Store the client secret securely. It will only be shown once. If you lose it, yo +#### Token endpoint authentication method + +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 | + +**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)). + +**Constraints:** Public clients must use `none`. Confidential clients cannot use `none`. + +You can set this when registering a client via the dashboard or programmatically. See [OAuth Flows](/docs/guides/auth/oauth-server/oauth-flows#step-5-token-exchange) for examples of each method in action. + @@ -518,6 +534,8 @@ const { data, error } = await supabase.auth.admin.oauth.createClient({ name: 'My Third-Party App', redirect_uris: ['https://my-app.com/auth/callback', 'https://my-app.com/auth/silent-callback'], client_type: 'confidential', + // Optional: defaults to 'client_secret_basic' for confidential, 'none' for public + token_endpoint_auth_method: 'client_secret_basic', }) if (error) { @@ -548,7 +566,9 @@ response = supabase.auth.admin.oauth.create_client({ 'https://my-app.com/auth/callback', 'https://my-app.com/auth/silent-callback' ], - 'client_type': 'confidential' + 'client_type': 'confidential', + # Optional: defaults to 'client_secret_basic' for confidential, 'none' for public + 'token_endpoint_auth_method': 'client_secret_basic' }) print('Client created:', response) @@ -572,7 +592,8 @@ curl -X POST 'https://.supabase.co/auth/v1/admin/oauth/clients' \ "https://my-app.com/auth/callback", "https://my-app.com/auth/silent-callback" ], - "client_type": "confidential" + "client_type": "confidential", + "token_endpoint_auth_method": "client_secret_basic" }' ``` @@ -585,7 +606,8 @@ curl -X POST 'http://localhost:54321/auth/v1/admin/oauth/clients' \ -d '{ "name": "Local Dev App", "redirect_uris": ["http://localhost:3000/auth/callback"], - "client_type": "confidential" + "client_type": "confidential", + "token_endpoint_auth_method": "client_secret_post" }' ``` @@ -598,6 +620,7 @@ Response: "name": "My Third-Party App", "redirect_uris": ["https://my-app.com/auth/callback", "https://my-app.com/auth/silent-callback"], "client_type": "confidential", + "token_endpoint_auth_method": "client_secret_basic", "created_at": "2025-01-15T10:30:00.000Z" } ``` diff --git a/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx b/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx index 028634e88c7dc..ec39a9588435b 100644 --- a/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx +++ b/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx @@ -230,7 +230,11 @@ The error parameters allow clients to display relevant error messages to users: ### Step 5: Token exchange -The client exchanges the authorization code for tokens by making a POST request to the token endpoint: +The client exchanges the authorization code for tokens by making a POST request to the token endpoint. How the client authenticates depends on its `token_endpoint_auth_method` (set during [client registration](/docs/guides/auth/oauth-server/getting-started#token-endpoint-authentication-method)). + +#### Public clients (`token_endpoint_auth_method: none`) + +Public clients send only the `client_id` in the request body with no secret: ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -242,7 +246,29 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ -d 'code_verifier=' ``` -For confidential clients (with client secret): +#### Confidential clients (`token_endpoint_auth_method: client_secret_basic`) + +This is the **default** for confidential clients. Credentials are sent via the `Authorization` header using HTTP Basic authentication (base64-encoded `client_id:client_secret`): + +```bash +curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -u ':' \ + -d 'grant_type=authorization_code' \ + -d 'code=' \ + -d 'redirect_uri=' \ + -d 'code_verifier=' +``` + + + +The `-u` flag in cURL automatically encodes the credentials and sets the `Authorization: Basic ` header. If you're not using cURL, you must base64-encode the `client_id:client_secret` string yourself. + + + +#### Confidential clients (`token_endpoint_auth_method: client_secret_post`) + +Credentials are sent as form parameters in the request body: ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -261,7 +287,37 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ // Retrieve the code verifier from storage const codeVerifier = sessionStorage.getItem('code_verifier') -// Exchange code for tokens +// --- Public clients (token_endpoint_auth_method: none) --- +const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: authorizationCode, + client_id: '', + redirect_uri: '', + code_verifier: codeVerifier, + }), +}) + +// --- Confidential clients (token_endpoint_auth_method: client_secret_basic) --- +const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Basic ' + btoa(':'), + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: '', + code_verifier: codeVerifier, + }), +}) + +// --- Confidential clients (token_endpoint_auth_method: client_secret_post) --- const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { method: 'POST', headers: { @@ -271,6 +327,7 @@ const response = await fetch(`https://.supabase.co/auth/v1/oauth/to grant_type: 'authorization_code', code: authorizationCode, client_id: '', + client_secret: '', redirect_uri: '', code_verifier: codeVerifier, }), @@ -377,7 +434,9 @@ Clients should refresh access tokens when: ### Refresh request -Make a POST request to the token endpoint with the refresh token: +Make a POST request to the token endpoint with the refresh token. The client authenticates the same way as during the [token exchange](#step-5-token-exchange), based on its `token_endpoint_auth_method`. + +#### Public clients (`token_endpoint_auth_method: none`) ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -387,7 +446,17 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ -d 'client_id=' ``` -For confidential clients: +#### Confidential clients (`token_endpoint_auth_method: client_secret_basic`) + +```bash +curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -u ':' \ + -d 'grant_type=refresh_token' \ + -d 'refresh_token=' +``` + +#### Confidential clients (`token_endpoint_auth_method: client_secret_post`) ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -401,6 +470,7 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ #### Example in JavaScript ```javascript +// Public clients (token_endpoint_auth_method: none) async function refreshAccessToken(refreshToken) { const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { method: 'POST', @@ -420,6 +490,27 @@ async function refreshAccessToken(refreshToken) { return await response.json() } + +// Confidential clients (token_endpoint_auth_method: client_secret_basic) +async function refreshAccessTokenConfidential(refreshToken) { + const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Basic ' + btoa(':'), + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }), + }) + + if (!response.ok) { + throw new Error('Failed to refresh token') + } + + return await response.json() +} ``` ### Refresh response diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index dd263a17e989a..dfd3aebff5cca 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -27,7 +27,7 @@ Supabase Auth needs a few scopes granting access to profile data of your end use - `openid` (add manually) - `.../auth/userinfo.email` (added by default) -- `...auth/userinfo.profile` (added by default) +- `.../auth/userinfo.profile` (added by default) If you add more scopes, especially those on the sensitive or restricted list your application might be subject to verification which may take a long time. diff --git a/apps/docs/content/guides/database/connection-management.mdx b/apps/docs/content/guides/database/connection-management.mdx index 9d8f9433688b7..62a011328fbac 100644 --- a/apps/docs/content/guides/database/connection-management.mdx +++ b/apps/docs/content/guides/database/connection-management.mdx @@ -28,12 +28,12 @@ These numbers are generalizations and depends on other Supabase products that yo Database client connections chart For Teams and Enterprise plans, Supabase provides Advanced Telemetry charts directly within the Dashboard. The `Database client connections` chart displays historical connection data broken down by connection type: diff --git a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx index acf82ca2796ff..5695f1aa00755 100644 --- a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx +++ b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx @@ -16,12 +16,12 @@ Wrappers introduce some new terminology and different workflows. Foreign Data Wrappers (FDW) ### Remote servers @@ -117,12 +117,12 @@ select cron.schedule( FDW with pg_cron This process can be taxing on your database if you are moving large amounts of data. Often, it's better to use an external tool for batch ETL, such as [Fivetran](https://fivetran.com/) or [Airbyte](https://airbyte.io/). diff --git a/apps/docs/content/guides/database/orioledb.mdx b/apps/docs/content/guides/database/orioledb.mdx index ab1e354f83711..ba6788ca40d6a 100644 --- a/apps/docs/content/guides/database/orioledb.mdx +++ b/apps/docs/content/guides/database/orioledb.mdx @@ -56,8 +56,8 @@ To get started with OrioleDB you need to [create a new Supabase project](/dashbo }} className="max-w-[550px] !mx-auto border rounded-md" - width={1376} - height={2034} +width={1376} +height={2034} /> ### Creating tables diff --git a/apps/docs/content/guides/database/postgres/indexes.mdx b/apps/docs/content/guides/database/postgres/indexes.mdx index d14f0b5a14ad1..69e8e668e0fe1 100644 --- a/apps/docs/content/guides/database/postgres/indexes.mdx +++ b/apps/docs/content/guides/database/postgres/indexes.mdx @@ -72,12 +72,12 @@ Here is a simplified diagram of the index we just created (note that in practice B-Tree index example in Postgres You can see that in any large data set, traversing the index to locate a given value can be done in much less operations (O(log n)) than compared to scanning the table one value at a time from top to bottom (O(n)). diff --git a/apps/docs/content/guides/database/replication/replication-monitoring.mdx b/apps/docs/content/guides/database/replication/replication-monitoring.mdx index 4b63cd0c95dbe..1854ead49e456 100644 --- a/apps/docs/content/guides/database/replication/replication-monitoring.mdx +++ b/apps/docs/content/guides/database/replication/replication-monitoring.mdx @@ -26,8 +26,8 @@ To monitor your replication pipelines: alt="Destinations List" src="/docs/img/database/replication/replication-destinations-list.png" - width={3560} - height={2146} +width={3560} +height={2146} /> #### Pipeline states @@ -50,8 +50,8 @@ For detailed information about a specific pipeline, click **View status** on the alt="Pipeline Status View" src="/docs/img/database/replication/replication-view-status.png" - width={3560} - height={2146} +width={3560} +height={2146} /> #### Replication lag metrics @@ -82,8 +82,8 @@ Table errors occur during the copy phase and affect individual tables. These err alt="Table Error Details" src="/docs/img/database/replication/replication-pipeline-table-error.png" - width={3560} - height={2146} +width={3560} +height={2146} /> **Viewing table error details:** @@ -110,8 +110,8 @@ When a pipeline error occurs, you'll receive an email notification immediately. alt="Pipeline Error Details" src="/docs/img/database/replication/replication-pipeline-error.png" - width={3560} - height={2146} +width={3560} +height={2146} /> **Viewing pipeline error details:** diff --git a/apps/docs/content/guides/database/replication/replication-setup.mdx b/apps/docs/content/guides/database/replication/replication-setup.mdx index 372b093bb69e5..0c9ff36b2fdaa 100644 --- a/apps/docs/content/guides/database/replication/replication-setup.mdx +++ b/apps/docs/content/guides/database/replication/replication-setup.mdx @@ -107,8 +107,8 @@ Before adding destinations, you need to enable replication for your project: alt="Enable Replication" src="/docs/img/database/replication/replication-enable-replication.png" - width={3560} - height={2146} +width={3560} +height={2146} /> ### Step 3: Add a destination @@ -139,23 +139,21 @@ First, create an analytics bucket to store your replicated data: 1. Navigate to [Storage](/dashboard/project/_/storage/buckets) → **Analytics** in your Supabase Dashboard 2. Click **New bucket** - Create new analytics bucket + Create new analytics bucket 3. Fill in the bucket details: - Analytics bucket details + Analytics bucket details - **Name**: A unique name for your bucket (e.g., `analytics_warehouse`) - **Region**: Select the region where your data will be stored @@ -172,13 +170,12 @@ First, create an analytics bucket to store your replicated data: 1. Navigate to [Database](/dashboard/project/_/database/replication) → **replication** in your Supabase Dashboard 2. Click **Add destination** - Add Destination + Add Destination 3. Configure the general settings: @@ -268,13 +265,12 @@ Before configuring BigQuery as a destination, set up the following in Google Clo 1. Navigate to [Database](/dashboard/project/_/database/replication) → **replication** in your Supabase Dashboard 2. Click **Add destination** - BigQuery Configuration Settings + BigQuery Configuration Settings 3. Configure the general settings: diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index 9483a832861ae..5af6671059c1a 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -30,12 +30,12 @@ When creating a table, it's best practice to add columns at the same time. Tables and columns You must define the "data type" of each column when it is created. You can add and remove columns at any time after creating a table. @@ -346,12 +346,12 @@ Tables can be "joined" together using Foreign Keys. Foreign Keys This is where the "Relational" naming comes from, as data typically forms some sort of relationship. @@ -426,12 +426,12 @@ Tables belong to `schemas`. Schemas are a way of organizing your tables, often f Schemas and tables If you don't explicitly pass a schema when creating a table, Postgres will assume that you want to create the table in the `public` schema. diff --git a/apps/docs/content/guides/deployment/branching/github-integration.mdx b/apps/docs/content/guides/deployment/branching/github-integration.mdx index d0af319e83910..ab1969960ed46 100644 --- a/apps/docs/content/guides/deployment/branching/github-integration.mdx +++ b/apps/docs/content/guides/deployment/branching/github-integration.mdx @@ -113,12 +113,12 @@ We highly recommend turning on a 'required check' for the Supabase integration. Check the "Require status checks to pass before merging" option. ### Email notifications diff --git a/apps/docs/content/guides/deployment/managing-environments.mdx b/apps/docs/content/guides/deployment/managing-environments.mdx index a992babfa332c..04213b6b00043 100644 --- a/apps/docs/content/guides/deployment/managing-environments.mdx +++ b/apps/docs/content/guides/deployment/managing-environments.mdx @@ -16,8 +16,8 @@ This guide shows you how to set up your local Supabase development environment t dark: '/docs/img/local-dev-environment.svg', }} - width={1600} - height={933} +width={1600} +height={933} /> ## Set up a local environment diff --git a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx index 10d91f4a8cea6..adc1ec62ebd47 100644 --- a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx +++ b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx @@ -13,8 +13,8 @@ Running databases is a shared responsibility between you and Supabase. There are dark: '/docs/img/platform/shared-responsibility-model--dark.png', }} - width={1674} - height={1405} +width={1674} +height={1405} /> To summarize, you are always responsible for: diff --git a/apps/docs/content/guides/functions/quickstart-dashboard.mdx b/apps/docs/content/guides/functions/quickstart-dashboard.mdx index 1cea338dd5ef5..1a6cbf3b4c367 100644 --- a/apps/docs/content/guides/functions/quickstart-dashboard.mdx +++ b/apps/docs/content/guides/functions/quickstart-dashboard.mdx @@ -46,9 +46,8 @@ Click the **"Deploy a new function"** button and select **"Via Editor"** to crea dark: '/docs/img/guides/functions/dashboard/create-edge-function--dark.png', }} - - width={2338} - height={926} +width={2338} +height={926} /> @@ -72,9 +71,8 @@ The dashboard will load your chosen template in the code editor. Here's what the dark: '/docs/img/guides/functions/dashboard/edge-function-template--dark.png', }} - - width={2430} - height={1248} +width={2430} +height={1248} /> If needed, you can modify this code directly in the browser editor. The function accepts a JSON payload with a `name` field and returns a greeting message. @@ -116,9 +114,8 @@ Click **"Send Request"** to test your function. dark: '/docs/img/guides/functions/dashboard/edge-function-test--dark.png', }} - - width={2430} - height={1350} +width={2430} +height={1350} /> In this example, we successfully tested our Hello World function by sending a JSON payload with a name field, and received the expected greeting message back. @@ -212,9 +209,8 @@ Go to your project > **Deploy a new function** > **Via AI Assistant**. dark: '/docs/img/guides/functions/dashboard/create-ai-edge-function--dark.png', }} - - width={2604} - height={926} +width={2604} +height={926} /> Describe what you want your function to do in the prompt @@ -226,9 +222,8 @@ Describe what you want your function to do in the prompt dark: '/docs/img/guides/functions/dashboard/ai-edge-function--dark.png', }} - - width={2768} - height={1836} +width={2768} +height={1836} /> Click **Deploy** and the Assistant will create and deploy the function for you. diff --git a/apps/docs/content/guides/functions/secrets.mdx b/apps/docs/content/guides/functions/secrets.mdx index 00b6121909446..3896efd2f9dda 100644 --- a/apps/docs/content/guides/functions/secrets.mdx +++ b/apps/docs/content/guides/functions/secrets.mdx @@ -105,8 +105,8 @@ You will also need to set secrets for your production Edge Functions. You can do dark: '/docs/img/edge-functions-secrets.jpg', }} - width={3757} - height={1525} +width={3757} +height={1525} /> Note that you can paste multiple secrets at a time. diff --git a/apps/docs/content/guides/getting-started/architecture.mdx b/apps/docs/content/guides/getting-started/architecture.mdx index c123e65741f12..c3ff93e152cf4 100644 --- a/apps/docs/content/guides/getting-started/architecture.mdx +++ b/apps/docs/content/guides/getting-started/architecture.mdx @@ -26,8 +26,8 @@ Each Supabase project consists of several tools: light: '/docs/img/supabase-architecture--light.svg', }} - width={1600} - height={767} +width={1600} +height={767} /> ### Postgres (database) diff --git a/apps/docs/content/guides/local-development/cli/getting-started.mdx b/apps/docs/content/guides/local-development/cli/getting-started.mdx index 246c90b65d792..1597264bcd9a3 100644 --- a/apps/docs/content/guides/local-development/cli/getting-started.mdx +++ b/apps/docs/content/guides/local-development/cli/getting-started.mdx @@ -174,9 +174,8 @@ The Supabase CLI uses Docker containers to manage the local development stack. F light: '/docs/img/guides/cli/docker-mac-light.png', }} - - width={2880} - height={1800} +width={2880} +height={1800} /> @@ -189,9 +188,8 @@ The Supabase CLI uses Docker containers to manage the local development stack. F light: '/docs/img/guides/cli/docker-win-light.png', }} - - width={2560} - height={1520} +width={2560} +height={1520} /> diff --git a/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx b/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx index 369f8ab904cdd..19b2224392325 100644 --- a/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx +++ b/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx @@ -17,11 +17,11 @@ First, download your project's backup file from dashboard and identify its backu Project Paused: 90 Days Remaining ## Restoring your backup diff --git a/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx b/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx index dd4784f380c89..b6945c6c0b17e 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx @@ -14,9 +14,8 @@ An AWS Marketplace subscription is linked to exactly one Supabase organization. light: '/docs/img/guides/platform/aws-marketplace-onboarding-page-extended--light.png', }} - - width={3040} - height={1312} +width={3040} +height={1312} /> ## Implications of linking a Supabase organization to a marketplace subscription diff --git a/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx b/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx index bd5eb285c636a..30b23d5403398 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx @@ -50,54 +50,54 @@ For more details on completing the setup and what it means to link an organizati Supabase purchase options on the AWS Marketplace - - - - - Click "Subscribe" at the bottom of the page. - Supabase product subscribe + + + +Click "Subscribe" at the bottom of the page. + - - - - - After the payment has been confirmed and your marketplace subscription is active, click "Set up your account" to be redirected to the Supabase platform. - Supabase product subscribe + + + +After the payment has been confirmed and your marketplace subscription is active, click "Set up your account" to be redirected to the Supabase platform. + - - - - - Complete the setup by linking a Supabase organization to the AWS Marketplace subscription. - Supabase product subscribe + + + +Complete the setup by linking a Supabase organization to the AWS Marketplace subscription. + - - + + + diff --git a/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx b/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx index 6f0b3c735b0fa..c924ea91ae0fa 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx @@ -11,9 +11,8 @@ You can view your invoices in the [AWS Billing and Cost Management console](http alt="Subscription upgrade modal" src="/docs/img/guides/platform/aws-marketplace-invoices.png" - - width={4544} - height={1714} +width={4544} +height={1714} /> ## What invoices you get from AWS diff --git a/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx b/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx index dfe011b58f3d3..66fe8a266c9f1 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx @@ -22,9 +22,8 @@ You can upgrade your plan at any time. The new plan will be active immediately, dark: '/docs/img/guides/platform/aws-marketplace-change-plan.png', }} - - width={2088} - height={2276} +width={2088} +height={2276} /> ### Downgrade @@ -48,9 +47,8 @@ If the downgrade causes you to exceed the [free projects limit](/docs/guides/pla dark: '/docs/img/guides/platform/aws-marketplace-configure-auto-renewal.png', }} - - width={2080} - height={2074} +width={2080} +height={2074} /> #### Downgrade to a paid plan diff --git a/apps/docs/content/guides/platform/billing-on-supabase.mdx b/apps/docs/content/guides/platform/billing-on-supabase.mdx index 529b967584034..a1bbd6e800e10 100644 --- a/apps/docs/content/guides/platform/billing-on-supabase.mdx +++ b/apps/docs/content/guides/platform/billing-on-supabase.mdx @@ -29,11 +29,9 @@ Different plans cannot be mixed within a single organization. For example, you c dark: '/docs/img/guides/platform/billing-overview.png', }} className="max-w-[600px] inline-block" - - - width={1600} - height={1475} -/> + width={1600} + height={1475} + /> ## Costs diff --git a/apps/docs/content/guides/platform/compute-and-disk.mdx b/apps/docs/content/guides/platform/compute-and-disk.mdx index 9463f6127cfd4..b0c5d466c40d8 100644 --- a/apps/docs/content/guides/platform/compute-and-disk.mdx +++ b/apps/docs/content/guides/platform/compute-and-disk.mdx @@ -44,10 +44,10 @@ Compute sizes can be changed by first selecting your project in the dashboard [h dark: '/docs/img/guides/platform/compute-size-selection--dark.png', }} - className="max-w-[500px]" +className="max-w-[500px]" - width={2122} - height={1302} +width={2122} +height={1302} /> We charge hourly for additional compute based on your usage. Read more about [usage-based billing for compute](/docs/guides/platform/manage-your-usage/compute). diff --git a/apps/docs/content/guides/platform/credits.mdx b/apps/docs/content/guides/platform/credits.mdx index 57d9516f066bc..942b39c4f4310 100644 --- a/apps/docs/content/guides/platform/credits.mdx +++ b/apps/docs/content/guides/platform/credits.mdx @@ -14,9 +14,8 @@ Each organization has a credit balance. Credits are applied to future invoices t dark: '/docs/img/guides/platform/credit-balance--dark.png', }} - - width={2034} - height={388} +width={2034} +height={388} /> You can find the credit balance on the [organization's billing page](/dashboard/org/_/billing). @@ -54,10 +53,10 @@ If you are interested in larger (> ) credit packages, [rea dark: '/docs/img/guides/platform/credit-top-up--dark.png', }} - className="max-w-[500px]" +className="max-w-[500px]" - width={1014} - height={758} +width={1014} +height={758} /> ## Credit FAQ diff --git a/apps/docs/content/guides/platform/manage-your-subscription.mdx b/apps/docs/content/guides/platform/manage-your-subscription.mdx index 8ce0e42d9f8b8..ac6b9a8d32cce 100644 --- a/apps/docs/content/guides/platform/manage-your-subscription.mdx +++ b/apps/docs/content/guides/platform/manage-your-subscription.mdx @@ -24,9 +24,8 @@ Upgrades take effect immediately. During the process, you are informed of the as }} className="max-w-[577px]" - - width={1536} - height={1116} +width={1536} +height={1116} /> If you still have credits in your account, we will use the credits first before charging your card. @@ -43,9 +42,8 @@ Downgrades take effect immediately. During the process, you are informed of the }} className="max-w-[577px]" - - width={2096} - height={1649} +width={2096} +height={1649} /> #### Credits upon downgrade diff --git a/apps/docs/content/guides/platform/manage-your-usage/branching.mdx b/apps/docs/content/guides/platform/manage-your-usage/branching.mdx index 20fde5a3aaaf5..774214977f346 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/branching.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/branching.mdx @@ -56,9 +56,8 @@ You can view Branching usage on the [organization's usage page](/dashboard/org/_ dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Usage Summary section, you can see how many hours your Preview branches existed during the selected time period. Hover over "Branching Compute Hours" for a detailed breakdown. @@ -70,9 +69,8 @@ In the Usage Summary section, you can see how many hours your Preview branches e dark: '/docs/img/guides/platform/usage-summary-branch-hours--dark.png', }} - - width={2258} - height={1360} +width={2258} +height={1360} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/compute.mdx b/apps/docs/content/guides/platform/manage-your-usage/compute.mdx index 2ca52ffb44c98..525b5ec5b33a4 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/compute.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/compute.mdx @@ -135,9 +135,8 @@ You can view Compute usage on the [organization's usage page](/dashboard/org/_/u dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Compute Hours section, you can see how many hours of a specific Compute size your projects have used during the selected time period. Hover over a specific date for a daily breakdown. @@ -149,9 +148,8 @@ In the Compute Hours section, you can see how many hours of a specific Compute s dark: '/docs/img/guides/platform/usage-compute--dark.png', }} - - width={2050} - height={810} +width={2050} +height={810} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx b/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx index 95af56393ab86..a3fad65268cc3 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx @@ -110,9 +110,8 @@ You can view Disk size usage on the [organization's usage page](/dashboard/org/_ dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Disk size section, you can see how much disk size your projects have provisioned. @@ -124,9 +123,8 @@ In the Disk size section, you can see how much disk size your projects have prov dark: '/docs/img/guides/platform/usage-disk-size--dark.png', }} - - width={2010} - height={670} +width={2010} +height={670} /> ### Disk size distribution diff --git a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx index 940b0ca34bf7e..94533e273b4fe 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx @@ -69,9 +69,8 @@ You can view Edge Function Invocations usage on the [organization's usage page]( dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Edge Function Invocations section, you can see how many invocations your projects have had during the selected time period. @@ -83,9 +82,8 @@ In the Edge Function Invocations section, you can see how many invocations your dark: '/docs/img/guides/platform/usage-function-invocations--dark.png', }} - - width={2008} - height={734} +width={2008} +height={734} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx index a4e2ac2d3bd7f..32c0197d645fb 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx @@ -122,9 +122,8 @@ You can view Egress usage on the [organization's usage page](/dashboard/org/_/us dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Total Egress section, you can see the usage for the selected time period. Hover over a specific date to view a breakdown by service. Note that this includes the cached egress. @@ -136,8 +135,8 @@ In the Total Egress section, you can see the usage for the selected time period. dark: '/docs/img/guides/platform/unified-egress.png', }} - width={803} - height={460} +width={803} +height={460} /> Separately, you can see the cached egress right below: @@ -149,8 +148,8 @@ Separately, you can see the cached egress right below: dark: '/docs/img/guides/platform/cached-egress.png', }} - width={1422} - height={586} +width={1422} +height={586} /> ### Custom report @@ -165,9 +164,8 @@ Separately, you can see the cached egress right below: dark: '/docs/img/guides/platform/egress-report--dark.png', }} - - width={2884} - height={948} +width={2884} +height={948} /> ## Debug usage @@ -185,9 +183,8 @@ On the Advisors [Query performance view](/dashboard/project/_/database/query-per dark: '/docs/img/guides/platform/advisor-most-frequent-queries--dark.png', }} - - width={5006} - height={510} +width={5006} +height={510} /> ### Most requested API endpoints @@ -201,9 +198,8 @@ In the [Logs Explorer](/dashboard/project/_/logs/explorer) you can access Edge L dark: '/docs/img/guides/platform/logs-top-paths--dark.png', }} - - width={4492} - height={1166} +width={4492} +height={1166} /> ## Optimize usage 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 bbf4e4da8b908..dac00f80c61ed 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 @@ -113,6 +113,6 @@ You can view Log Drain Events usage on the [organization's usage page](/dashboar dark: '/docs/img/guides/platform/usage-logdrain-events--dark.png', }} - width={2092} - height={762} +width={2092} +height={762} /> diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx index 2b740c2ff6d5b..3e35d94939af5 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx @@ -125,9 +125,8 @@ You can view Monthly Active SSO Users usage on the [organization's usage page](/ dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Monthly Active SSO Users section, you can see the usage for the selected time period. @@ -139,8 +138,8 @@ In the Monthly Active SSO Users section, you can see the usage for the selected dark: '/docs/img/guides/platform/usage-mau-sso--dark.png', }} - width={2034} - height={884} +width={2034} +height={884} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx index 348351e26bbfd..af9b98f14ca46 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx @@ -124,8 +124,8 @@ You can view Monthly Active Third-Party Users usage on the [organization's usage dark: '/docs/img/guides/platform/usage-mau-third-party--dark.png', }} - width={2040} - height={1040} +width={2040} +height={1040} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx index ac1fc876b304c..4e379d7a03fe8 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx @@ -113,9 +113,8 @@ You can view Monthly Active Users usage on the [organization's usage page](/dash dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Monthly Active Users section, you can see the usage for the selected time period. @@ -127,8 +126,8 @@ In the Monthly Active Users section, you can see the usage for the selected time dark: '/docs/img/guides/platform/usage-mau--dark.png', }} - width={2040} - height={878} +width={2040} +height={878} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx b/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx index c57ec32b3ce50..0126b42af9ece 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx @@ -75,9 +75,8 @@ You can view Realtime Messages usage on the [organization's usage page](/dashboa dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Realtime Messages section, you can see the usage for the selected time period. @@ -89,9 +88,8 @@ In the Realtime Messages section, you can see the usage for the selected time pe dark: '/docs/img/guides/platform/usage-realtime-messages--dark.png', }} - - width={2036} - height={760} +width={2036} +height={760} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx b/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx index c016f2c9bdb78..ce81990740424 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx @@ -80,9 +80,8 @@ You can view Realtime Peak Connections usage on the [organization's usage page]( dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Realtime Peak Connections section, you can see the usage for the selected time period. @@ -94,9 +93,8 @@ In the Realtime Peak Connections section, you can see the usage for the selected dark: '/docs/img/guides/platform/usage-realtime-peak-connections--dark.png', }} - - width={2046} - height={752} +width={2046} +height={752} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx index a97814777229c..6bdae59d65b5c 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx @@ -107,9 +107,8 @@ You can view Storage Image Transformations usage on the [organization's usage pa dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Storage Image Transformations section, you can see how many origin images were transformed during the selected time period. @@ -121,9 +120,8 @@ In the Storage Image Transformations section, you can see how many origin images dark: '/docs/img/guides/platform/usage-image-transformations--dark.png', }} - - width={2032} - height={848} +width={2032} +height={848} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx index c383f292ddb12..e8f6a32e5c31f 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx @@ -61,9 +61,8 @@ You can view Storage size usage on the [organization's usage page](/dashboard/or dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - - width={1546} - height={208} +width={1546} +height={208} /> In the Storage size section, you can see how much storage your projects have used during the selected time period. @@ -75,9 +74,8 @@ In the Storage size section, you can see how much storage your projects have use dark: '/docs/img/guides/platform/usage-storage-size--dark.png', }} - - width={2028} - height={882} +width={2028} +height={882} /> ### SQL Editor diff --git a/apps/docs/content/guides/platform/project-transfer.mdx b/apps/docs/content/guides/platform/project-transfer.mdx index e2ce46e3e4ef9..0ce7c2660f9cf 100644 --- a/apps/docs/content/guides/platform/project-transfer.mdx +++ b/apps/docs/content/guides/platform/project-transfer.mdx @@ -14,9 +14,8 @@ You can freely transfer projects between different organizations. Head to your [ }} className="max-w-[600px] !mx-auto border rounded-md" - - width={677} - height={224} +width={677} +height={224} /> Source organization - the organization the project currently belongs to diff --git a/apps/docs/content/guides/platform/read-replicas.mdx b/apps/docs/content/guides/platform/read-replicas.mdx index bdce1d5b6eb54..54ca456e81d17 100644 --- a/apps/docs/content/guides/platform/read-replicas.mdx +++ b/apps/docs/content/guides/platform/read-replicas.mdx @@ -15,8 +15,8 @@ Read Replicas are additional databases kept in sync with your Primary database. src="/docs/img/guides/platform/read-replicas/map-view.png?v=1" containerClassName="max-w-[700px] !mx-auto" - width={2000} - height={830} +width={2000} +height={830} /> ## About Read Replicas @@ -130,8 +130,8 @@ In the SQL editor, you can choose if you want to run the query on a particular R src="/docs/img/guides/platform/read-replicas/sql-editor.png?v=1" containerClassName="max-w-[700px]" - width={2024} - height={1048} +width={2024} +height={1048} /> ### Logging diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index a972449f6b1ea..3cdd941437670 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -81,10 +81,10 @@ During the 90-day restore window a paused project can be restored to the platfor Project Paused: 90 Days Remaining After the 90-day restore window, you can download your project's backup file, and Storage objects from the project dashboard. You can restore the data in the following ways: @@ -94,20 +94,20 @@ After the 90-day restore window, you can download your project's backup file, an Project Paused: 90 Days Remaining If you upgrade to a paid plan while your project is paused within the 90-day restore window, any expired one-click restore options are reenabled. Since the backup was taken outside the backwards compatibility window, it may fail to restore. If you have a problem restoring your backup after upgrading, contact [Support](/support). Project Paused: 90 Days Remaining ### Disk sizing diff --git a/apps/docs/content/guides/platform/your-monthly-invoice.mdx b/apps/docs/content/guides/platform/your-monthly-invoice.mdx index db213b260d739..78682a6a2f99b 100644 --- a/apps/docs/content/guides/platform/your-monthly-invoice.mdx +++ b/apps/docs/content/guides/platform/your-monthly-invoice.mdx @@ -38,9 +38,8 @@ The following invoice was issued on January 6, 2025 with the previous billing cy dark: '/docs/img/guides/platform/example-invoice.png', }} - - width={1690} - height={2192} +width={1690} +height={2192} /> 1. The final amount due diff --git a/apps/docs/content/guides/queues/quickstart.mdx b/apps/docs/content/guides/queues/quickstart.mdx index 85520f2c12541..95228840680a0 100644 --- a/apps/docs/content/guides/queues/quickstart.mdx +++ b/apps/docs/content/guides/queues/quickstart.mdx @@ -44,8 +44,8 @@ To get started, navigate to the [Supabase Queues](/dashboard/project/_/integrati light: '/docs/img/queues-quickstart-install.png', }} - width={2064} - height={1720} +width={2064} +height={1720} /> On the [Queues page](/dashboard/project/_/integrations/queues/queues): @@ -75,10 +75,10 @@ Queue names can only be lowercase and hyphens and underscores are permitted. light: '/docs/img/queues-quickstart-create.png', }} - className="max-w-lg !mx-auto" +className="max-w-lg !mx-auto" - width={1456} - height={1420} +width={1456} +height={1420} /> ### What happens when you create a queue? @@ -106,8 +106,8 @@ To get started, navigate to the Queues [Settings page](/dashboard/project/_/inte light: '/docs/img/queues-quickstart-settings.png', }} - width={2140} - height={1642} +width={2140} +height={1642} /> ### Enable RLS on your tables in `pgmq` schema @@ -123,8 +123,8 @@ You’ll want to create RLS policies for any Queues you want your client-side co light: '/docs/img/queues-quickstart-rls.png', }} - width={2130} - height={1508} +width={2130} +height={1508} /> ### Grant permissions to `pgmq_public` database functions @@ -148,8 +148,8 @@ To manage your queue permissions, click on the Queue Settings button. light: '/docs/img/queues-quickstart-queue-settings.png', }} - width={2150} - height={1192} +width={2150} +height={1192} /> Then enable the required roles permissions. @@ -161,8 +161,8 @@ Then enable the required roles permissions. light: '/docs/img/queues-quickstart-roles-light.png', }} - width={1271} - height={1315} +width={1271} +height={1315} /> diff --git a/apps/docs/content/guides/realtime/architecture.mdx b/apps/docs/content/guides/realtime/architecture.mdx index 875b6639b4fa5..dab8ff0f05153 100644 --- a/apps/docs/content/guides/realtime/architecture.mdx +++ b/apps/docs/content/guides/realtime/architecture.mdx @@ -16,8 +16,8 @@ Realtime is written in [Elixir](https://elixir-lang.org/), which compiles to [Er dark: '/docs/img/guides/platform/realtime/architecture--dark.png', }} - width={1990} - height={2226} +width={1990} +height={2226} /> ## Elixir & Phoenix diff --git a/apps/docs/content/guides/realtime/concepts.mdx b/apps/docs/content/guides/realtime/concepts.mdx index 2f4537308081f..25a9881e6c6ff 100644 --- a/apps/docs/content/guides/realtime/concepts.mdx +++ b/apps/docs/content/guides/realtime/concepts.mdx @@ -31,8 +31,6 @@ If you have a private channel and a public channel with the same topic name, Rea ## Database resources -Realtime uses several database connections to perform several operations. As a user, you are able to tune some of them using [Realtime Settings](/docs/guides/realtime/settings). - ### Database connections Realtime uses several database connections to perform various operations. You can configure some of these connections through [Realtime Settings](/docs/guides/realtime/settings). diff --git a/apps/docs/content/guides/realtime/reports.mdx b/apps/docs/content/guides/realtime/reports.mdx index 67996800ceb50..914fbde53b7a4 100644 --- a/apps/docs/content/guides/realtime/reports.mdx +++ b/apps/docs/content/guides/realtime/reports.mdx @@ -46,13 +46,13 @@ The report displays the total number of connected Realtime clients, showing how Connected Clients chart ### Actions you can take @@ -76,13 +76,13 @@ The report displays the total number of broadcast events sent by clients, showin Broadcast Events chart ### Actions you can take @@ -105,13 +105,13 @@ The report displays the total number of presence events sent by clients, showing Presence Events chart ### Actions you can take @@ -134,13 +134,13 @@ The report displays the total number of Postgres change events received by clien Postgres Changes Events chart ### Actions you can take @@ -164,13 +164,13 @@ The report displays the rate of channel joins per second, showing how frequently Rate of Channel Joins chart ### Actions you can take @@ -192,13 +192,13 @@ The report displays the median payload size in bytes, showing how message sizes Message Payload Size chart ### Actions you can take @@ -222,14 +222,14 @@ The report displays the median replication lag in milliseconds, showing the dela Broadcast from Database Replication Lag chart ### Actions you can take @@ -254,14 +254,14 @@ The report displays the median RLS execution time in milliseconds, showing how l (Read) Private Channel Subscription RLS Execution Time chart ### Actions you can take @@ -287,14 +287,14 @@ The report displays the median RLS execution time in milliseconds, showing how l (Write) Private Channel Subscription RLS Execution Time chart ### Actions you can take @@ -320,13 +320,13 @@ The report displays the total number of HTTP requests made to the Realtime servi Total Requests chart ### Actions you can take @@ -349,13 +349,13 @@ The report displays the total number of response errors from the Realtime API, s Response Errors chart ### Actions you can take @@ -382,13 +382,13 @@ The report displays the average response time in milliseconds, showing how quick Response Speed chart ### Actions you can take diff --git a/apps/docs/content/guides/realtime/settings.mdx b/apps/docs/content/guides/realtime/settings.mdx index 52a749f5843b8..8084aa391d949 100644 --- a/apps/docs/content/guides/realtime/settings.mdx +++ b/apps/docs/content/guides/realtime/settings.mdx @@ -19,9 +19,8 @@ All changes made in this screen will disconnect all your connected clients to en dark: '/docs/img/guides/platform/realtime/realtime-settings--dark.png', }} - - width={4600} - height={2600} +width={4600} +height={2600} /> You can set the following settings using the Realtime Settings screen in your Dashboard: diff --git a/apps/docs/content/guides/security/platform-audit-logs.mdx b/apps/docs/content/guides/security/platform-audit-logs.mdx index fbbc03aca1dd8..503ee81b9dea7 100644 --- a/apps/docs/content/guides/security/platform-audit-logs.mdx +++ b/apps/docs/content/guides/security/platform-audit-logs.mdx @@ -24,9 +24,8 @@ Platform Audit Logs can be found under your [organization's audit logs](/dashboa dark: '/docs/img/guides/security/platform-audit-logs--dark.png', }} - - width={2414} - height={958} +width={2414} +height={958} /> For each audit log, you can see additional details by clicking on the log entry: diff --git a/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx b/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx index 66996a9ae0578..de69594ebde64 100644 --- a/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx +++ b/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx @@ -25,8 +25,8 @@ The dashboard provides the easiest setup experience: alt="Query with PostgreSQL button on analytics bucket page" src="/docs/img/storage/query-analytics-with-postgres.png" - width={1860} - height={332} +width={1860} +height={332} /> 3. Enter the **Postgres schema** where you want to create the foreign tables. @@ -35,8 +35,8 @@ The dashboard provides the easiest setup experience: alt="Select destination PostgreSQL schema" src="/docs/img/storage/query-analytics-schema-name.png" - width={525} - height={297} +width={525} +height={297} /> 4. Click **Connect**. The wrapper is now configured. diff --git a/apps/docs/content/guides/storage/analytics/replication.mdx b/apps/docs/content/guides/storage/analytics/replication.mdx index 9e6f716b90bf3..30f26ab52b1fb 100644 --- a/apps/docs/content/guides/storage/analytics/replication.mdx +++ b/apps/docs/content/guides/storage/analytics/replication.mdx @@ -33,9 +33,8 @@ First, create a new analytics bucket to store your replicated data: alt="Creating a new analytics bucket" src="/docs/img/database/replication/replication-iceberg-new-bucket.png" - - width={3560} - height={2146} +width={3560} +height={2146} /> ### Step 2: Create a publication @@ -65,9 +64,8 @@ Now set up the pipeline to sync data to your analytics bucket: alt="Replication pipeline configuration" src="/docs/img/database/replication/replication-iceberg-details.png" - - width={3560} - height={2146} +width={3560} +height={2146} /> ## Monitoring your pipeline diff --git a/apps/docs/content/guides/telemetry/reports.mdx b/apps/docs/content/guides/telemetry/reports.mdx index d0be20636e9d2..5160b761a7529 100644 --- a/apps/docs/content/guides/telemetry/reports.mdx +++ b/apps/docs/content/guides/telemetry/reports.mdx @@ -59,13 +59,13 @@ The following charts provide a more advanced and detailed view of your database Memory usage chart | Component | Description | @@ -96,13 +96,13 @@ Actions you can take: CPU usage chart | Category | Description | @@ -135,13 +135,13 @@ Actions you can take: Disk IOPS chart This chart displays read and write IOPS with a reference line showing your compute size's maximum IOPS capacity. @@ -190,13 +190,13 @@ Actions you can take: Disk Size chart | Component | Description | @@ -227,13 +227,13 @@ Actions you can take: Database connections chart | Connection Type | Description | diff --git a/apps/docs/middleware.ts b/apps/docs/middleware.ts index f4812f9e6201b..ea55aafa52efc 100644 --- a/apps/docs/middleware.ts +++ b/apps/docs/middleware.ts @@ -1,23 +1,17 @@ -import { clientSdkIds } from '~/content/navigation.references' -import { BASE_PATH } from '~/lib/constants' -import { stampFirstReferrerCookie } from 'common/first-referrer-cookie' import { isbot } from 'isbot' import { NextResponse, type NextRequest } from 'next/server' +import { clientSdkIds } from '~/content/navigation.references' +import { BASE_PATH } from '~/lib/constants' + const REFERENCE_PATH = `${BASE_PATH ?? ''}/reference` export function middleware(request: NextRequest) { const url = new URL(request.url) - - // Non-reference paths: just handle the first-referrer cookie and pass through if (!url.pathname.startsWith(REFERENCE_PATH)) { - const response = NextResponse.next() - stampFirstReferrerCookie(request, response) - return response + return NextResponse.next() } - // Reference paths: existing rewrite logic with cookie stamping on every response - if (isbot(request.headers.get('user-agent'))) { let [, lib, maybeVersion, ...slug] = url.pathname.replace(REFERENCE_PATH, '').split('/') @@ -30,9 +24,7 @@ export function middleware(request: NextRequest) { if (slug.length > 0) { const rewriteUrl = new URL(url) rewriteUrl.pathname = (BASE_PATH ?? '') + '/api/crawlers' - const response = NextResponse.rewrite(rewriteUrl) - stampFirstReferrerCookie(request, response) - return response + return NextResponse.rewrite(rewriteUrl) } } } @@ -41,42 +33,28 @@ export function middleware(request: NextRequest) { if (lib === 'cli') { const rewritePath = [REFERENCE_PATH, 'cli'].join('/') - const response = NextResponse.rewrite(new URL(rewritePath, request.url)) - stampFirstReferrerCookie(request, response) - return response + return NextResponse.rewrite(new URL(rewritePath, request.url)) } if (lib === 'api') { const rewritePath = [REFERENCE_PATH, 'api'].join('/') - const response = NextResponse.rewrite(new URL(rewritePath, request.url)) - stampFirstReferrerCookie(request, response) - return response + return NextResponse.rewrite(new URL(rewritePath, request.url)) } if (lib?.startsWith('self-hosting-')) { const rewritePath = [REFERENCE_PATH, lib].join('/') - const response = NextResponse.rewrite(new URL(rewritePath, request.url)) - stampFirstReferrerCookie(request, response) - return response + return NextResponse.rewrite(new URL(rewritePath, request.url)) } if (clientSdkIds.includes(lib)) { const version = /v\d+/.test(maybeVersion) ? maybeVersion : null const rewritePath = [REFERENCE_PATH, lib, version].filter(Boolean).join('/') - const response = NextResponse.rewrite(new URL(rewritePath, request.url)) - stampFirstReferrerCookie(request, response) - return response + return NextResponse.rewrite(new URL(rewritePath, request.url)) } - const response = NextResponse.next() - stampFirstReferrerCookie(request, response) - return response + return NextResponse.next() } export const config = { - matcher: [ - // Broadened from `/reference/:path*` to stamp first-referrer cookies on all - // docs pages, not just reference paths. Excludes Next.js internals and static files. - '/((?!api|_next/static|_next/image|favicon.ico|__nextjs).*)', - ], + matcher: '/reference/:path*', } diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 9855f6d312cf4..93165e09f6dca 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -81,6 +81,7 @@ Firas El Rachidi Francesco Sansalvadore Garrett Crowell Gerardo Estaba +Gildas Garcia Greg Kress Greg P Greg Richardson @@ -126,6 +127,7 @@ Katerina Skroumpelou Kemal Y Kevin Brolly Kevin Grüneberg +Kevin Strong-Holte Kostas Botsas Krishna Sai Vandavasi Lakshan Perera @@ -176,6 +178,7 @@ Rafael Chacón Raminder Singh Raúl Barroso Riccardo Busetti +Rodrigo Esteves Rodrigo Mansueli Ronan Lehane Rory Wilding @@ -184,6 +187,7 @@ Ruan Maia Sam Meech-Ward Sam Rome Sam Rose +Sana Cordeaux Sara Read Sean Oliver Sean Thompson diff --git a/apps/studio/components/interfaces/Organization/Organization.utils.ts b/apps/studio/components/interfaces/Organization/Organization.utils.ts index e3b508193979e..338b1b255f68a 100644 --- a/apps/studio/components/interfaces/Organization/Organization.utils.ts +++ b/apps/studio/components/interfaces/Organization/Organization.utils.ts @@ -1,3 +1,27 @@ +/** + * Builds an org-scoped URL for the wildcard org route (/org/_). + * + * When `slug` is undefined the destination is the org project list at + * /org/. When `slug` is an array the sub-path is preserved so the + * user lands on the equivalent page for the chosen org. + */ +export function buildOrgUrl({ + slug, + orgSlug, + queryString, +}: { + slug: string | string[] | undefined + orgSlug: string + queryString: string +}): string { + const qs = queryString ? `?${queryString}` : '' + if (!Array.isArray(slug)) { + return `/org/${orgSlug}${qs}` + } + const slugPath = slug.reduce((a: string, b: string) => `${a}/${b}`, '').slice(1) + return `/org/${orgSlug}/${slugPath}${qs}` +} + // Invite is expired if older than 24hrs export function isInviteExpired(timestamp: string) { const inviteDate = new Date(timestamp) diff --git a/apps/studio/components/layouts/BillingLayout/Billing.Commands.tsx b/apps/studio/components/layouts/BillingLayout/Billing.Commands.tsx new file mode 100644 index 0000000000000..cced180f247e3 --- /dev/null +++ b/apps/studio/components/layouts/BillingLayout/Billing.Commands.tsx @@ -0,0 +1,27 @@ +import { IS_PLATFORM } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useBillingGotoCommands(options?: CommandOptions) { + const { data: organization } = useSelectedOrganizationQuery() + const billingEnabled = useIsFeatureEnabled('billing:all') + const slug = organization?.slug ?? '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + IS_PLATFORM && billingEnabled + ? [ + { + id: 'nav-billing', + name: 'Billing', + route: `/org/${slug}/billing`, + defaultHidden: true, + }, + ] + : [], + { ...options, deps: [slug] } + ) +} diff --git a/apps/studio/components/layouts/useLayoutNavCommands.ts b/apps/studio/components/layouts/useLayoutNavCommands.ts index 0ce1cf08c0be2..8ea4b8ccc07c0 100644 --- a/apps/studio/components/layouts/useLayoutNavCommands.ts +++ b/apps/studio/components/layouts/useLayoutNavCommands.ts @@ -2,6 +2,7 @@ import { useIsLoggedIn } from 'common' import { useApiDocsGotoCommands } from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands' import { useAdvisorsGoToCommands } from './AdvisorsLayout/Advisors.Commands' import { useAuthGotoCommands } from './AuthLayout/Auth.Commands' +import { useBillingGotoCommands } from './BillingLayout/Billing.Commands' import { useDatabaseGotoCommands } from './DatabaseLayout/Database.Commands' import { useFunctionsGotoCommands } from './EdgeFunctionsLayout/EdgeFunctions.Commands' import { useIntegrationsGotoCommands } from './IntegrationsLayout/Integrations.Commands' @@ -27,4 +28,5 @@ export function useLayoutNavCommands() { useApiDocsGotoCommands({ enabled: isLoggedIn }) useProjectSettingsGotoCommands({ enabled: isLoggedIn }) useIntegrationsGotoCommands({ enabled: isLoggedIn }) + useBillingGotoCommands({ enabled: isLoggedIn }) } diff --git a/apps/studio/pages/org/_/[[...routeSlug]].tsx b/apps/studio/pages/org/_/[[...routeSlug]].tsx index 2039e4e9ac888..a8bf05ab446f4 100644 --- a/apps/studio/pages/org/_/[[...routeSlug]].tsx +++ b/apps/studio/pages/org/_/[[...routeSlug]].tsx @@ -3,6 +3,7 @@ import { LoadingCardView, NoOrganizationsState, } from 'components/interfaces/Home/ProjectList/EmptyStates' +import { buildOrgUrl } from 'components/interfaces/Organization/Organization.utils' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { useOrganizationsQuery } from 'data/organizations/organizations-query' @@ -23,17 +24,6 @@ const GenericOrganizationPage: NextPage = () => { const { data: organizations, isPending: isLoading } = useOrganizationsQuery() - const urlRewriterFactory = (slug: string | string[] | undefined) => { - return (orgSlug: string) => { - if (!Array.isArray(slug)) { - return `/org/${orgSlug}/general${!!queryString ? `?${queryString}` : ''}` - } else { - const slugPath = slug.reduce((a: string, b: string) => `${a}/${b}`, '').slice(1) - return `/org/${orgSlug}/${slugPath}${!!queryString ? `?${queryString}` : ''}` - } - } - } - return ( <>
@@ -60,7 +50,7 @@ const GenericOrganizationPage: NextPage = () => { ))} diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index 24e593549344a..fdb99fae7de73 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -1,6 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Info } from 'lucide-react' - import { useParams } from 'common' import { BackupsList } from 'components/interfaces/Database/Backups/BackupsList' import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseBackupsNav' @@ -14,6 +12,7 @@ import { useBackupsQuery } from 'data/database/backups-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsOrioleDbInAws } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { Info } from 'lucide-react' import type { NextPageWithLayout } from 'types' import { Admonition } from 'ui-patterns' import { PageContainer } from 'ui-patterns/PageContainer' @@ -94,9 +93,9 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { title="Point-In-Time-Recovery (PITR) enabled" description={
- Your project uses PITR and full daily backups are no longer taken. - They're not needed, as PITR supports a superset of functionality, in - terms of the granular recovery that can be performed.{' '} + Your project uses PITR and full daily backups are no longer taken. PITR + lets you restore to a specific time (down to the second) within your + selected PITR retention period.{' '} request.nextUrl.pathname.endsWith(url)) - ) { - return Response.json( - { success: false, message: 'Endpoint not supported on hosted' }, - { status: 404 } - ) - } + if ( + IS_PLATFORM && + !HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url)) + ) { + return Response.json( + { success: false, message: 'Endpoint not supported on hosted' }, + { status: 404 } + ) } - - const response = NextResponse.next() - stampFirstReferrerCookie(request, response) - return response -} - -export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico|__nextjs).*)'], } diff --git a/apps/studio/tests/pages/org/routeSlug.utils.test.ts b/apps/studio/tests/pages/org/routeSlug.utils.test.ts new file mode 100644 index 0000000000000..90dc6bbf2c7b9 --- /dev/null +++ b/apps/studio/tests/pages/org/routeSlug.utils.test.ts @@ -0,0 +1,64 @@ +import { buildOrgUrl } from 'components/interfaces/Organization/Organization.utils' +import { describe, expect, test } from 'vitest' + +describe('buildOrgUrl', () => { + describe('when slug is undefined (bare /org/_ route)', () => { + test('returns the org project list url', () => { + expect(buildOrgUrl({ slug: undefined, orgSlug: 'my-org', queryString: '' })).toBe( + '/org/my-org' + ) + }) + + test('appends query string when present', () => { + expect(buildOrgUrl({ slug: undefined, orgSlug: 'my-org', queryString: 'foo=bar' })).toBe( + '/org/my-org?foo=bar' + ) + }) + + test('does not append a bare ? when query string is empty', () => { + expect(buildOrgUrl({ slug: undefined, orgSlug: 'my-org', queryString: '' })).not.toContain( + '?' + ) + }) + }) + + describe('when slug is a string (next.js router quirk — single segment)', () => { + test('returns the org project list url, ignoring the string slug', () => { + expect(buildOrgUrl({ slug: 'general', orgSlug: 'my-org', queryString: '' })).toBe( + '/org/my-org' + ) + }) + + test('appends query string when present', () => { + expect(buildOrgUrl({ slug: 'general', orgSlug: 'my-org', queryString: 'ref=abc' })).toBe( + '/org/my-org?ref=abc' + ) + }) + }) + + describe('when slug is an array (sub-path route)', () => { + test('preserves a single-segment sub-path', () => { + expect(buildOrgUrl({ slug: ['general'], orgSlug: 'my-org', queryString: '' })).toBe( + '/org/my-org/general' + ) + }) + + test('preserves a multi-segment sub-path', () => { + expect( + buildOrgUrl({ slug: ['settings', 'billing'], orgSlug: 'my-org', queryString: '' }) + ).toBe('/org/my-org/settings/billing') + }) + + test('appends query string when present', () => { + expect( + buildOrgUrl({ slug: ['general'], orgSlug: 'my-org', queryString: 'foo=1&bar=2' }) + ).toBe('/org/my-org/general?foo=1&bar=2') + }) + + test('does not append a bare ? when query string is empty', () => { + expect(buildOrgUrl({ slug: ['general'], orgSlug: 'my-org', queryString: '' })).not.toContain( + '?' + ) + }) + }) +}) diff --git a/apps/www/_go/index.tsx b/apps/www/_go/index.tsx index 6faa297aba18e..c9d67a30d985c 100644 --- a/apps/www/_go/index.tsx +++ b/apps/www/_go/index.tsx @@ -3,7 +3,15 @@ import type { GoPageInput } from 'marketing' import exampleLeadGen from './lead-gen/example-lead-gen' import exampleLegal from './legal/example-legal' import exampleThankYou from './thank-you/example-thank-you' +import boltWebinar from './webinar/bolt-webinar' +import boltWebinarThankYou from './webinar/bolt-webinar-thank-you' -const pages: GoPageInput[] = [exampleLeadGen, exampleThankYou, exampleLegal] +const pages: GoPageInput[] = [ + exampleLeadGen, + exampleThankYou, + exampleLegal, + boltWebinar, + boltWebinarThankYou, +] export default pages diff --git a/apps/www/_go/webinar/bolt-webinar-thank-you.tsx b/apps/www/_go/webinar/bolt-webinar-thank-you.tsx new file mode 100644 index 0000000000000..799d5c8b4b117 --- /dev/null +++ b/apps/www/_go/webinar/bolt-webinar-thank-you.tsx @@ -0,0 +1,35 @@ +import type { GoPageInput } from 'marketing' +import Link from 'next/link' +import { Button } from 'ui' + +const page: GoPageInput = { + template: 'thank-you', + slug: 'bolt-webinar/thank-you', + metadata: { + title: 'Thank you', + description: 'Thanks for getting in touch. We’ll be in touch with resources and next steps.', + }, + hero: { + title: 'Thanks for getting in touch', + description: "We've received your details. Our team will follow up with you shortly.", + }, + sections: [ + { + type: 'single-column', + title: 'In the meantime', + description: 'Explore Supabase and get ready to build with AI-assisted development.', + children: ( +
+ + +
+ ), + }, + ], +} + +export default page diff --git a/apps/www/_go/webinar/bolt-webinar.tsx b/apps/www/_go/webinar/bolt-webinar.tsx new file mode 100644 index 0000000000000..fe7f26a2ecff6 --- /dev/null +++ b/apps/www/_go/webinar/bolt-webinar.tsx @@ -0,0 +1,147 @@ +import type { GoPageInput } from 'marketing' +import { MediaBlock } from 'marketing' + +const page: GoPageInput = { + template: 'lead-gen', + slug: 'bolt-webinar', + metadata: { + title: 'Vibe Coding, Done Right: Learn More | Supabase + Bolt.new', + description: + 'You watched the webinar. Want to go deeper? Get resources, talk to our team, or try Supabase with Bolt for AI-assisted development in production.', + ogImage: '/images/landing-pages/bolt-webinar/og.png', + }, + hero: { + title: 'Thanks for watching', + subtitle: 'Vibe Coding, Done Right: AI Development in Production', + description: + 'You saw how enterprise teams use Bolt and Supabase to build production apps with AI coding tools. Want to learn more, get hands-on, or talk to our team? Share your details below.', + image: { + src: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=partnerships&layout=icon-only©=%5B2.5x+faster%5D%0A%5BPostgres+parser%5D%0Awith+Claude+Code&icon=supabase.svg&icon2=bolt.svg', + alt: 'Ebook cover: Building Modern Applications with Supabase', + width: 400, + height: 500, + }, + ctas: [ + { + label: 'Get in touch', + href: '#form', + variant: 'primary', + }, + { + label: 'Start your project', + href: 'https://supabase.com/dashboard', + variant: 'secondary', + }, + ], + }, + sections: [ + { + type: 'single-column', + title: 'Watch the recording', + description: + 'Join Bolt CEO Eric Simons and learn how enterprise innovation teams are using AI coding tools to build real applications on Supabase.', + children: , + }, + { + type: 'feature-grid', + title: 'What you learned', + description: 'Key takeaways from Vibe Coding, Done Right: AI Development in Production.', + items: [ + { + title: 'Non-technical teams building in production', + description: + 'How to give non-technical teams the ability to build production software without compromising security or stability.', + }, + { + title: 'Governance for AI-assisted development', + description: + 'The governance model that makes AI-assisted development safe for enterprises.', + }, + { + title: 'Prototypes that go to production', + description: + 'Why prototypes built on the right foundation can go to production without being rebuilt.', + }, + { + title: 'Build vs. buy', + description: + 'How to evaluate SaaS contracts differently when building becomes cheaper than buying.', + }, + { + title: 'Rapid prototyping and internal tools', + description: + 'Real-world use cases for rapidly prototyping and building internal tools with Bolt and Supabase.', + }, + { + title: 'MCP and your database', + description: + 'The MCP integration that connects AI coding tools directly to your database.', + }, + ], + }, + { + type: 'form', + title: 'Tell us how we can help', + description: 'Share your details and we’ll follow up with resources or a conversation.', + 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: 'Work email', + required: true, + }, + { + type: 'text', + name: 'company_name', + label: 'Company', + placeholder: 'Company name', + required: false, + }, + { + type: 'textarea', + name: 'message', + label: 'Tell us about your project', + placeholder: 'I want to build...', + required: false, + }, + ], + submitLabel: 'Get in touch', + successRedirect: '/go/bolt-webinar/thank-you', + disclaimer: + 'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy).', + crm: { + hubspot: { + formGuid: 'b110ee65-7caf-4ad1-bbf3-c02c2b5b0550', + fieldMap: { + first_name: 'firstname', + last_name: 'lastname', + email_address: 'email', + company_name: 'company', + message: 'what_are_you_currently_working_on_', + }, + consent: + 'By submitting this form, I confirm that I have read and understood the Privacy Policy.', + }, + }, + }, + ], +} + +export default page diff --git a/apps/www/app/go/[...slug]/page.tsx b/apps/www/app/go/[...slug]/page.tsx new file mode 100644 index 0000000000000..942752270de81 --- /dev/null +++ b/apps/www/app/go/[...slug]/page.tsx @@ -0,0 +1,62 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' + +import GoPageRenderer from '@/components/Go/GoPageRenderer' +import { SITE_ORIGIN } from '@/lib/constants' +import { getAllGoSlugs, getGoPageBySlug } from '@/lib/go' + +export const dynamic = 'force-static' + +type Params = { slug: string[] } + +function getSlugFromSegments(segments: string[]): string { + return segments.join('/') +} + +export async function generateStaticParams() { + return getAllGoSlugs() + .filter((slug) => slug.includes('/')) + .map((slug) => ({ slug: slug.split('/') })) +} + +export async function generateMetadata({ params }: { params: Promise }): Promise { + const { slug: segments } = await params + const slug = getSlugFromSegments(segments) + const page = getGoPageBySlug(slug) + + if (!page) { + return { title: 'Page Not Found' } + } + + const { metadata } = page + + return { + title: `${metadata.title} | Supabase`, + description: metadata.description, + openGraph: { + title: metadata.title, + description: metadata.description, + url: `${SITE_ORIGIN}/go/${page.slug}`, + images: metadata.ogImage ? [{ url: metadata.ogImage }] : undefined, + }, + twitter: { + card: 'summary_large_image', + title: metadata.title, + description: metadata.description, + images: metadata.ogImage ? [metadata.ogImage] : undefined, + }, + robots: metadata.noIndex ? { index: false, follow: false } : undefined, + } +} + +export default async function GoPage({ params }: { params: Promise }) { + const { slug: segments } = await params + const slug = getSlugFromSegments(segments) + const page = getGoPageBySlug(slug) + + if (!page) { + notFound() + } + + return +} diff --git a/apps/www/middleware.ts b/apps/www/middleware.ts deleted file mode 100644 index 72a74634d8afd..0000000000000 --- a/apps/www/middleware.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { stampFirstReferrerCookie } from 'common/first-referrer-cookie' -import { NextResponse, type NextRequest } from 'next/server' - -export function middleware(request: NextRequest) { - const response = NextResponse.next() - stampFirstReferrerCookie(request, response) - return response -} - -export const config = { - matcher: [ - // Match all paths except Next.js internals and static files - '/((?!api|_next/static|_next/image|favicon.ico|__nextjs).*)', - ], -} diff --git a/packages/common/first-referrer-cookie.test.ts b/packages/common/first-referrer-cookie.test.ts deleted file mode 100644 index ce308e2d24487..0000000000000 --- a/packages/common/first-referrer-cookie.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { - buildFirstReferrerData, - FIRST_REFERRER_COOKIE_NAME, - hasPaidSignals, - isExternalReferrer, - parseFirstReferrerCookie, - serializeFirstReferrerCookie, - shouldRefreshCookie, -} from './first-referrer-cookie' - -describe('first-referrer-cookie', () => { - describe('isExternalReferrer', () => { - it('returns false for supabase domains', () => { - expect(isExternalReferrer('https://supabase.com')).toBe(false) - expect(isExternalReferrer('https://www.supabase.com')).toBe(false) - expect(isExternalReferrer('https://docs.supabase.com')).toBe(false) - }) - - it('returns true for external domains', () => { - expect(isExternalReferrer('https://google.com')).toBe(true) - expect(isExternalReferrer('https://chatgpt.com')).toBe(true) - }) - - it('returns true for http:// referrers', () => { - expect(isExternalReferrer('http://google.com')).toBe(true) - expect(isExternalReferrer('http://example.org/page')).toBe(true) - }) - - it('returns false for invalid values', () => { - expect(isExternalReferrer('')).toBe(false) - expect(isExternalReferrer('not-a-url')).toBe(false) - }) - }) - - describe('buildFirstReferrerData', () => { - it('handles malformed landing URL gracefully', () => { - const data = buildFirstReferrerData({ - referrer: 'https://google.com', - landingUrl: 'not-a-valid-url', - }) - - expect(data.referrer).toBe('https://google.com') - expect(data.landing_url).toBe('not-a-valid-url') - expect(data.utms).toEqual({}) - expect(data.click_ids).toEqual({}) - }) - - it('extracts utm and click-id params from landing url', () => { - const data = buildFirstReferrerData({ - referrer: 'https://www.google.com/', - landingUrl: - 'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456', - }) - - expect(data.referrer).toBe('https://www.google.com/') - expect(data.landing_url).toBe( - 'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456' - ) - - expect(data.utms).toEqual({ - utm_source: 'google', - utm_medium: 'cpc', - utm_campaign: 'test', - }) - - expect(data.click_ids).toEqual({ - gclid: 'abc123', - msclkid: 'xyz456', - }) - }) - }) - - describe('serialize / parse', () => { - it('round-trips valid cookie payloads', () => { - const input = buildFirstReferrerData({ - referrer: 'https://www.google.com/', - landingUrl: 'https://supabase.com/pricing?utm_source=google', - }) - - const encoded = serializeFirstReferrerCookie(input) - const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`) - - expect(parsed).toEqual(input) - }) - - it('returns null for empty string', () => { - expect(parseFirstReferrerCookie('')).toBeNull() - }) - - it('parses cookie from header with multiple cookies', () => { - const input = buildFirstReferrerData({ - referrer: 'https://google.com/', - landingUrl: 'https://supabase.com/', - }) - const encoded = serializeFirstReferrerCookie(input) - const header = `session=abc123; ${FIRST_REFERRER_COOKIE_NAME}=${encoded}; theme=dark` - - expect(parseFirstReferrerCookie(header)).toEqual(input) - }) - - it('returns null for malformed json', () => { - expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=%7Bnot-json`)).toBeNull() - }) - - it('returns null for invalid payload shape', () => { - const encoded = encodeURIComponent(JSON.stringify({ foo: 'bar' })) - expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)).toBeNull() - }) - - it('drops non-string values in utms/click_ids', () => { - const encoded = encodeURIComponent( - JSON.stringify({ - referrer: 'https://www.google.com/', - landing_url: 'https://supabase.com/pricing', - utms: { utm_source: 'google', utm_medium: 123 }, - click_ids: { gclid: 'abc', msclkid: null }, - ts: 123, - }) - ) - - const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`) - - expect(parsed).toEqual({ - referrer: 'https://www.google.com/', - landing_url: 'https://supabase.com/pricing', - utms: { utm_source: 'google' }, - click_ids: { gclid: 'abc' }, - ts: 123, - }) - }) - }) - - describe('hasPaidSignals', () => { - it('detects click IDs', () => { - expect(hasPaidSignals(new URL('https://supabase.com/?gclid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?fbclid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?msclkid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?gbraid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?wbraid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?rdt_cid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?ttclid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?twclid=abc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?li_fat_id=abc'))).toBe(true) - }) - - it('detects paid utm_medium values', () => { - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=cpc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=ppc'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_search'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paidsocial'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_social'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=display'))).toBe(true) - }) - - it('is case-insensitive for utm_medium', () => { - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=CPC'))).toBe(true) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=Paid_Search'))).toBe(true) - }) - - it('returns false for organic traffic', () => { - expect(hasPaidSignals(new URL('https://supabase.com/'))).toBe(false) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_source=google'))).toBe(false) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=email'))).toBe(false) - expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=organic'))).toBe(false) - }) - }) - - describe('shouldRefreshCookie', () => { - it('stamps when no cookie and external referrer', () => { - expect( - shouldRefreshCookie(false, { - referrer: 'https://google.com', - url: 'https://supabase.com/', - }) - ).toEqual({ stamp: true }) - }) - - it('skips when no cookie and internal referrer', () => { - expect( - shouldRefreshCookie(false, { - referrer: 'https://supabase.com/docs', - url: 'https://supabase.com/dashboard', - }) - ).toEqual({ stamp: false }) - }) - - it('skips when cookie exists and no paid signals', () => { - expect( - shouldRefreshCookie(true, { - referrer: 'https://google.com', - url: 'https://supabase.com/', - }) - ).toEqual({ stamp: false }) - }) - - it('refreshes when cookie exists but URL has paid signals', () => { - expect( - shouldRefreshCookie(true, { - referrer: 'https://google.com', - url: 'https://supabase.com/?gclid=abc123', - }) - ).toEqual({ stamp: true }) - - expect( - shouldRefreshCookie(true, { - referrer: 'https://google.com', - url: 'https://supabase.com/?utm_medium=cpc&utm_source=google', - }) - ).toEqual({ stamp: true }) - }) - - it('skips when no cookie and no referrer (direct navigation)', () => { - expect(shouldRefreshCookie(false, { referrer: '', url: 'https://supabase.com/' })).toEqual({ - stamp: false, - }) - }) - - it('handles malformed URL gracefully', () => { - expect( - shouldRefreshCookie(true, { - referrer: 'https://google.com', - url: 'not-a-valid-url', - }) - ).toEqual({ stamp: false }) - }) - }) -}) diff --git a/packages/common/first-referrer-cookie.ts b/packages/common/first-referrer-cookie.ts deleted file mode 100644 index cfa7d6b7131f8..0000000000000 --- a/packages/common/first-referrer-cookie.ts +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Shared utilities for the cross-app first-referrer handoff cookie. - * - * The `_sb_first_referrer` cookie is written by edge middleware on `apps/www`, - * `apps/docs`, and `apps/studio` when a user arrives from an - * external source. Studio reads it on the first telemetry pageview to recover - * external attribution context that would otherwise be lost at the app boundary. - * - * The cookie is normally write-once (365-day TTL, domain=supabase.com), but is - * refreshed when a returning visitor arrives with paid traffic signals (click IDs - * or paid UTM medium values) to ensure paid attribution overrides stale organic data. - */ - -// --------------------------------------------------------------------------- -// Structural types for Next.js middleware request/response -// --------------------------------------------------------------------------- -// Using structural interfaces instead of importing NextRequest/NextResponse -// avoids version conflicts when different apps pin different Next.js versions -// (e.g. studio on Next 15, docs/www on Next 16). - -interface MiddlewareRequest { - headers: { get(name: string): string | null } - cookies: { has(name: string): boolean } - url: string - nextUrl: { hostname: string } -} - -interface MiddlewareResponse { - cookies: { - set( - name: string, - value: string, - options?: { - path?: string - sameSite?: 'lax' | 'strict' | 'none' - secure?: boolean - domain?: string - maxAge?: number - } - ): void - } -} - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -export const FIRST_REFERRER_COOKIE_NAME = '_sb_first_referrer' - -/** 365 days in seconds */ -export const FIRST_REFERRER_COOKIE_MAX_AGE = 365 * 24 * 60 * 60 - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface FirstReferrerData { - /** The external referrer URL (e.g. https://www.google.com/) */ - referrer: string - /** The landing URL on our site when the external referrer was captured */ - landing_url: string - /** UTM params parsed from the landing URL (e.g. utm_source, utm_medium) */ - utms: Record - /** Ad-network click IDs parsed from the landing URL */ - click_ids: Record - /** Unix timestamp (ms) when the cookie was written */ - ts: number -} - -// --------------------------------------------------------------------------- -// Referrer classification -// --------------------------------------------------------------------------- - -/** - * Returns true if the referrer URL points to an external (non-Supabase) domain. - * Handles malformed URLs gracefully by returning false. - */ -export function isExternalReferrer(referrer: string): boolean { - if (!referrer) return false - try { - const hostname = new URL(referrer).hostname - return hostname !== 'supabase.com' && !hostname.endsWith('.supabase.com') - } catch { - return false - } -} - -// --------------------------------------------------------------------------- -// UTM + click-ID extraction -// --------------------------------------------------------------------------- - -const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const - -const CLICK_ID_KEYS = [ - 'gclid', // Google Ads - 'gbraid', // Google Ads (iOS) - 'wbraid', // Google Ads (iOS) - 'msclkid', // Microsoft Ads (Bing) - 'fbclid', // Meta (Facebook/Instagram) - 'rdt_cid', // Reddit Ads - 'ttclid', // TikTok Ads - 'twclid', // X Ads (Twitter) - 'li_fat_id', // LinkedIn Ads -] as const - -function pickParams( - searchParams: URLSearchParams, - keys: readonly string[] -): Record { - const result: Record = {} - for (const key of keys) { - const value = searchParams.get(key) - if (value) { - result[key] = value - } - } - return result -} - -function toStringRecord(value: unknown): Record { - if (!value || typeof value !== 'object') return {} - - return Object.fromEntries( - Object.entries(value as Record).filter( - ([key, v]) => typeof key === 'string' && typeof v === 'string' - ) - ) as Record -} - -// --------------------------------------------------------------------------- -// Build cookie payload from a request (edge-compatible) -// --------------------------------------------------------------------------- - -/** - * Build a `FirstReferrerData` payload from raw request values. - * Intended for use in Next.js middleware where `document` is not available. - */ -export function buildFirstReferrerData({ - referrer, - landingUrl, -}: { - referrer: string - landingUrl: string -}): FirstReferrerData { - let utms: Record = {} - let click_ids: Record = {} - - try { - const url = new URL(landingUrl) - utms = pickParams(url.searchParams, UTM_KEYS) - click_ids = pickParams(url.searchParams, CLICK_ID_KEYS) - } catch { - // If landing URL is malformed, just skip param extraction - } - - return { - referrer, - landing_url: landingUrl, - utms, - click_ids, - ts: Date.now(), - } -} - -// --------------------------------------------------------------------------- -// Serialize / parse -// --------------------------------------------------------------------------- - -export function serializeFirstReferrerCookie(data: FirstReferrerData): string { - return encodeURIComponent(JSON.stringify(data)) -} - -// --------------------------------------------------------------------------- -// Paid-signal detection -// --------------------------------------------------------------------------- - -const PAID_UTM_MEDIUMS = new Set([ - 'cpc', - 'ppc', - 'paid_search', - 'paidsocial', - 'paid_social', - 'display', -]) - -/** - * Returns true if the URL contains ad-network click IDs or paid UTM medium values. - * These indicate the user arrived via a paid campaign, which should override - * stale organic attribution. - */ -export function hasPaidSignals(url: URL): boolean { - for (const key of CLICK_ID_KEYS) { - if (url.searchParams.has(key)) return true - } - const medium = url.searchParams.get('utm_medium')?.toLowerCase() - return medium !== undefined && PAID_UTM_MEDIUMS.has(medium) -} - -/** - * Decides whether the first-referrer cookie should be (re-)stamped. - * - * - No cookie + external referrer → stamp (first visit attribution) - * - Cookie exists + paid signals in URL → stamp (paid traffic refresh) - * - Otherwise → skip - */ -export function shouldRefreshCookie( - existingCookie: boolean, - request: { referrer: string; url: string } -): { stamp: boolean } { - if (!existingCookie) { - return { stamp: isExternalReferrer(request.referrer) } - } - - try { - const url = new URL(request.url) - return { stamp: hasPaidSignals(url) } - } catch { - return { stamp: false } - } -} - -// --------------------------------------------------------------------------- -// Middleware helper — shared across apps/www, apps/docs, and apps/studio -// --------------------------------------------------------------------------- - -/** - * Stamp the first-referrer cookie on a Next.js middleware response if the - * request warrants it. This is the single entry point for all app middleware - * files — call it with the incoming request and outgoing response. - * - * On *.supabase.com the cookie is set with `domain=supabase.com` so it's - * readable across all subdomains (www, docs, studio). On other hosts - * (localhost, preview deploys) the domain is left unset so the browser - * stores a host-only cookie instead of rejecting an invalid domain. - */ -export function stampFirstReferrerCookie(request: MiddlewareRequest, response: MiddlewareResponse): void { - const referrer = request.headers.get('referer') ?? '' - - const { stamp } = shouldRefreshCookie(request.cookies.has(FIRST_REFERRER_COOKIE_NAME), { - referrer, - url: request.url, - }) - - if (!stamp) return - - const data = buildFirstReferrerData({ - referrer, - landingUrl: request.url, - }) - - response.cookies.set(FIRST_REFERRER_COOKIE_NAME, serializeFirstReferrerCookie(data), { - path: '/', - sameSite: 'lax', - ...(request.nextUrl.hostname === 'supabase.com' || - request.nextUrl.hostname.endsWith('.supabase.com') - ? { domain: 'supabase.com', secure: true } - : {}), - maxAge: FIRST_REFERRER_COOKIE_MAX_AGE, - }) -} - -// --------------------------------------------------------------------------- -// Parse cookie from document.cookie header (client-side) -// --------------------------------------------------------------------------- - -export function parseFirstReferrerCookie(cookieHeader: string): FirstReferrerData | null { - try { - const cookies = cookieHeader.split(';') - const match = cookies - .map((c) => c.trim()) - .find((c) => c.startsWith(`${FIRST_REFERRER_COOKIE_NAME}=`)) - - if (!match) return null - - const value = match.slice(`${FIRST_REFERRER_COOKIE_NAME}=`.length) - const parsed = JSON.parse(decodeURIComponent(value)) as unknown - - if (!parsed || typeof parsed !== 'object') return null - - const parsedRecord = parsed as Record - const referrer = parsedRecord.referrer - const landingUrl = parsedRecord.landing_url - - if (typeof referrer !== 'string' || typeof landingUrl !== 'string') { - return null - } - - const utmsRaw = parsedRecord.utms - const clickIdsRaw = parsedRecord.click_ids - const tsRaw = parsedRecord.ts - - const utms = toStringRecord(utmsRaw) - const click_ids = toStringRecord(clickIdsRaw) - - const ts = typeof tsRaw === 'number' && Number.isFinite(tsRaw) ? tsRaw : Date.now() - - return { - referrer, - landing_url: landingUrl, - utms, - click_ids, - ts, - } - } catch { - return null - } -} diff --git a/packages/common/index.tsx b/packages/common/index.tsx index ee8ebe133070c..87b60f4e6f996 100644 --- a/packages/common/index.tsx +++ b/packages/common/index.tsx @@ -10,6 +10,5 @@ export * from './helpers' export * from './hooks' export * from './MetaFavicons/pages-router' export * from './Providers' -export * from './first-referrer-cookie' export * from './telemetry' export * from './telemetry-utils' diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 36b3ea4ee011c..29c416c8bd90d 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -12,8 +12,6 @@ import { hasConsented } from './consent-state' import { IS_PLATFORM, IS_PROD, LOCAL_STORAGE_KEYS } from './constants' import { useFeatureFlags } from './feature-flags' import { post } from './fetchWrappers' -import type { FirstReferrerData } from './first-referrer-cookie' -import { isExternalReferrer, parseFirstReferrerCookie } from './first-referrer-cookie' import { ensurePlatformSuffix, isBrowser } from './helpers' import { useParams, useTelemetryCookie } from './hooks' import { posthogClient, type ClientTelemetryEvent } from './posthog-client' @@ -98,25 +96,25 @@ function getFirstTouchAttributionProps(telemetryData: SharedTelemetryData) { } } -interface HandlePageTelemetryOptions { - apiUrl: string - pathname?: string - featureFlags?: Record - slug?: string - ref?: string - telemetryDataOverride?: SharedTelemetryData - firstReferrerData?: FirstReferrerData | null +function isExternalReferrer(referrer: string) { + try { + const hostname = new URL(referrer).hostname + return hostname !== 'supabase.com' && !hostname.endsWith('.supabase.com') + } catch { + return false + } } -function handlePageTelemetry({ - apiUrl: API_URL, - pathname, - featureFlags, - slug, - ref, - telemetryDataOverride, - firstReferrerData, -}: HandlePageTelemetryOptions) { +function handlePageTelemetry( + API_URL: string, + pathname?: string, + featureFlags?: { + [key: string]: unknown + }, + slug?: string, + ref?: string, + telemetryDataOverride?: SharedTelemetryData +) { // Send to PostHog client-side (only in browser) if (typeof window !== 'undefined') { const livePageData = getSharedTelemetryData(pathname) @@ -135,51 +133,10 @@ function handlePageTelemetry({ referrer: shouldUseCookieReferrer ? cookieReferrer! : liveReferrer, }, } - : { ...livePageData, ph: { ...livePageData.ph } } - const firstTouchAttributionProps: Record = { - ...(telemetryDataOverride ? getFirstTouchAttributionProps(telemetryDataOverride) : {}), - } - - // --- First-referrer edge cookie handoff --- - // If the edge cookie has external context and the current referrer is internal, - // override the referrer so PostHog gets the real acquisition source. - const firstReferrerCookiePresent = Boolean(firstReferrerData) - let firstReferrerCookieConsumed = false - - if ( - firstReferrerData && - isExternalReferrer(firstReferrerData.referrer) && - !isExternalReferrer(pageData.ph.referrer) - ) { - pageData.ph.referrer = firstReferrerData.referrer - firstReferrerCookieConsumed = true - - // Prefer attribution context captured at the external entry point. - const { utms, click_ids, landing_url } = firstReferrerData - - Object.entries(utms).forEach(([key, value]) => { - const phKey = key.startsWith('utm_') ? `$${key}` : key - firstTouchAttributionProps[phKey] = value - }) - - Object.entries(click_ids).forEach(([key, value]) => { - firstTouchAttributionProps[key] = value - }) - - try { - const url = new URL(landing_url) - firstTouchAttributionProps.first_touch_url = url.href - firstTouchAttributionProps.first_touch_pathname = url.pathname - - if (url.search) { - firstTouchAttributionProps.first_touch_search = url.search - } else { - delete firstTouchAttributionProps.first_touch_search - } - } catch { - // Skip if landing URL is malformed - } - } + : livePageData + const firstTouchAttributionProps = telemetryDataOverride + ? getFirstTouchAttributionProps(telemetryDataOverride) + : {} const $referrer = pageData.ph.referrer const $referring_domain = (() => { @@ -213,13 +170,6 @@ function handlePageTelemetry({ ...Object.fromEntries( Object.entries(featureFlags || {}).map(([k, v]) => [`$feature/${k}`, v]) ), - // Measurement properties for handoff observability - // Only included on the initial pageview (when firstReferrerData is explicitly - // passed as null or a value — subsequent pageviews leave it as undefined) - ...(firstReferrerData !== undefined && { - first_referrer_cookie_present: firstReferrerCookiePresent, - first_referrer_cookie_consumed: firstReferrerCookieConsumed, - }), }) } @@ -284,13 +234,13 @@ export const PageTelemetry = ({ const sendPageTelemetry = useCallback(() => { if (!(enabled && hasAcceptedConsent)) return Promise.resolve() - return handlePageTelemetry({ - apiUrl: API_URL, - pathname: pathnameRef.current, - featureFlags: featureFlagsRef.current, + return handlePageTelemetry( + API_URL, + pathnameRef.current, + featureFlagsRef.current, slug, - ref, - }).catch((e) => { + ref + ).catch((e) => { console.error('Problem sending telemetry page:', e) }) }, [API_URL, enabled, hasAcceptedConsent, slug, ref]) @@ -333,9 +283,6 @@ export const PageTelemetry = ({ hasAcceptedConsent && !hasSentInitialPageTelemetryRef.current ) { - // Read the edge-set first-referrer cookie (cross-app handoff) - const firstReferrerData = parseFirstReferrerCookie(document.cookie) - const cookies = document.cookie.split(';') const telemetryCookieValue = cookies .map((cookie) => cookie.trim()) @@ -347,39 +294,24 @@ export const PageTelemetry = ({ const telemetryData = JSON.parse( decodeURIComponent(telemetryCookieValue) ) as SharedTelemetryData - handlePageTelemetry({ - apiUrl: API_URL, - pathname: pathnameRef.current, - featureFlags: featureFlagsRef.current, + handlePageTelemetry( + API_URL, + pathnameRef.current, + featureFlagsRef.current, slug, ref, - telemetryDataOverride: telemetryData, - firstReferrerData, - }) + telemetryData + ) } catch (error) { if (!IS_PROD) { console.warn('Invalid telemetry cookie data:', error) } - handlePageTelemetry({ - apiUrl: API_URL, - pathname: pathnameRef.current, - featureFlags: featureFlagsRef.current, - slug, - ref, - firstReferrerData, - }) + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref) } finally { clearTelemetryDataCookie() } } else { - handlePageTelemetry({ - apiUrl: API_URL, - pathname: pathnameRef.current, - featureFlags: featureFlagsRef.current, - slug, - ref, - firstReferrerData, - }) + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref) } hasSentInitialPageTelemetryRef.current = true diff --git a/packages/marketing/src/go/sections/FormSection.tsx b/packages/marketing/src/go/sections/FormSection.tsx index 8442d41825611..0939d15e86153 100644 --- a/packages/marketing/src/go/sections/FormSection.tsx +++ b/packages/marketing/src/go/sections/FormSection.tsx @@ -221,7 +221,14 @@ export default function FormSection({ section }: { section: GoFormSection }) { {section.disclaimer && (

{children}

, a: ({ href, children }) =>
{children} }} + components={{ + p: ({ children }) =>

{children}

, + a: ({ href, children }) => ( + + {children} + + ), + }} > {section.disclaimer} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5363c8433b790..d21c6a0c70d1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,17 +10,17 @@ catalogs: specifier: ^10.26.0 version: 10.27.0 '@supabase/auth-js': - specifier: 2.97.1-canary.0 - version: 2.97.1-canary.0 + specifier: 2.97.1-canary.3 + version: 2.97.1-canary.3 '@supabase/postgrest-js': - specifier: 2.97.1-canary.0 - version: 2.97.1-canary.0 + specifier: 2.97.1-canary.3 + version: 2.97.1-canary.3 '@supabase/realtime-js': - specifier: 2.97.1-canary.0 - version: 2.97.1-canary.0 + specifier: 2.97.1-canary.3 + version: 2.97.1-canary.3 '@supabase/supabase-js': - specifier: 2.97.1-canary.0 - version: 2.97.1-canary.0 + specifier: 2.97.1-canary.3 + version: 2.97.1-canary.3 '@types/node': specifier: ^22.0.0 version: 22.13.14 @@ -317,7 +317,7 @@ importers: version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))) @@ -829,7 +829,7 @@ importers: version: 7.5.0 '@supabase/auth-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@supabase/mcp-server-supabase': specifier: ^0.6.3 version: 0.6.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(supports-color@8.1.1)(zod@3.25.76))(zod@3.25.76) @@ -841,7 +841,7 @@ importers: version: link:../../packages/pg-meta '@supabase/realtime-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@supabase/shared-types': specifier: 0.1.84 version: 0.1.84 @@ -850,7 +850,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@tanstack/react-query': specifier: ^5.0.0 version: 5.83.0(react@18.3.1) @@ -1367,7 +1367,7 @@ importers: version: 7.4.0(@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) '@supabase/postgrest-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -1488,10 +1488,10 @@ importers: version: 1.6.0 '@supabase/ssr': specifier: ^0.7.0 - version: 0.7.0(@supabase/supabase-js@2.97.1-canary.0) + version: 0.7.0(@supabase/supabase-js@2.97.1-canary.3) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@tanstack/react-router': specifier: ^1.150.0 version: 1.158.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1602,7 +1602,7 @@ importers: version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@vercel/og': specifier: ^0.6.2 version: 0.6.2 @@ -1849,10 +1849,10 @@ importers: dependencies: '@supabase/ssr': specifier: ^0.7.0 - version: 0.7.0(@supabase/supabase-js@2.97.1-canary.0) + version: 0.7.0(@supabase/supabase-js@2.97.1-canary.3) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@vueuse/core': specifier: ^14.1.0 version: 14.1.0(vue@3.5.21(typescript@5.9.2)) @@ -1898,7 +1898,7 @@ importers: version: 1.56.1 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 cross-fetch: specifier: ^4.1.0 version: 4.1.0(encoding@0.1.13) @@ -1926,7 +1926,7 @@ importers: version: 0.18.5 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 ai: specifier: 5.0.52 version: 5.0.52(zod@3.25.76) @@ -2020,10 +2020,10 @@ importers: dependencies: '@supabase/auth-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@types/dat.gui': specifier: ^0.7.12 version: 0.7.12 @@ -2582,7 +2582,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.0 + version: 2.97.1-canary.3 '@vitest/coverage-v8': specifier: ^3.2.0 version: 3.2.4(supports-color@8.1.1)(vitest@3.2.4) @@ -8209,12 +8209,12 @@ packages: resolution: {integrity: sha512-Cq3KKe+G1o7PSBMbmrgpT2JgBeyH2THHr3RdIX2MqF7AnBuspIMgtZ3ktcCgP7kZsTMvnmWymr7zZCT1zeWbMw==} engines: {node: '>=12.16'} - '@supabase/auth-js@2.97.1-canary.0': - resolution: {integrity: sha512-+CTL/2OKJ8pkNIR5hn9yj99riq3r/6Z7WLr4i/2fOsMARXkpVGPsEBxc3QiFn2VMNZAN31aaALD+VS8BWkCufQ==} + '@supabase/auth-js@2.97.1-canary.3': + resolution: {integrity: sha512-MLsim92wAP6zC20dfl98HlZmpNdVWCQHvuaVvorT1wXiqe4n6iseGtLXhHrC1rBsV8UreEJ7TltPa97bUAH32w==} engines: {node: '>=20.0.0'} - '@supabase/functions-js@2.97.1-canary.0': - resolution: {integrity: sha512-RJk4HwPFFERO29dd5I56kZxPFJNgBjC4HzyzYbQBbU7/IhHdgnmutFmqJFxngIC/gp+Av/QIV5wI5s59PGRCcw==} + '@supabase/functions-js@2.97.1-canary.3': + resolution: {integrity: sha512-lgjHUVnRwsmtlbrfBvq/BTK8SIzzX8zGskjLQpxI0SRVeppJ0/krcrhgqys4bdns5bGOfn2QIRqhGSW4eDpj5g==} engines: {node: '>=20.0.0'} '@supabase/mcp-server-supabase@0.6.3': @@ -8234,12 +8234,12 @@ packages: resolution: {integrity: sha512-vz5gc6RKNfDVnIfRUmH2ssTMYFI0U3MYOVyQ9R4YkzOS2dKSanjC4rTEDGjlMFwGTCUPW3N3pbY7HJIW81wMyg==} engines: {node: '>=16', npm: '>=8'} - '@supabase/postgrest-js@2.97.1-canary.0': - resolution: {integrity: sha512-EAA28lQCHU0l2lzy118hRh+A3bnt54YzMW3TnkoGGI59KOaHzUBoH9S4wd/YL5Ky8UzTinCxivpiqZ8OWzDS1A==} + '@supabase/postgrest-js@2.97.1-canary.3': + resolution: {integrity: sha512-M8IVst0FYK7LunTyVDorT2PVn9R9p1u0dBAsqOUxhZDdhvUiMrU2hq8xlUK2F6TEMN2YHLjCC3tFm9rfg9R54g==} engines: {node: '>=20.0.0'} - '@supabase/realtime-js@2.97.1-canary.0': - resolution: {integrity: sha512-1t9I9KIFU0mA1e/PzubPyoaO/1SimnyyJiDxkl+LYgaSQalEn/tslHQjfRjbiO9lvvvVCVVNFxox839Jn72jIA==} + '@supabase/realtime-js@2.97.1-canary.3': + resolution: {integrity: sha512-jZ+UK2M7aQbXze5p+IAt02jLZdDDCP981WdrhoUnQR536WdY385tRqAMoKgl21xCpozAo3maO4VraOhagipUtg==} engines: {node: '>=20.0.0'} '@supabase/shared-types@0.1.84': @@ -8253,8 +8253,8 @@ packages: peerDependencies: '@supabase/supabase-js': ^2.43.4 - '@supabase/storage-js@2.97.1-canary.0': - resolution: {integrity: sha512-FhdQxoYYPyZdTK6Xt3PlCia33cMAipn+sbxC9vdqRqZ3cd4bKaFn4K5VxxUodj5sDXW4hH2vQX1VcShNzio0Qw==} + '@supabase/storage-js@2.97.1-canary.3': + resolution: {integrity: sha512-2QK8NR8OFtiLmyHWduG/6Gi+6ArJvmEzD5yegLeqbsh0VAoBoW4qUCQyiCNYbua7RPpb72zsaaKQg4wO2y6ZIw==} engines: {node: '>=20.0.0'} '@supabase/supa-mdx-lint-darwin@0.2.6-alpha': @@ -8347,8 +8347,8 @@ packages: resolution: {integrity: sha512-TNbBLSofM6jQg3JwzO4lttd59dScTTzW4p504/OWcgRWghQLRNfxXRJJtdui83gBMLWpgeUZqvgtfYIwS1Flzw==} hasBin: true - '@supabase/supabase-js@2.97.1-canary.0': - resolution: {integrity: sha512-Dc4GeioHV6Fn5rATZZ7e+ccvMvC7g2RvzicaLvB68zpK9zOYf5VICzwVDcTeyuiFm76wavTS8vBnWQIkPngQnA==} + '@supabase/supabase-js@2.97.1-canary.3': + resolution: {integrity: sha512-fpZT3obVxsNvjdBEYeGqcGDaF9XsNYuGN9bKFt4Ooqwhe97y9tpf72sswqiRzuFRmgxmZ1dFnAMiFveTeoQDwQ==} engines: {node: '>=20.0.0'} '@swc/helpers@0.5.15': @@ -25497,11 +25497,11 @@ snapshots: '@stripe/stripe-js@7.5.0': {} - '@supabase/auth-js@2.97.1-canary.0': + '@supabase/auth-js@2.97.1-canary.3': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.97.1-canary.0': + '@supabase/functions-js@2.97.1-canary.3': dependencies: tslib: 2.8.1 @@ -25536,11 +25536,11 @@ snapshots: - pg-native - supports-color - '@supabase/postgrest-js@2.97.1-canary.0': + '@supabase/postgrest-js@2.97.1-canary.3': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.97.1-canary.0': + '@supabase/realtime-js@2.97.1-canary.3': dependencies: '@types/phoenix': 1.6.6 '@types/ws': 8.18.1 @@ -25561,12 +25561,12 @@ snapshots: - encoding - supports-color - '@supabase/ssr@0.7.0(@supabase/supabase-js@2.97.1-canary.0)': + '@supabase/ssr@0.7.0(@supabase/supabase-js@2.97.1-canary.3)': dependencies: - '@supabase/supabase-js': 2.97.1-canary.0 + '@supabase/supabase-js': 2.97.1-canary.3 cookie: 1.0.2 - '@supabase/storage-js@2.97.1-canary.0': + '@supabase/storage-js@2.97.1-canary.3': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 @@ -25635,13 +25635,13 @@ snapshots: '@supabase/supa-mdx-lint-win32-x64': 0.3.1 node-pty: 1.0.0 - '@supabase/supabase-js@2.97.1-canary.0': + '@supabase/supabase-js@2.97.1-canary.3': dependencies: - '@supabase/auth-js': 2.97.1-canary.0 - '@supabase/functions-js': 2.97.1-canary.0 - '@supabase/postgrest-js': 2.97.1-canary.0 - '@supabase/realtime-js': 2.97.1-canary.0 - '@supabase/storage-js': 2.97.1-canary.0 + '@supabase/auth-js': 2.97.1-canary.3 + '@supabase/functions-js': 2.97.1-canary.3 + '@supabase/postgrest-js': 2.97.1-canary.3 + '@supabase/realtime-js': 2.97.1-canary.3 + '@supabase/storage-js': 2.97.1-canary.3 transitivePeerDependencies: - bufferutil - utf-8-validate diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ff4ee79cb1e48..70fc50f1d6b97 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,10 +6,10 @@ packages: catalog: '@sentry/nextjs': ^10.26.0 - '@supabase/auth-js': 2.97.1-canary.0 - '@supabase/postgrest-js': 2.97.1-canary.0 - '@supabase/realtime-js': 2.97.1-canary.0 - '@supabase/supabase-js': 2.97.1-canary.0 + '@supabase/auth-js': 2.97.1-canary.3 + '@supabase/postgrest-js': 2.97.1-canary.3 + '@supabase/realtime-js': 2.97.1-canary.3 + '@supabase/supabase-js': 2.97.1-canary.3 '@types/node': ^22.0.0 '@types/react': ^18.3.0 '@types/react-dom': ^18.3.0 diff --git a/scripts/upload-static-assets.sh b/scripts/upload-static-assets.sh index f3bfc48a2bd0a..aa5aa2eef98c9 100755 --- a/scripts/upload-static-assets.sh +++ b/scripts/upload-static-assets.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -eo pipefail ####### @@ -82,13 +83,11 @@ aws s3 sync "$PUBLIC_DIR" "s3://$BUCKET_NAME/$SITE_NAME/${VERCEL_GIT_COMMIT_SHA: --region auto \ --only-show-errors -if [ $? -eq 0 ]; then - echo -e "${GREEN}Upload completed successfully!${NC}" - - # Clean up local static files so we prevent a double upload - echo -e "${YELLOW}Cleaning up local static files...${NC}" - rm -rf "$STATIC_DIR"/* - echo -e "${GREEN}Local static files cleaned up${NC}" +echo -e "${GREEN}Upload completed successfully!${NC}" - # We still keep the public dir, as Next.js does not officially support serving the public files via CDN -fi \ No newline at end of file +# Clean up local static files so we prevent a double upload +echo -e "${YELLOW}Cleaning up local static files...${NC}" +rm -rf "$STATIC_DIR"/* +echo -e "${GREEN}Local static files cleaned up${NC}" + +# We still keep the public dir, as Next.js does not officially support serving the public files via CDN \ No newline at end of file