@@ -305,7 +305,7 @@ hideToc: true
<$Show if="docs:mobile_tutorials">
-### Mobile tutorials
+## Mobile tutorials
<$Show if="sdk:dart">
diff --git a/apps/docs/content/guides/platform/aws-marketplace.mdx b/apps/docs/content/guides/platform/aws-marketplace.mdx
index 0d9c1b3ab7661..b3f559a58a9fd 100644
--- a/apps/docs/content/guides/platform/aws-marketplace.mdx
+++ b/apps/docs/content/guides/platform/aws-marketplace.mdx
@@ -7,7 +7,7 @@ You can purchase Supabase through the AWS Marketplace. Buying through AWS Market
When you make a purchase on AWS Marketplace, AWS will calculate sales taxes, VAT, GST, service tax, etc. (“Indirect Taxes”), if applicable, based on the location of your AWS account. You can find more details in the [AWS tax help guide](https://aws.amazon.com/tax-help/marketplace-buyers/).
-### Plans available through the AWS Marketplace
+## Plans available through the AWS Marketplace
- Free Plan: not available
- Pro Plan: available, self-serve
diff --git a/apps/docs/content/guides/platform/aws-marketplace/faq.mdx b/apps/docs/content/guides/platform/aws-marketplace/faq.mdx
index 14e25ae8c49fb..d1875ac9e022d 100644
--- a/apps/docs/content/guides/platform/aws-marketplace/faq.mdx
+++ b/apps/docs/content/guides/platform/aws-marketplace/faq.mdx
@@ -3,19 +3,19 @@ id: 'aws-marketplace-faq'
title: 'AWS Marketplace FAQ'
---
-#### The payment for completing the subscription on the AWS Marketplace fails.
+## The payment for completing the subscription on the AWS Marketplace fails.
For more information on payment errors, refer to the [AWS documentation](https://docs.aws.amazon.com/marketplace/latest/buyerguide/buyer-paying-for-products.html#payment-methods).
-#### How can the Spend Cap for an organization managed through the AWS Marketplace be enabled?
+## How can the Spend Cap for an organization managed through the AWS Marketplace be enabled?
For organizations on the Pro Plan that are managed through the AWS Marketplace, the Spend Cap is not available.
In your AWS account, you can set up a budget for marketplace purchases (or for a specific marketplace product) and receive notifications once the budget is exceeded.
-#### How to cancel your AWS Marketplace subscription
+## How to cancel your AWS Marketplace subscription
You can cancel your marketplace subscription within 48 hours of purchase. To do so, open a support ticket via the Supabase dashboard. After the 48-hour period, cancellation is no longer possible. If you cancel within the first 48 hours, the upfront charge for the fixed subscription fee will be refunded. Any usage costs incurred up to that point will not be refunded.
-#### Does purchasing Supabase through the AWS Marketplace count toward your AWS spend commitment?
+## Does purchasing Supabase through the AWS Marketplace count toward your AWS spend commitment?
Yes, marketplace purchases do count toward the spend commitment.
diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx
index c4c7567395ddd..4ea5fdb68c7de 100644
--- a/apps/docs/content/guides/platform/billing-faq.mdx
+++ b/apps/docs/content/guides/platform/billing-faq.mdx
@@ -9,26 +9,26 @@ subtitle: 'This documentation covers frequently asked questions around subscript
## Organizations and projects
-#### What are organizations and projects?
+### What are organizations and projects?
The Supabase Platform has "organizations" and "projects". An organization may contain multiple projects. Each project is a dedicated Supabase instance with all of its sub-services including Storage, Auth, Functions and Realtime.
Each organization only has a single subscription with a single plan (Free, Pro, Team or Enterprise). Project add-ons such as [Compute](/docs/guides/platform/compute-and-disk), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/telemetry/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone), [Custom Domains](/docs/guides/platform/custom-domains) and [PITR](/docs/guides/platform/backups#point-in-time-recovery) are configured per project and are added to your organization subscription.
Read more on [About billing on Supabase](/docs/guides/platform/billing-on-supabase#organization-based-billing).
-#### How many free projects can I have?
+### How many free projects can I have?
You are entitled to two active free projects. Paused projects do not count towards your quota. Note that within an organization, we count the free project limits from all members that are either Owner or Admin. If you’ve got another organization member with the Admin or Owner role that has already exhausted their free project quota, you won’t be able to launch another free project in that organization. You can create another Free Plan organization or change the role of the affected member in your [organization’s team settings](/dashboard/org/_/team).
-#### Can I mix free and paid projects in a single organization?
+### Can I mix free and paid projects in a single organization?
The subscription plan is set on the organization level and it is not possible to mix paid and non-paid projects inside a single organization. However, you can have a paid and a free organization and make use of the [self-serve project transfers](/docs/guides/platform/project-transfer) to organize your projects. All projects in an organization benefit from the subscription plan. If your organization is on the Pro Plan, all projects within the organization benefit from no project pausing, automated backups and so on.
-#### Can I transfer my projects to another organization?
+### Can I transfer my projects to another organization?
Yes, you can transfer your projects to another organization. You can find instructions on how to transfer your projects [here](/docs/guides/platform/project-transfer).
-#### Can I transfer my credits to another organization?
+### Can I transfer my credits to another organization?
Yes, you can transfer the credits to another organization. Submit a [support ticket](https://supabase.help).
@@ -36,11 +36,11 @@ Yes, you can transfer the credits to another organization. Submit a [support tic
See the [Pricing page](/pricing) for details.
-#### Are there any charges for paused projects?
+### Are there any charges for paused projects?
No, we do not charge for paused projects. Compute hours are only counted for active instances. Paused projects do not incur any compute usage charges.
-#### How are multiple projects billed under a paid organization?
+### How are multiple projects billed under a paid organization?
We provide a dedicated server for every Supabase project. Each paid organization comes with
in Compute Credits to cover one project on the default compute size. Additional projects start at ~
a month (billed hourly).
@@ -52,7 +52,7 @@ Running 3 projects in a Pro Plan organization on the default Micro instance:
Refer to our [Compute](/docs/guides/platform/manage-your-usage/compute#billing-examples) docs for more examples and insights.
-#### How does compute billing work?
+### How does compute billing work?
Each Supabase project is a dedicated VM and Postgres database. By default, your instance runs on the Micro compute instance. You have the option to upgrade your compute size in your [Project settings](/dashboard/project/_/settings/addons). See [Compute Add-ons](/docs/guides/platform/compute-and-disk) for available options.
@@ -64,7 +64,7 @@ If you upgrade your project to a larger instance for 10 hours and then downgrade
Read more about [Compute usage](/docs/guides/platform/manage-your-usage/compute).
-#### What is egress and how is it billed?
+### What is egress and how is it billed?
Egress refers to the total bandwidth (network traffic) quota available to each organization. This quota can be used for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly.
@@ -74,41 +74,41 @@ Read more about [Egress usage](/docs/guides/platform/manage-your-usage/egress).
## Plans and subscriptions
-#### How do I change my subscription plan?
+### How do I change my subscription plan?
Change your subscription plan in your [organization's billing settings](/dashboard/org/_/billing). To upgrade to an Enterprise Plan, complete the [Enterprise request form](https://forms.supabase.com/enterprise).
-#### What happens if I cancel my subscription?
+### What happens if I cancel my subscription?
The organization is given [credits](/docs/guides/platform/credits) for unused time on the subscription plan. The credits will not expire and can be used again in the future. You may see an additional charge for unbilled excessive usage charges from your previous billing cycle.
Read more about [downgrades](/docs/guides/platform/manage-your-subscription#downgrade).
-#### I mistakenly upgraded the wrong organization and then downgraded it. Could you issue a refund?
+### I mistakenly upgraded the wrong organization and then downgraded it. Could you issue a refund?
We can transfer the amount as [credits](/docs/guides/platform/credits) to another organization of your choice. You can use these credits to upgrade the organization, or if you have already upgraded, the credits will be used to pay the next month's invoice. Please create a [support ticket](https://supabase.help) for this case.
-#### How do I get an annual subscription?
+### How do I get an annual subscription?
We currently do not support annual plans officially. However, you can do a [credit top-up](/docs/guides/platform/credits#credit-top-ups) to avoid monthly payments.
## Quotas and spend caps
-#### What will happen when I exceed the Free Plan quota?
+### What will happen when I exceed the Free Plan quota?
You will be notified when you exceed the Free Plan quota. It is important to take action at this point. If you continue to exceed the limits, service restrictions will apply. To avoid service restrictions, you can [manage your usage](/docs/guides/platform/manage-your-usage) or upgrade to a paid plan. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section.
-#### What will happen when I exceed the Pro Plan quota and have the spend cap on?
+### What will happen when I exceed the Pro Plan quota and have the spend cap on?
You will be notified when you exceed your Pro Plan quota. To unblock yourself, you can toggle off your spend cap in your [organization's billing settings](/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without managing your usage or turning off the spend cap, restrictions will apply. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section.
-#### How do I scale beyond the limits of my Pro Plan?
+### How do I scale beyond the limits of my Pro Plan?
The Pro Plan has a Spend Cap enabled by default to keep costs under control. If you want to scale beyond the plan's included quota, switch off the Spend Cap to pay for additional usage beyond the plans included limits. You can toggle the Spend Cap in the [organization's billing settings](/dashboard/org/_/billing). Read more about the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
## Fair Use Policy
-#### What is the Fair Use Policy?
+### What is the Fair Use Policy?
Our Fair Use Policy gives developers the freedom to build and experiment with Supabase, while protecting our infrastructure. Under the Fair Use policy, service restrictions may apply to your organization if:
@@ -119,13 +119,13 @@ Our Fair Use Policy gives developers the freedom to build and experiment with Su
You will receive a notification before Fair Use Policy restrictions are applied. However, in some cases, like suspected abuse of our services, restrictions may be applied without prior notice.
-#### What is a grace period and does it reset after usage drops?
+### What is a grace period and does it reset after usage drops?
When your organization exceeds plan limits, you receive a grace period before fair use policy applies. After this grace period ends, the dashboard will continue to show a notice indicating that your grace period is over, even if you have dropped back under plan limits. This is a warning that serves as an indicator that your organization previously exceeded usage limits.
This persistent warning means that if you exceed your plan limits again, you will not receive another grace period and your project will be restricted. The notice and indicator will automatically clear if you continue to stay under plan limits for multiple billing cycles.
-#### How is the Fair Use Policy applied?
+### How is the Fair Use Policy applied?
The Fair Use Policy is applied through service restrictions. This could mean:
@@ -136,7 +136,7 @@ The Fair Use Policy is applied through service restrictions. This could mean:
The Fair Use Policy is generally applied to all projects of the restricted organization.
-#### How can I remove restrictions applied from the Fair Use Policy?
+### How can I remove restrictions applied from the Fair Use Policy?
To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted.
@@ -150,69 +150,69 @@ Pausing or deleting a project stops new usage from accumulating, but does not re
## Reports and invoices
-#### Where do I find my invoices?
+### Where do I find my invoices?
You can find all invoices from your organization on your [organization’s invoices page](/dashboard/org/_/billing#invoices).
-#### Where can I see a breakdown of usage?
+### Where can I see a breakdown of usage?
You can find the breakdown of your usage on your [organization’s usage page](/dashboard/org/_/usage).
-#### Where can I check my credit balance?
+### Where can I check my credit balance?
You can check your Credit balance on the [organization’s billing page](/dashboard/org/_/billing). Credits will be used on future invoices before charging your payment method. If you have enough credits to cover an invoice, there is no charge at all.
-#### Can I change the details of an existing invoice?
+### Can I change the details of an existing invoice?
Any changes made to your billing details will only be reflected in your upcoming invoices. Our payment provider cannot regenerate previous invoices. Therefore, make sure to update the billing details before the upcoming invoices are finalized.
## Payments and billing cycle
-#### What payment methods are available?
+### What payment methods are available?
We accept credit card payments only. If you cannot pay via credit card, we do offer alternatives for larger upfront payments. Create a [support ticket](https://supabase.help) in case you’re interested.
-#### What credit card brands are supported?
+### What credit card brands are supported?
Visa, Mastercard, American Express, Japan Credit Bureau (JCB), China UnionPay (CUP), Cartes Bancaires
-#### What currency can I pay in?
+### What currency can I pay in?
All our invoices are issued in USD, but you can pay in any currency so long as the credit card provider allows charging in USD after conversion.
-#### Can I change the payment method?
+### Can I change the payment method?
Yes, you will have to add the new payment method before being allowed to remove the old one.
This can be done from your dashboard on the [organization’s billing page](/dashboard/org/_/billing).
Read more on [Manage your payment methods](/docs/guides/platform/manage-your-subscription#manage-your-payment-methods).
-#### Can I pay upfront for multiple months?
+### Can I pay upfront for multiple months?
You can top up your credit balance to cover multiple months through your [organization’s billing page](/dashboard/org/_/billing).
Read more on [Credit top-ups](/docs/guides/platform/credits#credit-top-ups).
-#### When are payments taken?
+### When are payments taken?
Payments are taken at the beginning of each billing cycle. You will be charged once a month. You can see the current billing cycle and upcoming invoice in your [organization's billing settings](/dashboard/org/_/billing). The subscription plan fee is charged upfront, whereas usage-charges, including compute, are charged in arrears based on your usage.
Read more on [Your monthly invoice](/docs/guides/platform/your-monthly-invoice).
-#### Where can I change my billing details?
+### Where can I change my billing details?
You can update your billing details on the [organization’s billing page](/dashboard/org/_/billing).
Note that any changes made to your billing details will only be reflected in your upcoming invoices. Our payment provider cannot regenerate previous invoices.
-#### What happens if I am unable to make the payment?
+### What happens if I am unable to make the payment?
When an invoice becomes overdue, we will pause your projects and downgrade your organization to the Free Plan. You will be able to restore your projects once you have paid all outstanding invoices.
-#### Can I use a credit top-up to pay an outstanding invoice?
+### Can I use a credit top-up to pay an outstanding invoice?
Credit top-ups apply only to future invoices. They cannot be used to pay or adjust outstanding invoices.
-#### Why am I overdue?
+### Why am I overdue?
We were unable to charge your payment method. This likely means that the payment was not successfully processed with the credit card on your account profile.
You can be overdue when
@@ -227,39 +227,39 @@ If you are still facing issues, raise a [support ticket](https://supabase.help).
Payments are always in USD and may show up as coming from Singapore, given our payment entity is in Singapore. Make sure you allow payments from Singapore and in USD
-#### Can I delay my payment?
+### Can I delay my payment?
No, you cannot delay your payment.
-#### Can I get a refund of my unused credits?
+### Can I get a refund of my unused credits?
No, we do not provide refunds. Please refer to our [Terms of Service](/terms#1-fees).
-#### What do I do if my bill looks wrong?
+### What do I do if my bill looks wrong?
Take a moment to review our [Your monthly invoice](/docs/guides/platform/your-monthly-invoice) page, which may help clarify any questions about your invoice. If it still looks wrong, submit a [support ticket](https://supabase.help) through the dashboard. Select the affected organization and provide the invoice number for us to look at your case.
## Taxes
-#### Does Supabase charge sales tax, VAT or GST?
+### Does Supabase charge sales tax, VAT or GST?
Supabase is rolling out sales tax in applicable US states, and VAT, GST, and other indirect taxes for customers where required by law. The tax amount applied to your invoice depends on your billing address and the tax regulations in your jurisdiction.
-#### Why is Supabase collecting tax now?
+### Why is Supabase collecting tax now?
As a cloud services provider operating globally, Supabase is required to collect and remit indirect taxes in an increasing number of jurisdictions. We’re updating our billing practices to meet these obligations and ensure compliance with local tax regulations.
-#### Will every customer be charged tax?
+### Will every customer be charged tax?
No. Tax is only applied in jurisdictions where Supabase is registered to collect it. If your billing address is in one of those jurisdictions, you’ll see tax applied on your invoice. If it is not, your invoices will not include tax.
-#### When will customers start seeing tax on their invoices?
+### When will customers start seeing tax on their invoices?
We are progressively rolling out tax collection across international jurisdictions. The roll out begins on May 1, 2026 and completes by June 30, 2026.
You will receive an email notification in advance of any changes to your invoicing.
-#### Do I need to do anything?
+### Do I need to do anything?
For most customers, nothing changes on your end. Supabase automatically calculates the applicable tax based on the billing address associated with your organization.
@@ -268,11 +268,11 @@ There are two cases where action may be needed:
- If your organization is VAT- or GST-registered, make sure you have entered a valid Tax ID in your [organization’s billing page](/dashboard/org/_/billing#address). This allows us to apply the correct tax treatment, such as reverse charge for eligible B2B transactions.
- If your organization is tax-exempt, submit your exemption certificate to [tax-documents@supabase.io](mailto:tax-documents@supabase.io) so we can verify and apply the exemption to your organization.
-#### What if my billing address is missing or incorrect?
+### What if my billing address is missing or incorrect?
A valid billing address is required for us to calculate the correct tax and comply with tax regulations. Make sure your billing address is up to date in your [organization’s billing page](/dashboard/org/_/billing#address) to avoid any disruption.
-#### Where do I add my tax ID?
+### Where do I add my tax ID?
You can add or update your Tax ID directly in the Supabase Dashboard under your [organization’s billing page](/dashboard/org/_/billing#address).
@@ -280,24 +280,24 @@ Providing a valid Tax ID ensures we apply the correct tax treatment for your reg
If you do not see an option for your country’s Tax ID format, please open a [support ticket](https://supabase.help) and we’ll make sure it is recorded on your account.
-#### What if my organization is tax-exempt?
+### What if my organization is tax-exempt?
If your organization qualifies for a tax exemption, email your exemption certificate to [tax-documents@supabase.io](mailto:tax-documents@supabase.io).
Our team will verify the certificate and update your account accordingly. Once approved, tax will no longer be applied to your invoices.
-#### How does tax appear on my invoices?
+### How does tax appear on my invoices?
Tax is shown separately at the bottom of your invoice, clearly broken out from the cost of the products and services you’re subscribed to. This makes it easy to distinguish between your subscription costs and any applicable tax.
-#### How is tax handled on prepaid credit top ups or packages?
+### How is tax handled on prepaid credit top ups or packages?
If you purchase a prepaid top up or credit package, tax is assessed at the time of purchase, not when the credits are later consumed against usage or subscription invoices. This ensures the correct tax rate is applied based on your billing address at the time of purchase.
-#### Are marketplace purchases affected?
+### Are marketplace purchases affected?
If you use Supabase through a cloud marketplace such as AWS Marketplace or Vercel Marketplace, the marketplace provider handles tax collection and remittance. In those cases, Supabase does not separately charge tax on marketplace-billed invoices.
-#### Who should I contact with questions about tax?
+### Who should I contact with questions about tax?
For questions about tax collection, exemptions, or your Tax ID, please open a [support ticket](https://supabase.help).
diff --git a/apps/docs/content/guides/platform/ipv4-address.mdx b/apps/docs/content/guides/platform/ipv4-address.mdx
index f17be34c3cda7..6c9666211d28c 100644
--- a/apps/docs/content/guides/platform/ipv4-address.mdx
+++ b/apps/docs/content/guides/platform/ipv4-address.mdx
@@ -100,7 +100,7 @@ nslookup db.
.supabase.co
The pooler and direct connection strings can be found in the [project connect page](/dashboard/project/_?showConnect=true):
-#### Direct connection
+### Direct connection
IPv6 unless IPv4 Add-On is enabled
@@ -109,7 +109,7 @@ IPv6 unless IPv4 Add-On is enabled
postgresql://postgres:[YOUR-PASSWORD]@db.ajrbwkcuthywfihaarmflo.supabase.co:5432/postgres
```
-#### Supavisor in transaction mode (port 6543)
+### Supavisor in transaction mode (port 6543)
Always uses an IPv4 address
@@ -118,7 +118,7 @@ Always uses an IPv4 address
postgresql://postgres.ajrbwkcuthywddfihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
```
-#### Supavisor in session mode (port 5432)
+### Supavisor in session mode (port 5432)
Always uses an IPv4 address
diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx
index 444fb1e514d34..5a38ace3123ef 100644
--- a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx
+++ b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx
@@ -213,7 +213,7 @@ These steps cover a manual logical restore (`pg_dump` / `psql`) into a project y
### Special considerations
-##### Preserving migration history
+#### Preserving migration history
If you were using Supabase CLI for managing migrations on your old database and would like to preserve the migration history in your newly restored project, you need to insert the migration records separately using the following commands.
@@ -228,7 +228,7 @@ psql \
--dbname "$NEW_DB_URL"
```
-##### Schema changes to `auth` and `storage`
+#### Schema changes to `auth` and `storage`
If you have modified the `auth` and `storage` schemas in your old project, such as adding triggers or Row Level Security(RLS) policies, you have to restore them separately. The Supabase CLI can help you diff the changes to these schemas using the following commands.
@@ -239,11 +239,11 @@ supabase db diff --linked --schema auth,storage > changes.sql
### Troubleshooting notes
-##### Disabling triggers during restore:
+#### Disabling triggers during restore:
Setting `session_replication_role` to `replica` disables triggers during the migration, preventing columns from being double encrypted.
-##### Custom roles require passwords
+#### Custom roles require passwords
If you created any [custom roles](/dashboard/project/_/database/roles) with the `LOGIN` attribute, you must manually set their passwords in the new project. This can be done with the SQL command:
@@ -251,7 +251,7 @@ If you created any [custom roles](/dashboard/project/_/database/roles) with the
alter user "YOUR_USER" with password 'SOME_NEW_PASSWORD';
```
-##### `supabase_admin` permission errors
+#### `supabase_admin` permission errors
If you encounter permission errors related to `supabase_admin` during restore:
@@ -262,7 +262,7 @@ If you encounter permission errors related to `supabase_admin` during restore:
ALTER ... OWNER TO "supabase_admin"
```
-##### `cli_login_postgres` role grant error
+#### `cli_login_postgres` role grant error
If you encounter the error:
@@ -278,7 +278,7 @@ DETAIL: Only roles with the ADMIN option on role "postgres" may grant this role
GRANT "postgres" TO "cli_login_postgres" WITH INHERIT FALSE GRANTED BY "supabase_admin";
```
-##### `cli_login_postgres` role issues after cloning
+#### `cli_login_postgres` role issues after cloning
The `cli_login_role` must be created by the `supabase_admin` role. If the migration process cloned over the role before the CLI could generate its own version, it may encounter the error:
diff --git a/apps/docs/content/guides/platform/privatelink.mdx b/apps/docs/content/guides/platform/privatelink.mdx
index d3417eb68a508..2ccb08fc65f4e 100644
--- a/apps/docs/content/guides/platform/privatelink.mdx
+++ b/apps/docs/content/guides/platform/privatelink.mdx
@@ -38,7 +38,7 @@ To use PrivateLink with your Supabase project:
## Getting started
-#### Step 1: Add AWS account
+### Step 1: Add AWS account
Navigate to your project's Integrations section to set up PrivateLink:
@@ -52,7 +52,7 @@ Navigate to your project's Integrations section to set up PrivateLink:
After submission, Supabase creates a VPC Lattice Resource Configuration for your project and sends an AWS Resource Share to the specified AWS Account ID. This process may take a few moments. Once complete, the account will show a "Ready" status, indicating that the resource share has been sent to your AWS account and is ready to be accepted.
-#### Step 2: Accept resource share
+### Step 2: Accept resource share
Supabase will send you an AWS Resource Share containing the VPC Lattice Resource Configurations for your projects. To accept this share:
@@ -69,7 +69,7 @@ Supabase will send you an AWS Resource Share containing the VPC Lattice Resource
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
After accepting, you'll see the resource configurations appear in your [Shared with me > Shared resources](https://console.aws.amazon.com/ram/home#SharedResources) section of the RAM console and the [PrivateLink and Lattice > Resource configurations](https://console.aws.amazon.com/vpcconsole/home#ResourceConfigs) section of the VPC console.
-#### Step 3: Configure security groups
+### Step 3: Configure security groups
Ensure your security groups allow traffic on the appropriate ports:
@@ -81,11 +81,11 @@ Ensure your security groups allow traffic on the appropriate ports:
- Destination that is appropriate for your network. i.e. the subnet of your VPC or security group of your application instances
5. Finish creating the security group by clicking **Create security group**
-#### Step 4: Create connection
+### Step 4: Create connection
In your AWS account, you have two options to establish connectivity:
-##### Option A: Create a PrivateLink endpoint
+#### Option A: Create a PrivateLink endpoint
1. Navigate to the VPC console in your AWS account
2. Go to [Endpoints](https://console.aws.amazon.com/vpcconsole/home#Endpoints:) in the left sidebar
@@ -106,7 +106,7 @@ In your AWS account, you have two options to establish connectivity:
- The IP addresses of the endpoint will be listed in the **Subnets** section of the endpoint details
- The DNS record will be in the **Associations** section of the endpoint details in the **DNS Name** field if you enabled it in step 8
-##### Option B: Attach resource configuration to an existing VPC lattice service network
+#### Option B: Attach resource configuration to an existing VPC lattice service network
1. **This method is only recommended if you have an existing VPC Lattice Service Network**
2. Navigate to the VPC Lattice console in your AWS account
@@ -118,7 +118,7 @@ In your AWS account, you have two options to establish connectivity:
8. After creation, you will see the resource configuration in the Resource configurations section of your service network with the status "Active"
9. For connectivity, click on the association details and the domain name will be listed in the **DNS entries** section
-#### Step 5: Test connectivity
+### Step 5: Test connectivity
Verify the private connection is working correctly from your VPC:
@@ -132,7 +132,7 @@ psql "postgresql://[username]:[password]@[private-endpoint]:5432/postgres"
You should see a successful connection without any public internet traffic.
-#### Step 6: Update applications
+### Step 6: Update applications
Configure your applications to use the private connection details:
@@ -151,7 +151,7 @@ postgresql://user:pass@db.[project-ref].supabase.co:5432/postgres
postgresql://user:pass@your-private-endpoint.vpce.amazonaws.com:5432/postgres
```
-#### Step 7: Disable public connectivity (optional)
+### Step 7: Disable public connectivity (optional)
For maximum security, you can disable public internet access for your database:
diff --git a/apps/docs/content/guides/queues/api.mdx b/apps/docs/content/guides/queues/api.mdx
index 178c65272454f..22c5784d3c148 100644
--- a/apps/docs/content/guides/queues/api.mdx
+++ b/apps/docs/content/guides/queues/api.mdx
@@ -7,7 +7,7 @@ When you create a Queue in Supabase, you can choose to create helper database fu
Database functions in `pgmq_public` can be exposed via Supabase Data API so consumers client-side can call them. Visit the [Quickstart](/docs/guides/queues/quickstart) for an example.
-### `pgmq_public.pop(queue_name)`
+## `pgmq_public.pop(queue_name)`
Retrieves the next available message and deletes it from the specified Queue.
@@ -15,7 +15,7 @@ Retrieves the next available message and deletes it from the specified Queue.
---
-### `pgmq_public.send(queue_name, message, sleep_seconds)`
+## `pgmq_public.send(queue_name, message, sleep_seconds)`
Adds a Message to the specified Queue, optionally delaying its visibility to all consumers by a number of seconds.
@@ -25,7 +25,7 @@ Adds a Message to the specified Queue, optionally delaying its visibility to all
---
-### `pgmq_public.send_batch(queue_name, messages, sleep_seconds)`
+## `pgmq_public.send_batch(queue_name, messages, sleep_seconds)`
Adds a batch of Messages to the specified Queue, optionally delaying their availability to all consumers by a number of seconds.
@@ -35,7 +35,7 @@ Adds a batch of Messages to the specified Queue, optionally delaying their avail
---
-### `pgmq_public.archive(queue_name, message_id)`
+## `pgmq_public.archive(queue_name, message_id)`
Archives a Message by moving it from the Queue table to the Queue's archive table.
@@ -44,7 +44,7 @@ Archives a Message by moving it from the Queue table to the Queue's archive tabl
---
-### `pgmq_public.delete(queue_name, message_id)`
+## `pgmq_public.delete(queue_name, message_id)`
Permanently deletes a Message from the specified Queue.
@@ -53,7 +53,7 @@ Permanently deletes a Message from the specified Queue.
---
-### `pgmq_public.read(queue_name, sleep_seconds, n)`
+## `pgmq_public.read(queue_name, sleep_seconds, n)`
Reads up to "n" Messages from the specified Queue with an optional "sleep_seconds" (visibility timeout).
diff --git a/apps/docs/content/guides/resources.mdx b/apps/docs/content/guides/resources.mdx
index 6e29ac25665ac..5268fbf5927c0 100644
--- a/apps/docs/content/guides/resources.mdx
+++ b/apps/docs/content/guides/resources.mdx
@@ -32,7 +32,7 @@ hideToc: true
-### Migrate to Supabase
+## Migrate to Supabase
@@ -134,7 +134,7 @@ hideToc: true
-### Postgres resources
+## Postgres resources
diff --git a/apps/docs/content/guides/security/hipaa-compliance.mdx b/apps/docs/content/guides/security/hipaa-compliance.mdx
index c387b5c694781..f56ed670b18de 100644
--- a/apps/docs/content/guides/security/hipaa-compliance.mdx
+++ b/apps/docs/content/guides/security/hipaa-compliance.mdx
@@ -14,7 +14,7 @@ The hosted Supabase platform has the necessary controls to meet HIPAA requiremen
-### Customer responsibilities
+## Customer responsibilities
Covered entities (the customer) are organizations that directly handle PHI, such as health plans, healthcare clearinghouses, and healthcare providers that conduct certain electronic transactions.
@@ -22,7 +22,7 @@ Covered entities (the customer) are organizations that directly handle PHI, such
2. **Business Associate Agreements (BAAs)**: Customers must sign a BAA with Supabase. When the covered entity engages a business associate to help carry out its healthcare activities, it must have a written BAA. This agreement outlines the business associate's responsibilities and requires them to comply with HIPAA Rules.
3. **Internal Compliance Programs**: Customers must [configure their HIPAA projects](/docs/guides/platform/hipaa-projects) and follow the guidance given by the security advisor. Covered entities are responsible for implementing internal processes and compliance programs to ensure they meet HIPAA requirements.
-### Supabase responsibilities
+## Supabase responsibilities
Supabase as the business associate, and the vendors used by Supabase, are the entities that perform functions or activities on behalf of the customer.
diff --git a/apps/docs/content/guides/security/security-testing.mdx b/apps/docs/content/guides/security/security-testing.mdx
index b130d20037244..63b6eab82bfc8 100644
--- a/apps/docs/content/guides/security/security-testing.mdx
+++ b/apps/docs/content/guides/security/security-testing.mdx
@@ -12,7 +12,7 @@ It is the customer’s responsibility to ensure that testing activities are alig
Furthermore, Supabase runs a [Vulnerability Disclosure Program](https://hackerone.com/ca63b563-9661-4ac3-8d23-7581582ef451/embedded_submissions/new) (VDP) with HackerOne, and external security researchers may report any bugs found within the scope of the aforementioned program. Customer penetration testing does not form part of this VDP.
-### Permitted services
+## Permitted services
- Authentication
- Database
@@ -22,7 +22,7 @@ Furthermore, Supabase runs a [Vulnerability Disclosure Program](https://hackeron
- `https://
.supabase.co/*`
- `https://db..supabase.co/*`
-### Prohibited testing and activities
+## Prohibited testing and activities
- Any activity contrary to what is listed in the AUP.
- Denial of Service (DoS) and Distributed Denial of Service (DDoS) testing.
diff --git a/apps/docs/content/guides/security/soc-2-compliance.mdx b/apps/docs/content/guides/security/soc-2-compliance.mdx
index 7e202e4d2a4ea..efeb650bd81a9 100644
--- a/apps/docs/content/guides/security/soc-2-compliance.mdx
+++ b/apps/docs/content/guides/security/soc-2-compliance.mdx
@@ -25,14 +25,14 @@ Our [HIPAA documentation](/docs/guides/security/hipaa-compliance) provides more
SOC 2 compliance is a critical aspect of data security for Supabase and our customers. Being fully SOC 2 compliant is a shared responsibility and here’s a breakdown of the responsibilities for both parties:
-#### Supabase responsibilities
+### Supabase responsibilities
1. **Security Measures**: Supabase implements robust security controls to protect customer data. These includes measures to prevent data breaches and ensure the confidentiality and integrity of the information managed and stored by the platform. Supabase is obliged to be vigilant about security risks and must demonstrate that our security measures meet industry standards through regular audits.
2. **Compliance Audits**: Supabase undergoes SOC 2 audits yearly to verify that our data management practices comply with the Trust Services Criteria (TSC), which include security, availability, processing integrity, confidentiality, and privacy. These audits are conducted by an independent third party.
3. **Incident Response**: Supabase has an incident response plan in place to handle data breaches efficiently. This plan outlines how the organization detects issues, responds to incidents, and manages system vulnerabilities.
4. **Reporting**: Upon a successful audit, Supabase receive a SOC 2 report that details our compliance status. This report is available to customers as a SOC 2 Type 2 report, and allows customers and stakeholders to assure that Supabase has implemented adequate and the requisite safeguards to protect sensitive information.
-#### Customer responsibilities
+### Customer responsibilities
1. **Compliance Requirements**: Understand your own compliance requirements. While SOC 2 compliance is not a legal requirement, many enterprise customers require their providers to have a SOC 2 report. This is because it provides assurance that the provider has implemented robust controls to protect customer data.
2. **Due Diligence**: Customers must perform due diligence when selecting Supabase as a provider. This includes reviewing the SOC 2 Type 2 report to ensure that Supabase meets the expected security standards. Customers should also understand the division of responsibilities between themselves and Supabase to avoid duplication of effort.
@@ -40,7 +40,7 @@ SOC 2 compliance is a critical aspect of data security for Supabase and our cust
4. **Control Compliance**: If a customer needs to be SOC 2 compliant, they should themselves implement the requisite controls and undergo a SOC 2 audit.
5. **Audit logging**: Supabase sets [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) to off by default for new projects. If your SOC 2 program requires connection audit evidence, enable connection logging and define how you retain and review those logs.
-#### Shared responsibilities
+### Shared responsibilities
1. **Data Security**: Both customers and Supabase share the responsibility of ensuring data security. While the Supabase, as the provider, implements the security controls, the customer must ensure that their use of the Supabase platform does not compromise these controls.
2. **Control Compliance**: Supabase asserts through our SOC 2 that all requisite security controls are met. Customers wishing to also be SOC 2 compliant need to go through their own SOC 2 audit, verifying that security controls are met on the customer's side.
diff --git a/apps/docs/content/guides/self-hosting.mdx b/apps/docs/content/guides/self-hosting.mdx
index 7858060697cf2..9345b7b846f76 100644
--- a/apps/docs/content/guides/self-hosting.mdx
+++ b/apps/docs/content/guides/self-hosting.mdx
@@ -7,7 +7,7 @@ hideToc: true
Self-hosting is a good fit if you need full control over your data, have compliance requirements that prevent you from using managed services, or want to run Supabase in an isolated environment.
-### How self-hosted Supabase differs
+## How self-hosted Supabase differs
Self-hosted Supabase is different from:
@@ -16,7 +16,7 @@ Self-hosted Supabase is different from:
Self-hosted Supabase mimics a single project. Studio doesn't support multiple organizations or projects. Platform-only [features](/features) such as branching, advanced metrics beyond logs, managed backups and PITR, analytics and vector buckets, ETL, and the platform management API are **unavailable** in self-hosted configuration. Most settings are configured through [environment variables](https://github.com/supabase/supabase/blob/master/docker/.env.example).
-### Your responsibilities when self-hosting
+## Your responsibilities when self-hosting
When you self-host, **you are responsible for**:
@@ -28,7 +28,7 @@ When you self-host, **you are responsible for**:
- Backups and disaster recovery
- Monitoring and uptime
-### Telemetry
+## Telemetry
Self-hosted Supabase (run via Docker Compose) **does not phone home or collect any telemetry**.
diff --git a/apps/docs/content/guides/storage/cdn/fundamentals.mdx b/apps/docs/content/guides/storage/cdn/fundamentals.mdx
index 825fd0dc8bd9c..30646fc883077 100644
--- a/apps/docs/content/guides/storage/cdn/fundamentals.mdx
+++ b/apps/docs/content/guides/storage/cdn/fundamentals.mdx
@@ -7,7 +7,7 @@ sidebar_label: 'CDN'
All assets uploaded to Supabase Storage are cached on a Content Delivery Network (CDN) to improve the latency for users all around the world. CDNs are a geographically distributed set of servers or **nodes** which cache content from an **origin server**. For Supabase Storage, the origin is the storage server running in the [same region as your project](/dashboard/project/_/settings/general). Aside from performance, CDNs also help with security and availability by mitigating Distributed Denial of Service (DDoS) and other application attacks.
-### Example
+## Example
The following example shows how a CDN helps with performance.
@@ -27,7 +27,7 @@ Note that CDNs might still evict your object from their cache if it has not been
The cache status of a particular request is sent in the `cf-cache-status` header. A cache status of `MISS` indicates that the CDN node did not have the object in its cache and had to ping the origin to get it. A cache status of `HIT` indicates that the object was sent directly from the CDN.
-### Public vs private buckets
+## Public vs private buckets
Objects in public buckets do not require any authorization to access objects. This leads to a better cache hit rate compared to private buckets.
diff --git a/apps/docs/content/guides/storage/debugging/logs.mdx b/apps/docs/content/guides/storage/debugging/logs.mdx
index 9470a55b25e58..27848a61b9a85 100644
--- a/apps/docs/content/guides/storage/debugging/logs.mdx
+++ b/apps/docs/content/guides/storage/debugging/logs.mdx
@@ -15,9 +15,9 @@ For more details on filtering the log tables, see [Advanced Log Filtering](/docs
-### Example Storage queries for the Logs Explorer
+## Example Storage queries for the Logs Explorer
-#### Filter by status 5XX error
+### Filter by status 5XX error
```sql
select
@@ -37,7 +37,7 @@ order by timestamp desc
limit 100;
```
-#### Filter by status 4XX error
+### Filter by status 4XX error
```sql
select
@@ -57,7 +57,7 @@ order by timestamp desc
limit 100;
```
-#### Filter by method
+### Filter by method
```sql
select id, storage_logs.timestamp, event_message, r.method
@@ -70,7 +70,7 @@ order by timestamp desc
limit 100;
```
-#### Filter by IP address
+### Filter by IP address
```sql
select id, storage_logs.timestamp, event_message, r.remoteAddress
diff --git a/apps/docs/content/guides/storage/production/scaling.mdx b/apps/docs/content/guides/storage/production/scaling.mdx
index 031ae6a62f97b..898589512c754 100644
--- a/apps/docs/content/guides/storage/production/scaling.mdx
+++ b/apps/docs/content/guides/storage/production/scaling.mdx
@@ -12,19 +12,19 @@ Here are some optimizations that you can consider to improve performance and red
If your project has high egress, these optimizations can help reducing it.
-#### Resize images
+### Resize images
Images typically make up most of your egress. By keeping them as small as possible, you can cut down on egress and boost your application's performance. You can take advantage of our [Image Transformation](/docs/guides/storage/serving/image-transformations) service to optimize any image on the fly.
-#### Set a high cache-control value
+### Set a high cache-control value
Using the browser cache can effectively lower your egress since the asset remains stored in the user's browser after the initial download. Setting a high `cache-control` value ensures the asset stays in the user's browser for an extended period, decreasing the need to download it from the server repeatedly. Read more [here](/docs/guides/storage/cdn/smart-cdn#cache-duration)
-#### Limit the upload size
+### Limit the upload size
You have the option to set a maximum upload size for your bucket. Doing this can prevent users from uploading and then downloading excessively large files. You can control the maximum file size by configuring this option at the [bucket level](/docs/guides/storage/buckets/creating-buckets).
-#### Smart CDN
+### Smart CDN
By leveraging our [Smart CDN](/docs/guides/storage/cdn/smart-cdn), you can achieve a higher cache hit rate and therefore lower your egress cached, as we charge less for cached egress (see [egress pricing](/docs/guides/platform/manage-your-usage/egress#pricing)).
diff --git a/apps/docs/content/guides/storage/schema/helper-functions.mdx b/apps/docs/content/guides/storage/schema/helper-functions.mdx
index afdc2ba8d06ce..295639a22c4a9 100644
--- a/apps/docs/content/guides/storage/schema/helper-functions.mdx
+++ b/apps/docs/content/guides/storage/schema/helper-functions.mdx
@@ -8,7 +8,7 @@ sidebar_label: 'Schema'
Supabase Storage provides SQL helper functions which you can use to write RLS policies.
-### `storage.filename()`
+## `storage.filename()`
Returns the name of a file. For example, if your file is stored in `public/subfolder/avatar.png` it would return: `'avatar.png'`
@@ -26,7 +26,7 @@ using (
);
```
-### `storage.foldername()`
+## `storage.foldername()`
Returns an array path, with all of the subfolders that a file belongs to. For example, if your file is stored in `public/subfolder/avatar.png` it would return: `[ 'public', 'subfolder' ]`
@@ -44,7 +44,7 @@ with check (
);
```
-### `storage.extension()`
+## `storage.extension()`
Returns the extension of a file. For example, if your file is stored in `public/subfolder/avatar.png` it would return: `'png'`
@@ -62,7 +62,7 @@ with check (
);
```
-### `storage.allow_only_operation()`
+## `storage.allow_only_operation()`
Returns `true` when the current Storage API operation exactly matches the provided operation name.
@@ -92,7 +92,7 @@ using (
);
```
-### `storage.allow_any_operation()`
+## `storage.allow_any_operation()`
Returns `true` when the current Storage API operation exactly matches any operation in the provided array.
diff --git a/apps/docs/content/guides/storage/serving/image-transformations.mdx b/apps/docs/content/guides/storage/serving/image-transformations.mdx
index 734e2fa391f1a..549a9e663eb3c 100644
--- a/apps/docs/content/guides/storage/serving/image-transformations.mdx
+++ b/apps/docs/content/guides/storage/serving/image-transformations.mdx
@@ -40,6 +40,7 @@ Our client libraries methods like `getPublicUrl` and `createSignedUrl` support t
```ts
import { createClient } from '@supabase/supabase-js'
+
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -131,6 +132,7 @@ To share a transformed image in a private bucket for a fixed amount of time, pro
```ts
import { createClient } from '@supabase/supabase-js'
+
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -206,6 +208,7 @@ To download a transformed image, pass the `transform` option to the `download` f
```ts
import { createClient } from '@supabase/supabase-js'
+
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -314,6 +317,7 @@ In case you'd like to return the original format of the image and **opt-out** fr
```ts
import { createClient } from '@supabase/supabase-js'
+
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -459,6 +463,7 @@ Example:
```ts
import { createClient } from '@supabase/supabase-js'
+
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -567,6 +572,7 @@ Example:
```ts
import { createClient } from '@supabase/supabase-js'
+
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -691,7 +697,7 @@ If you run the official self-hosted stack from the [`supabase/supabase`](https:/
-#### imgproxy configuration:
+### imgproxy configuration:
Deploy an imgproxy container with the following configuration:
@@ -705,7 +711,7 @@ imgproxy:
Note: make sure that this service can only be reachable within an internal network and not exposed to the public internet
-#### Storage API configuration:
+### Storage API configuration:
Once [imgproxy](https://imgproxy.net/) is deployed we need to configure a couple of environment variables in your self-hosted [`storage-api`](https://github.com/supabase/storage-api) service as follows:
diff --git a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx
index 753e8b6bea940..1cb5ba06123c3 100644
--- a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx
+++ b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx
@@ -283,13 +283,13 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa
$Show>
-### Upload URL
+## Upload URL
When uploading using the resumable upload endpoint, the storage server creates a unique URL for each upload, even for multiple uploads to the same path. All chunks will be uploaded to this URL using the `PATCH` method.
This unique upload URL will be valid for **up to 24 hours**. If the upload is not completed within 24 hours, the URL will expire and you'll need to start the upload again. TUS client libraries typically create a new URL if the previous one expires.
-### Concurrency
+## Concurrency
When two or more clients upload to the same upload URL only one of them will succeed. The other clients will receive a `409 Conflict` error. Only 1 client can upload to the same upload URL at a time which prevents data corruption.
@@ -297,7 +297,7 @@ When two or more clients upload a file to the same path using different upload U
If you provide the `x-upsert` header the last client to complete the upload will succeed instead.
-### Uppy example
+## Uppy example
You can check a [full example using Uppy](https://github.com/supabase/supabase/tree/master/examples/storage/resumable-upload-uppy).
@@ -308,7 +308,7 @@ Uppy has integrations with different frameworks:
- [Vue](https://uppy.io/docs/vue/)
- [Angular](https://uppy.io/docs/angular/)
-### Presigned uploads
+## Presigned uploads
Resumable uploads also supports using signed upload tokens to created time-limited URLs that you can share to your users by invoking the `createSignedUploadUrl` method on the SDK and including the returned token in the `x-signature` header of the resumable upload.
diff --git a/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx b/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx
index 4d03e6e93bd89..56ff90e90514b 100644
--- a/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx
+++ b/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx
@@ -23,7 +23,7 @@ We're currently investigating an issue where the tables responsible for keeping
We've documented some of the migrations that run into this issue and their corresponding fix here:
-### Auth: `operator does not exist: uuid = text`
+## Auth: `operator does not exist: uuid = text`
Temporary fix: Run `insert into auth.schema_migrations values ('20221208132122');` via the [SQL editor](/dashboard/project/_/sql/new) to fix the issue.
diff --git a/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx b/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx
index 57d163952f625..80f71ba1ba6b0 100644
--- a/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx
+++ b/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx
@@ -6,16 +6,16 @@ topics = [ "self-hosting" ]
database_id = "03854567-8838-4f12-8a6c-095fc1671d9f"
---
-### Overview
+## Overview
The self-hosted version is pretty similar to the hosted one. It might not always have the latest features right away, but it includes everything you need to get your application up and running.
-### Feature availability
+## Feature availability
To know what features are available in the self-hosted version, refer to the comprehensive list here:
[Supabase Features](/features)
-### Self-hosting documentation
+## Self-hosting documentation
For detailed steps and guidance on how to set up and manage your self-hosted Supabase instance, follow the documentation provided:
[Self-Hosting Guide](/docs/guides/self-hosting)
diff --git a/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx b/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx
index f69691f74910e..d827aaa9ff48c 100644
--- a/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx
+++ b/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx
@@ -17,7 +17,7 @@ Certain queries, like indexing a table or changing a column's data type, are inh
To execute long-running queries, follow the below steps.
-### Install an external SQL client
+## Install an external SQL client
The guide focuses on [psql](/docs/guides/database/psql) but you can use any Postgres client.
@@ -41,7 +41,7 @@ If you are working in an [IPv6 environment](https://github.com/orgs/supabase/dis
-### Increase the query timeout
+## Increase the query timeout
Then you can increase the query timeout solely for your session:
diff --git a/apps/docs/content/troubleshooting/database-api-42501-errors.mdx b/apps/docs/content/troubleshooting/database-api-42501-errors.mdx
index d128a896b5a11..fe0db64c9a859 100644
--- a/apps/docs/content/troubleshooting/database-api-42501-errors.mdx
+++ b/apps/docs/content/troubleshooting/database-api-42501-errors.mdx
@@ -37,15 +37,15 @@ limit 100;
They tend to be caused by one of the following factors.
-### Attempted to access a forbidden schema
+## Attempted to access a forbidden schema
API roles cannot access certain schemas, most notably `auth` and `vault`. This restriction extends to Foreign Data Wrappers relying on `vault`. While you can bypass it using a [security definer function](/docs/guides/database/functions?queryGroups=language&language=sql&queryGroups=example-view&example-view=sql#security-definer-vs-invoker), these schemas are intentionally restricted for security reasons.
-### Attempted to access a custom schema
+## Attempted to access a custom schema
If you created a custom schema, you will have to give the Database API permission to query it. Follow our [Using Custom Schemas guide](/docs/guides/api/using-custom-schemas) for more directions.
-### Missing table-level privileges
+## Missing table-level privileges
If you see an error like `permission denied for table your_table`, the querying role may not have the required privilege for the operation.
@@ -79,10 +79,10 @@ For more information, see [Securing your API](/docs/guides/api/securing-your-api
-### Configured column-level restrictions
+## Configured column-level restrictions
If you've set column-based access in the [Dashboard](/dashboard/project/_/database/column-privileges) or via SQL, queries will fail with a `42501` error when accessing restricted columns. This includes using `select *`, as it expands to include forbidden columns.
-### RLS:
+## RLS:
If the anon or authenticated roles attempt to UPDATE or INSERT values without the necessary RLS permissions, Postgres will return a 42501 error.
diff --git a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx
index c9741c78913b2..7d0c3ab413d4b 100644
--- a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx
+++ b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx
@@ -7,7 +7,7 @@ keywords = [ "prepared", "statements", "transaction", "mode", "disable" ]
database_id = "04801b69-e7eb-4f40-8d41-81110397bbc2"
---
-### It is important to note that although the direct connections and Supavisor in session mode support prepared statements, Supavisor in transaction mode does not.
+## It is important to note that although the direct connections and Supavisor in session mode support prepared statements, Supavisor in transaction mode does not.
## How to disable prepared statements for Supavisor in transaction mode
diff --git a/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx b/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx
index 252b274381a42..69b09ddfba4a2 100644
--- a/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx
+++ b/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx
@@ -164,7 +164,7 @@ from
## Finding errors
-#### API level errors
+### API level errors
The `metadata.request.url` contains PostgREST formatted queries.
@@ -213,7 +213,7 @@ where
PostgREST has an [error reference table](https://postgrest.org/en/v12/references/errors.html) that you can use to interpret status codes.
-#### Database-level errors
+### Database-level errors
However, some errors that are reported through the Database API occur at the Postgres level. If it is not clear which error occurred you should reference the timestamp of the error and try to see if you can find it in the Postgres logs.
diff --git a/apps/docs/content/troubleshooting/edge-function-504-error-response.mdx b/apps/docs/content/troubleshooting/edge-function-504-error-response.mdx
index 86e2d5e939430..0d940386d119a 100644
--- a/apps/docs/content/troubleshooting/edge-function-504-error-response.mdx
+++ b/apps/docs/content/troubleshooting/edge-function-504-error-response.mdx
@@ -11,7 +11,7 @@ An internal 504 from an Edge Function means the function failed to initiate a re
As of now, this limit cannot be increased. If your function always needs more time than allowed, skip to [When optimization isn't enough](#when-optimization-is-not-enough) section. Otherwise, follow the steps below to speed up response times.
-### Step 1: Identifying slow Functions
+## Step 1: Identifying slow Functions
You can filter for 504 events in the Log Explorer with the below [query](/dashboard/project/_/logs/explorer?q=select%0A++cast%28timestamp+as+datetime%29+as+timestamp%2C%0A++req.pathname%2C%0A++res.status_code%2C%0A++metadata.execution_time_ms%0Afrom%0A++function_edge_logs%0A++cross+join+UNNEST%28metadata%29+as+metadata%0A++cross+join+UNNEST%28metadata.request%29+as+req%0A++cross+join+UNNEST%28metadata.response%29+as+res%0Awhere+res.status_code+%3D+504%0Alimit+20%3B):
@@ -53,7 +53,7 @@ limit 20;
Once candidate functions are identified, you can investigate more thoroughly.
-### Step 2: Investigating slow Functions
+## Step 2: Investigating slow Functions
Add `console.time` labels around suspicious sections to pinpoint where time is being spent:
@@ -76,11 +76,11 @@ Common culprits:
- External APIs that are throttling or rate-limiting you
- Loops without a clear escape condition
-### Step 3: Optimize
+## Step 3: Optimize
The below suggestions are some strategies you can pursue to reduce execution times.
-#### Parallelizing requests
+### Parallelizing requests
If requests are independent of one another, rather than calling them sequentially, you can speed up operations by calling them in parallel with [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all):
@@ -96,15 +96,15 @@ const [resultA, resultB] = await Promise.all([
])
```
-#### Splitting up logic
+### Splitting up logic
The Edge Function may be doing more than it needs to. It may be better to split up it's logic into multiple parts that can be called individually and run for shorter periods.
-#### Use background tasks [utilize-background-tasks]
+### Use background tasks [utilize-background-tasks]
You can initiate functions in a background task to be handled independently of the request/response handler. The process is outlined in the [Background Task docs](/docs/guides/functions/background-tasks)
-#### Offload work to the client or an external service
+### Offload work to the client or an external service
Instead of managing all operations within the function itself, there may be an external API that can execute jobs faster and then send back results. Alternatively, you may be able to offload some processing to the requester rather than doing everything within the function itself.
@@ -143,7 +143,7 @@ group by req.pathname, res.status_code
limit 10;
```
-### When optimization is not enough
+## When optimization is not enough
Edge Functions have a hard runtime ceiling that cannot be raised. If your use case genuinely requires more, consider:
diff --git a/apps/docs/content/troubleshooting/high-cpu-and-slow-queries-with-error-must-be-a-superuser-to-terminate-superuser-process.mdx b/apps/docs/content/troubleshooting/high-cpu-and-slow-queries-with-error-must-be-a-superuser-to-terminate-superuser-process.mdx
index 22d1be7e136f9..4f7fbee362a6c 100644
--- a/apps/docs/content/troubleshooting/high-cpu-and-slow-queries-with-error-must-be-a-superuser-to-terminate-superuser-process.mdx
+++ b/apps/docs/content/troubleshooting/high-cpu-and-slow-queries-with-error-must-be-a-superuser-to-terminate-superuser-process.mdx
@@ -7,7 +7,7 @@ database_id = "23668386-7a72-44ff-a412-4cd8a005fa18"
When facing high CPU utilization, slow query performance, and an `ERROR: must be a superuser to terminate superuser process` message regarding an autovacuum, it indicates that a critical, non-terminable autovacuum operation is running on your Postgres database. This guide explains why this happens and what steps you can take.
-### **Core Postgres concepts**
+## Core Postgres concepts
To understand this issue, it's essential to grasp a few core Postgres concepts:
@@ -25,7 +25,7 @@ Every transaction in Postgres is assigned a unique Transaction ID (XID). These X
To prevent this critical issue, Postgres initiates a special autovacuum operation: the **"wraparound prevention vacuum."**
-### **Understanding the problem: A critical autovacuum**
+## Understanding the problem: A critical autovacuum
When you encounter the `ERROR: must be a superuser to terminate superuser process` associated with an autovacuum marked "to prevent wraparound," it signifies that this mandatory, system-critical operation is underway.
@@ -38,7 +38,7 @@ When you encounter the `ERROR: must be a superuser to terminate superuser proces
**Why is it running?**
This situation often arises in large, high-write tables (e.g., `your_table`, which might be hundreds of GBs in size and contain hundreds of millions of rows) that accumulate dead tuples rapidly. When the transaction ID age of the table approaches a critical threshold, Postgres automatically triggers this emergency autovacuum. For instance, if a table has millions of rows and over a million dead rows, exceeding its configured `autovacuum_vacuum_scale_factor` (e.g., 0.2), a regular autovacuum might initiate. However, if the XID age continues to increase, the system prioritizes the wraparound prevention vacuum to safeguard data integrity.
-### **Mitigating performance impact during a critical autovacuum**
+## Mitigating performance impact during a critical autovacuum
Since the wraparound prevention autovacuum cannot be stopped, the best approach is to provide the database with sufficient resources to complete the operation as efficiently as possible.
@@ -52,7 +52,7 @@ Since the wraparound prevention autovacuum cannot be stopped, the best approach
- **Why it helps:** Autovacuum is an I/O-intensive operation, involving a lot of reading and writing. Higher disk performance can significantly speed up the process.
- **Considerations:** Cloud providers may limit the number of disk modifications (e.g., up to four in a rolling 24-hour window). You can start a new modification immediately after the previous one finishes, provided you have not exceeded this rolling 24-hour quota.
-### **Monitoring progress and future prevention**
+## Monitoring progress and future prevention
**Monitoring the Current Autovacuum:**
You can monitor the progress of the active autovacuum processes using the `pg_stat_progress_vacuum` view:
diff --git a/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx b/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx
index ac3880bae9844..c7825cc9c51a3 100644
--- a/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx
+++ b/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx
@@ -9,9 +9,9 @@ database_id = "e7671f20-2639-442e-9df9-4f6fa3766598"
> For the curious: [here is a list of all built-in indexes in Postgres](https://www.postgresql.org/docs/current/indexes-types.html)
-### Postgres internals
+## Postgres internals
-#### How an index is chosen
+### How an index is chosen
Postgres, internally, contains a few components that manage query execution:
@@ -98,11 +98,11 @@ To reset statistics within the database, you can use the following query:
select pg_stat_reset();
```
-### Complex or composite indexes
+## Complex or composite indexes
> For a more complete rundown, check the [Postgres Official Docs](https://www.postgresql.org/docs/current/indexes-multicolumn.html)
-#### Multi-column indexes
+### Multi-column indexes
If you make independent indexes on multiple columns, Postgres will likely use each of them independently to find the relevant rows and then combine the results together.
@@ -118,7 +118,7 @@ from test2
where major = constant and minor = constant;
```
-#### Ordered indexes
+### Ordered indexes
If you're using an ORDER BY clause, [indexes can also be pre-sorted by DESC/ASC](https://www.postgresql.org/docs/current/indexes-ordering.html) for better performance.
@@ -127,7 +127,7 @@ If you're using an ORDER BY clause, [indexes can also be pre-sorted by DESC/ASC]
CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
```
-#### Functional indexes
+### Functional indexes
Although not as common, indexes can also be leveraged against modified values, such as when using a LOWER function:
@@ -139,7 +139,7 @@ create index test1_lower_col1_idx on test1 (lower(col1));
select * from test1 where lower(col1) = 'value';
```
-#### Covering indexes
+### Covering indexes
Indexes contain pointers to a specific row, but you could instruct an index to hold a copy of a column's value for even faster retrieval. These are known as `covering` indexes. Because maintaining a copy is storage intensive, you should avoid using it for values with large data footprints.[ FULL VIDEO ON TOPIC](https://www.youtube.com/watch?v=bBu_V8CfWgM)
@@ -147,7 +147,7 @@ Indexes contain pointers to a specific row, but you could instruct an index to h
CREATE INDEX a_b_idx ON x (a,b) INCLUDE (c);
```
-#### Indexes on JSONB
+### Indexes on JSONB
Although a GIN/GIST index can be used to index entire JSONB bodies, you can also target only specific Key-values with standard BTREE indexes:
diff --git a/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx b/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx
index 34b4650368bd6..4b80798da3fec 100644
--- a/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx
+++ b/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx
@@ -54,17 +54,17 @@ SHOW max_connections;
**Three** factors must be taken into consideration when adjusting the direct connection limit:
-#### Process schedulers and Postgres internals
+### Process schedulers and Postgres internals
Allowing too many direct connections in your database can overburden Postgres schedulers and other internal modules. This will result in a noticeable decrease in query throughput, despite having more connections available. EnterpriseDB wrote a wonderful [article](https://www.enterprisedb.com/postgres-tutorials/why-you-should-use-connection-pooling-when-setting-maxconnections-postgres) that outlines some of the considerations.
The default connection values are set based on a solid understanding of Postgres architecture, and straying too far from them is _likely_ to hinder performance. However, with some experimentation, you might discover a value better suited to your specific needs. Still, unless there's a compelling reason to adjust the setting, it's generally advisable to stick with the defaults or change the values judiciously.'
-#### Memory
+### Memory
> If you do not know how to monitor memory and CPU with Supabase Grafana, [check here](https://github.com/orgs/supabase/discussions/27141).
-##### Each direct connection is a running process that will consume active memory
+#### Each direct connection is a running process that will consume active memory
This is a Grafana Chart of unhealthy memory usage:
@@ -92,7 +92,7 @@ select
) || ' * ' || current_setting('maintenance_work_mem') || ')) / ' || current_setting('work_mem');
```
-#### CPU
+### CPU
The below chart is an example of what can occur to the CPU if 100s of connections are inappropriately opened/closed every second or many CPU intensive queries are run in parallel
diff --git a/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx b/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx
index a05664e26540b..5ed6eea10b049 100644
--- a/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx
+++ b/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx
@@ -191,7 +191,7 @@ Queries can use complex syntax, so it is often helpful to isolate by referenced
All failed queries, including those from PostgREST, Auth, and external libraries (e.g., Prisma) are logged with helpful error messages for debugging.
-##### Server/Role mapping
+#### Server/Role mapping
API servers have assigned database roles for connecting to the database:
@@ -257,7 +257,7 @@ limit 100;
## Logging for compliance and security
-#### Customized object and role activity logging
+### Customized object and role activity logging
> ⚠️ NOTE: This is specifically designated for those using the `postgres` role or [custom roles](/docs/guides/database/postgres/roles) to interact with their database. Those using the Database REST API should reference the [Database API Logging Guide](https://github.com/orgs/supabase/discussions/22849) instead.
@@ -279,7 +279,7 @@ where
parsed.user_name = 'API_role'
```
-#### Filtering by IP
+### Filtering by IP
> If you are connecting from a known, limited range of IP addresses, you should enable [network restrictions](/docs/guides/platform/network-restrictions).
@@ -341,7 +341,7 @@ where
> WARNING: lenient settings can lead to over-logging, impacting database performance while creating noise in the logs.
-##### Severity levels
+#### Severity levels
The `log_min_messages` variable determines what is severe enough to log. Here are the severity thresholds from the [Postgres docs](https://www.postgresql.org/docs/current/runtime-config-logging.html).
@@ -365,7 +365,7 @@ alter role postgres set log_min_messages = '';
show log_min_messages; -- default WARNING
```
-##### Configuring queries logged
+#### Configuring queries logged
By default, only failed queries are logged. The [PGAudit extension](/docs/guides/database/extensions/pgaudit) extends Postgres's built-in logging abilities. It can be used to selectively track all queries in your database by:
@@ -374,25 +374,25 @@ By default, only failed queries are logged. The [PGAudit extension](/docs/guides
- database object
- entire database
-##### Logging within database functions
+#### Logging within database functions
To track or debug functions, logging can be configured by following the [function debugging guide](/docs/guides/database/functions#general-logging)
## Frequently Asked Questions
-##### How to join different log tables
+### How to join different log tables
No, log tables are independent from each other and do not share any primary/foreign key relations for joining.
-##### How to download logs
+### How to download logs
At the moment, the way to download logs is through the Log Dashboard as a CSV
-##### What is logged?
+### What is logged?
To see the default types of events that are logged, you can check this [guide](https://gist.github.com/TheOtherBrian1/991d32c2b00dbc75d29b80d4cdf41aa7).
-#### Other resources:
+### Other resources:
- [Regex for filtering logs](https://github.com/orgs/supabase/discussions/22640)
- [Debugging with the DB API logs](https://github.com/orgs/supabase/discussions/22849)
diff --git a/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx b/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx
index d35d2376ae2c8..52f2f3f15e5aa 100644
--- a/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx
+++ b/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx
@@ -13,7 +13,7 @@ Here are the steps for you to migrate your application from the `auth-helpers` p
Depending on your implementation, you may ignore some parts of this documentation and use your own implementation (i.e. using API routes vs. Server Actions). What's important is you replace the clients provided by `auth-helpers` with the utility functions created using clients provided by `@supabase/ssr`.
-### 1. Uninstall Supabase Auth helpers and install the Supabase SSR package
+## 1. Uninstall Supabase Auth helpers and install the Supabase SSR package
It's important that you don't use both `auth-helpers-nextjs` and `@supabase/ssr` packages in the same application to avoid running into authentication issues.
@@ -22,7 +22,7 @@ npm uninstall @supabase/auth-helpers-nextjs @supabase/supabase-js
npm install @supabase/ssr @supabase/supabase-js
```
-### 2. Create the library functions to create Supabase clients
+## 2. Create the library functions to create Supabase clients
```
// lib/supabase/client.ts
@@ -136,7 +136,7 @@ export async function updateSession(request: NextRequest) {
}
```
-### 3. Replace your proxy.ts file
+## 3. Replace your proxy.ts file
```
// proxy.ts
@@ -162,7 +162,7 @@ export const config = {
}
```
-### 4. Create your server actions to handle login and sign up
+## 4. Create your server actions to handle login and sign up
```
// app/login/actions.ts
@@ -215,7 +215,7 @@ export async function signup(formData: FormData) {
}
```
-### 5. Use the server actions in your login page UI
+## 5. Use the server actions in your login page UI
```
// app/login/page.tsx
@@ -236,7 +236,7 @@ export default function LoginPage() {
}
```
-### 6. Client components
+## 6. Client components
```
'use client';
@@ -258,7 +258,7 @@ export default async function Page() {
}
```
-### 7. Server components
+## 7. Server components
```
// replace
@@ -282,7 +282,7 @@ export default async function Page() {
}
```
-### 8. Route handlers
+## 8. Route handlers
```
// replace
diff --git a/apps/docs/content/troubleshooting/http-api-issues.mdx b/apps/docs/content/troubleshooting/http-api-issues.mdx
index 936ec4d58c522..de8836ef69d07 100644
--- a/apps/docs/content/troubleshooting/http-api-issues.mdx
+++ b/apps/docs/content/troubleshooting/http-api-issues.mdx
@@ -17,7 +17,7 @@ Symptoms of HTTP API issues include:
- 5xx response codes
- High response times
-### Under-provisioned resources
+## Under-provisioned resources
The most common class of issues that causes HTTP timeouts and 5xx response codes is the under-provisioning of resources for your project. This can cause your project to be unable to service the traffic it is receiving.
diff --git a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx
index a35f30c5325ec..9312406008910 100644
--- a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx
+++ b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx
@@ -11,7 +11,7 @@ database_id = "7d755701-747f-4c3a-b8be-236c5518e4eb"
> Building an index without the `CONCURRENTLY` modifier will lock the table, but it will also increase build times. For general advice about indexes, check out this [guide](https://github.com/orgs/supabase/discussions/22449).
-### **To speed up queries, it is ideal to create an HSNW index on your embedded column**
+## **To speed up queries, it is ideal to create an HSNW index on your embedded column**
The general structure for creating an HNSW index follows this pattern:
diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx
index eba2626d04573..a5828b5c87372 100644
--- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx
+++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx
@@ -25,13 +25,13 @@ The CPU chart shows 4 distinct metrics of interest:
As the CPU peaks towards 100%, queries and database tasks will begin to throttle, as they won't have enough time or access to the CPU.
-#### Other useful Supabase Grafana guides:
+### Other useful Supabase Grafana guides:
- [Connections](https://github.com/orgs/supabase/discussions/27141)
- [Disk](https://github.com/orgs/supabase/discussions/27003)
- [Memory](https://github.com/orgs/supabase/discussions/27021)
-#### Optimizing
+### Optimizing
1. [Optimize your queries](/docs/guides/database/query-optimization).
2. [Add indexes](https://github.com/orgs/supabase/discussions/22449) if possible.
diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx
index 430f998536714..394a440fddb76 100644
--- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx
+++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx
@@ -61,7 +61,7 @@ Other useful Supabase Grafana guides:
- [Memory](https://github.com/orgs/supabase/discussions/27021)
- [CPU](https://github.com/orgs/supabase/discussions/27022)
-### Esoteric factors
+## Esoteric factors
**Webhooks**:
Supabase webhooks use the pg_net extension to handle requests. The `net.http_request_queue` table isn't indexed to keep write costs low. However, if you upload millions of rows to a webhook-enabled table in rapid succession, it can significantly increase the read costs for the extension.
diff --git a/apps/docs/content/troubleshooting/new-branch-doesnt-copy-database.mdx b/apps/docs/content/troubleshooting/new-branch-doesnt-copy-database.mdx
index 5449693bd7224..f601bb17c7a6c 100644
--- a/apps/docs/content/troubleshooting/new-branch-doesnt-copy-database.mdx
+++ b/apps/docs/content/troubleshooting/new-branch-doesnt-copy-database.mdx
@@ -7,7 +7,7 @@ database_id = "c5dae428-8dd0-4879-b312-df7f7da25986"
Branching in Supabase (Branching 2.0) relies on the current migration files in your project—**not** a schema dump—when creating environments from `main`. This means if your `main` branch lacks migration history, branching will not fully capture your schema. This is a known limitation highlighted in the [Branching 2.0 documentation](/blog/branching-2-0#current-limitations). Follow the steps below to generate, synchronize, and repair your migration history for smooth branching.
-#### 1. Prerequisites: Prepare local Supabase environment
+## 1. Prerequisites: Prepare local Supabase environment
- If you do not have a local environment set up already, follow the Supabase local development getting started guide:
- [Running Supabase Locally](/docs/guides/local-development/cli/getting-started).
@@ -21,7 +21,7 @@ supabase start
---
-#### 2. Pull remote schema into migrations
+## 2. Pull remote schema into migrations
With your project linked, run:
@@ -33,7 +33,7 @@ This command generates a migration file in your `supabase/migrations` folder whi
---
-#### 3. Sync remote migration history
+## 3. Sync remote migration history
Upon running the above command, the Supabase CLI will typically prompt with:
@@ -54,12 +54,12 @@ Run the exact repair command provided, replacing the timestamp as instructed (ex
---
-#### 4. Proceed with Branching
+## 4. Proceed with Branching
Once your migration history is up to date, continue to use branching features as normal. Each new branch will now inherit the correct migrations from your `main` branch.
---
-#### Additional tips
+## Additional tips
If further issues arise (such as schema drift or migration mismatches), review the [troubleshooting documentation](/docs/guides/deployment/branching/troubleshooting#migration-issues), and consider manual repair with [`supabase migration repair`](/docs/reference/cli/supabase-migration-repair).
diff --git a/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx b/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx
index 710d10e270c3f..9f6258ae9d1f8 100644
--- a/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx
+++ b/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx
@@ -15,11 +15,11 @@ As the initial step in debugging not-delivered emails, check your project's [Aut
Once handed over to the email provider, Supabase has no control over email delivery. There can be multiple reasons why emails do not reach the user's inbox.
-#### 1. Issues with the email provider.
+## 1. Issues with the email provider.
The email provider's logs are the next place to look for email delivery issues. There are cases where the email provider blocks the delivery due to past bounced-back emails. Some email providers maintain a suppression list for not sending emails due to several reasons. You can read more about it here - https://sendgrid.com/en-us/blog/what-is-a-suppression-list
-#### 2. Issues with the user's email server
+## 2. Issues with the user's email server
Many email firewalls maintain a denylist of IPs and domain names for security reasons and as spam filters. Sometimes, they block incoming emails from unknown addresses with certain keywords such as password reset, verification link, etc. In these cases, ask the user to check with their email server admin to see if they are blocking your email domain or quarantining the incoming emails. If you use the default provider, the email domain is `supabase.io`. Some information on email firewalls are available here - https://mailchimp.com/help/about-email-firewalls/.
diff --git a/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx b/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx
index 344f13d04ab52..c3dd78fd39e05 100644
--- a/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx
+++ b/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx
@@ -14,15 +14,15 @@ http_status_code = 500
A 500 error in Auth typically indicates an issue with an external dependency, such as your database or SMTP provider, rather than with Auth itself. This guide will help you explore the Auth logs to identify the underlying cause.
-#### Prerequisites
+### Prerequisites
-##### Open the log explorer
+#### Open the log explorer
Ensure you have access to the [Dashboard's Log Explorer](/dashboard/project/_/logs/explorer) and set the time range appropriately:

-##### Improving log readability
+#### Improving log readability
Logs are displayed in a table format, which can be challenging to read. Double-clicking on a row will expand it for easier viewing:
diff --git a/apps/docs/content/troubleshooting/resolving-cannot-execute-update-in-a-read-only-transaction-on-transaction-pooler-connections-ef582c.mdx b/apps/docs/content/troubleshooting/resolving-cannot-execute-update-in-a-read-only-transaction-on-transaction-pooler-connections-ef582c.mdx
index 30c9a12ca6d91..53f7457ce7799 100644
--- a/apps/docs/content/troubleshooting/resolving-cannot-execute-update-in-a-read-only-transaction-on-transaction-pooler-connections-ef582c.mdx
+++ b/apps/docs/content/troubleshooting/resolving-cannot-execute-update-in-a-read-only-transaction-on-transaction-pooler-connections-ef582c.mdx
@@ -4,7 +4,7 @@ topics = [ "database", "supavisor" ]
keywords = []
---
-### Key technical terms
+## Key technical terms
**Transaction Pooling**
A connection management method used by tools like Supavisor or PgBouncer. Instead of giving every client a dedicated, permanent connection to the database, the pooler maintains a small set of "backend connections." It lends one of these connections to a client for the duration of a single transaction, then immediately takes it back to give to another client.
@@ -17,29 +17,29 @@ That backend connection has _state_. Postgres connections carry settings like ti
---
-### Understanding the problem: The "sticky" state
+## Understanding the problem: The "sticky" state
When you encounter the error `cannot execute UPDATE in a read-only transaction` while using a transaction pooler (typically on port 6543), even when you have verified that the connection is made to the primary database and the database itself is not in read-only mode (check with `SHOW default_transaction_read_only;` or `SELECT pg_is_in_recovery();` using a direct connection on port 5432), it signifies that a backend connection has been unintentionally locked into a read-only state.
**Note:** If your database is in read-only mode (for example, due to exceeding disk space limits), you can review this guide: [Database Size and Read-Only Mode](/docs/guides/platform/database-size#read-only-mode). The remainder of this guide addresses a different issue specific to transaction pooling.
-#### The cause: Connection contamination
+### The cause: Connection contamination
In transaction pooling mode, reset behavior is intentionally limited for performance, so session state can persist unless explicitly reset. If a client, script, or automated task changes a session-level setting, that setting "sticks" to the backend connection.
When that backend connection is returned to the pool, the next client to use it inherits that exact state. If a previous script set the connection to "read-only" for safety and failed to reset it, any subsequent application attempt to perform an `UPDATE` or `INSERT` using that same backend will fail.
-#### Why is the error sporadic?
+### Why is the error sporadic?
The error appears intermittent because it only occurs when your application is randomly assigned a "contaminated" backend connection from the pool. Other connections in the same pool may still be in the default read-write state, leading to a confusing mix of successful and failed requests.
---
-### Step-by-step resolution
+## Step-by-step resolution
To resolve this issue, you must identify and remove any commands that modify the session state globally rather than locally.
-#### 1. Audit application and scripts
+### 1. Audit application and scripts
Search your application code, migration scripts, and maintenance tasks for the following session-level commands:
@@ -48,7 +48,7 @@ Search your application code, migration scripts, and maintenance tasks for the f
Even if these commands are used in secondary scripts (like data exports or safety-first maintenance tasks) and not the main application, they can still contaminate the pool used by the main application.
-#### 2. Implement "safe" settings
+### 2. Implement "safe" settings
If you need to execute a read-only transaction for safety, use transaction-level commands that only affect the current transaction and do not persist on the backend connection.
@@ -60,7 +60,7 @@ If you have existing scripts that use session-level settings and cannot be immed
- **Temporary workaround:** Ensure they explicitly reset the state before closing the connection with `SET default_transaction_read_only = off;`
- **Best approach:** Connect directly to port 5432 (bypassing the pooler) for scripts requiring special session states
-#### 3. Connection string verification
+### 3. Connection string verification
Ensure your application is using the intended pooler.
@@ -69,6 +69,6 @@ Ensure your application is using the intended pooler.
---
-### Best practices for transaction pooling
+## Best practices for transaction pooling
- **Use Dedicated Connections for Maintenance:** If a script requires a specific session state (like a long-running read-only export), connect directly to the database (port 5432) rather than using the transaction pooler (port 6543). This prevents maintenance settings from leaking into the application's connection pool.%
diff --git a/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx b/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx
index f6ef566822605..1881d1f9ac5e8 100644
--- a/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx
+++ b/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx
@@ -7,7 +7,7 @@ keywords = [ "hostname", "ip", "ipv4", "ipv6" ]
database_id = "c89147f8-a66a-4c04-a1a7-45442fd7f2ee"
---
-### Finding your database hostname
+## Finding your database hostname
Your database's hostname is crucial for establishing a direct connection. It resolves to the underlying IP address of your database.
@@ -17,7 +17,7 @@ Example Hostname: `db.zcjtzmeifsoteyjytnbc.supabase.co`

-### Managing your IP address
+## Managing your IP address
To determine your current IP address, you can use an [IP address lookup](https://whatismyipaddress.com/hostname-ip) website or the terminal command:
@@ -26,14 +26,14 @@ To determine your current IP address, you can use an [IP address lookup](https:/
Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df`
-### IPv6 address
+## IPv6 address
Upon project creation, a static IPv6 address is assigned. However, it's essential to understand that this IPv6 address can change due to specific actions:
- When a project is paused or resumed.
- During database version upgrades.
-### IPv4 address
+## IPv4 address
Opting for the static [IPv4 add-on](/docs/guides/platform/ipv4-address) provides a more stable connection address. The IPv4 address remains constant unless:
diff --git a/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx b/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx
index e2cbb853fde23..fe7a641b2cb0e 100644
--- a/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx
+++ b/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx
@@ -7,7 +7,7 @@ keywords = [ "rls", "sql", "policy" ]
database_id = "5b8a5115-f81a-4a1f-bcc3-fdc2a2672e80"
---
-### Basic summary
+## Basic summary
Row-Level Security (RLS) Policy: A `WHERE` or `CHECK` condition applied automatically to database queries
diff --git a/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx b/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx
index ba1a957e5184f..bb9ab7e9cbd1e 100644
--- a/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx
+++ b/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx
@@ -9,7 +9,7 @@ database_id = "46bab0a2-9780-4e4f-99f5-fcc9fa51c496"
We want to clarify and provide reassurance on this topic.
-### Security overview:
+## Security overview:
Enabling anonymous sign-ins on your project does not reduce its security. Here's why:
@@ -17,7 +17,7 @@ Enabling anonymous sign-ins on your project does not reduce its security. Here's
- Security Policies: All role-based security policies (RLS) applicable to regular users also apply to anonymous users.
- Identity Verification Measures: Even though anonymous users do not initially provide an email or phone number, the security of your project remains robust. But to prevent misuse, we recommend implementing additional security measure such as [CAPTCHA](/docs/guides/auth/auth-captcha): to ensure that interactions are genuinely human.
-### Practical use cases:
+## Practical use cases:
- Demo Mode: You can enable users to try out your product in a demo mode without full account creation.
- Feature Restrictions: You can limit certain actions (like posting public content) to users who sign up with more identifiable information (e.g., Google or Apple sign-ins), while still allowing anonymous users to explore your app.
diff --git a/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx b/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx
index 35751edb9fc51..f7e2ba5f59372 100644
--- a/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx
+++ b/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx
@@ -14,11 +14,11 @@ The internet uses a system called the Internet Protocol (IP) to route communicat
- **IPv4**: Introduced in 1980, it's the original version.
- **IPv6**: Launched in 1999, it offers a much larger address space and is the preferred future-proof option.
-#### Supabase and IPv6
+### Supabase and IPv6
All Supabase databases provide a direct connection string that maps to an IPv6 address.
-#### Working with IPv6 incompatible hosts
+### Working with IPv6 incompatible hosts
Here are your options if your server platform doesn't support IPv6:
@@ -28,7 +28,7 @@ Here are your options if your server platform doesn't support IPv6:
> Note: the IPv4 Add-On costs an hour, which equates to ~ if left on for a full month (~720 hours)
-#### Checking IPv6 support
+### Checking IPv6 support
The majority of services are IPv6 compatible. However, there are a few prominent ones that only accept IPv4 connections:
@@ -45,7 +45,7 @@ curl -6 https://ifconfig.co/ip
If the command returns an IPv6 address, the network is IPv6 compatible.
-#### Finding your database's IP address
+### Finding your database's IP address
To determine your current IP address, you can use an IP address [lookup website](https://whatismyipaddress.com/hostname-ip) or the terminal command:
@@ -57,7 +57,7 @@ This command queries the domain name servers to find the IP address of the given
Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df`
-#### Identifying your connections
+### Identifying your connections
The pooler and direct connection strings can be found on the dashboard by clicking [Connect](/dashboard/project/_?showConnect=true).
@@ -68,14 +68,14 @@ The pooler and direct connection strings can be found on the dashboard by clicki
postgresql://postgres:[YOUR-PASSWORD]@db.ajrbwkcuthywfihaarmflo.supabase.co:5432/postgres
```
-##### Supavisor in transaction mode (port 6543)
+#### Supavisor in transaction mode (port 6543)
```sh
# Example transaction string
postgresql://postgres.ajrbwkcuthywddfihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
```
-##### Supavisor in session mode (port 5432)
+#### Supavisor in session mode (port 5432)
```sh
# Example session string
diff --git a/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx b/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx
index de577915a8da5..3c636edfa58ee 100644
--- a/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx
+++ b/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx
@@ -50,7 +50,7 @@ Optimizing:
5. [Remove bloat](/docs/reference/cli/supabase-inspect-db): Bloat can fragment data across pages, causing redundant data to be pulled from disk.
6. Table refactoring: Split tables to isolate columns that are less frequently accessed, so they are not redundantly pulled into memory while accessing hotter data
-### Other useful Supabase Grafana guides:
+## Other useful Supabase Grafana guides:
- [Connections](https://github.com/orgs/supabase/discussions/27141)
- [Disk](https://github.com/orgs/supabase/discussions/27003)
diff --git a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx
index 1b1ed37893bf5..96218a1a82e05 100644
--- a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx
+++ b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx
@@ -7,7 +7,7 @@ keywords = [ "pooler", "connections", "supavisor", "database" ]
database_id = "f252ba39-e540-462b-868f-42eb31a5d40c"
---
-### What problems do poolers solve?
+## What problems do poolers solve?
Postgres stands out from other databases by opting to create a new process, not a new thread, for each direct connection. While this design choice brings [numerous benefits](https://www.postgresql.org/message-id/1098894087.31930.62.camel@localhost.localdomain), it introduces a startup penalty for new connections. Moreover, connections are more memory-intensive and can strain Postgres's internal schedulers, limiting the sustainable number that can be formed. Resultingly, developers must be mindful of how they allocate the resource.
@@ -15,11 +15,11 @@ When a client (backend server) connects to Postgres, the connection is stateful
Postgres's shortcomings are particularly evident when handling transient servers, like edge functions. They not only hoard connections for brief queries but also aggressively open and close connections, straining the database.
-### How do poolers solve the problem?
+## How do poolers solve the problem?
A pooler is ultimately a load balancer for database connections. It maintains several hot connections that it triages to clients. This reduces the startup cost of creating a new process on Postgres. The pooler can also more efficiently manage a database's finite connections by only allowing clients to access them when they need to execute a query (A.K.A. transaction mode).
-### Are poolers necessary?
+## Are poolers necessary?
All database connection libraries, such as Prisma, SQLAlchemy, and Postgres.js have built-in poolers. These are known as application-side poolers and they are fundamental for sustainable connection management. Most libraries have default pool sizes that may need to be changed for specific workloads. As an example, most edge/serverless functions are called to service a single user's request. They usually require significantly fewer connections (often 1 is optimal) than a dedicated application server.
@@ -31,7 +31,7 @@ When connecting to your application from serverless/edge functions, horizontally
They sit between the database and your client servers. They are solely optimized for sustaining high numbers of client connections and queuing and triaging queries to the database. Although they add network complexity, they are necessary when managing auto-scaling servers that can hypothetically form an infinite amount of connections.
-### Where are the connection strings
+## Where are the connection strings
Supabase provides 3 database connection strings that can be used simultaneously if necessary. You can find them on the dashboard by clicking [Connect](/dashboard/project/_?showConnect=true).
@@ -41,7 +41,7 @@ Supabase provides 3 database connection strings that can be used simultaneously
src="https://github.com/supabase/supabase/assets/91111415/1d653203-84d9-406a-a7c9-1f7d097f5a29"
/>
-#### Direct connections:
+### Direct connections:
> "Note uses an IPv6 address by default. [Check here to see if your network is IPv6 compatible](https://github.com/orgs/supabase/discussions/27034)"
@@ -50,21 +50,21 @@ Supabase provides 3 database connection strings that can be used simultaneously
postgresql://postgres:[YOUR-PASSWORD]@db.ajrbwkcuthywfihaarmflo.supabase.co:5432/postgres
```
-#### Supavisor in transaction mode (port 6543)
+### Supavisor in transaction mode (port 6543)
```sh
# Example transaction string
postgresql://postgres.ajrbwkcuthywddfihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
```
-#### Supavisor in session mode (port 5432)
+### Supavisor in session mode (port 5432)
```sh
# Example session string
postgresql://postgres.ajrbwkcuthywfddihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres
```
-### Supavisor: Transaction mode vs. Session mode?
+## Supavisor: Transaction mode vs. Session mode?
When a client forms a direct connection with Postgres, it usually makes a few queries but may not use the connection the entire time. In transaction mode, a client is allowed to make a single query before being sent back to the figurative "waiting room". This prevents greedy or sedentary clients from hoarding connections. In most cases, this increases query throughput and is optimal.
@@ -74,11 +74,11 @@ This behavior mirrors a direct connection, allowing greedy clients to monopolize
Depending on your application's configurations, having the pooler manage a queue of patient clients is preferable to the alternative of constantly polling the database to check for an available connection. Session mode can queue clients for up to a minute. If this isn't particularly relevant to your application design, then the primary benefit is that it is [IPv4 compatible](https://github.com/orgs/supabase/discussions/27034). Also, unlike transaction mode, it supports prepared statements.
-### What happens when a client library, such as Prisma, connects through Supavisor?
+## What happens when a client library, such as Prisma, connects through Supavisor?
When clients connect to either Postgres or Supavisor, they do so with the Postgres Wire Protocol. Because of this, clients treat connections with pooler as if they were directly connected to Postgres. The pooler then smoothly acts as a messenger between the database and the client.
-### What are "client connections"?
+## What are "client connections"?
In summary, they have nothing to do with front-end clients. They are the amount of backend-server connections that can connect to a serverside pooler.
Imagine a chess tournament with 60 boards. Each board represents a connection in a database. When a player sits down at a board, it's like a client connecting to the database. They can take their time with their moves or sit there, not making any.
@@ -87,11 +87,11 @@ But when the tournament fills up and all the boards are taken, new players are t
Now, imagine the tournament organizers decide to expand the venue to house 200 people without adding more tables. Even when all boards are occupied, players don't have to leave. They can wait in the wings, and the moment a board opens up, someone from the waiting area can take their place. Likewise, if someone ends their game, but wants to play again, they can go back to the waiting area. The waiting area represents the "Max Client Connections". Ultimately, the additional capacity provided by the pooler ensures fewer people are turned away from the "tournament".
-### In the context of Supavisor, what does "pool size" mean?
+## In the context of Supavisor, what does "pool size" mean?
"Pool size" refers to the maximum number of direct connections the pooler can maintain per unique user, database, and mode combination. You can adjust it in the [project connect page](/dashboard/project/_/database/settings) to strike a balance between efficient resource utilization and accommodating peak traffic.
-### What is the "user+db+mode" combination?
+## What is the "user+db+mode" combination?
Postgres is not a database. It is a Relational Database Management System (RDMS). Within it, you can spawn Postgres databases. In Supabase, it is a common pattern to use the default database called `postgres`, but you could create more:
@@ -117,7 +117,7 @@ postgres://[USER].shfmmplnqscentnakbkl:[password]@aws-0-ca-central-1.pooler.supa
When a distinct combination connects to your database, a new direct connection pool will be created. That means if you have two combinations connecting and the "Pool Size" is set to 120, each combination will have permission to form 120 connections. This can become problematic if collectively they exhaust all available direct connections.
-### **Does Supavisor immediately establish the max pool size?**
+## **Does Supavisor immediately establish the max pool size?**
No, it doesn't. See why:
@@ -127,7 +127,7 @@ No, it doesn't. See why:
- In transaction mode, once a direct connection is established, the pooler will keep it available for reuse for another client. However, if the connection remains unused for 5 minutes, the pooler will close it to free up database resources. In session mode, the connection will be immediately closed.
- If a client is connected to the pooler, then at least 1 hot connection will be sustained, even if no client needs it.
-### **How to change pool size**
+## **How to change pool size**
In the [Dashboard's Database Settings](/dashboard/project/_/database/settings), you can configure Supavisor's "Pool Size":
@@ -139,13 +139,13 @@ In the [Dashboard's Database Settings](/dashboard/project/_/database/settings),
You can also change the pool size for PostgREST's (DB API) internal pooler at the bottom of the [application settings](/dashboard/project/_/settings/api).
-### Do all services on Supabase use Supavisor?
+## Do all services on Supabase use Supavisor?
Supabase Storage uses Supavisor internally. The other servers that communicate with Postgres (PostgREST, Realtime, and Auth) all rely on internal application poolers.
Supavisor is primarily intended for users who do not want to rely on the Supabase Client libraries and instead prefer to work with external ORMs, such as Prisma, Drizzle, and Psycopg.
-### **Whether to change Supavisor's pool size?**
+## **Whether to change Supavisor's pool size?**
In an ideal scenario, Postgres would support an unlimited number of direct connections, but there's a limit to how many it can handle. If Supavisor uses most of the available connections, you risk depriving other servers, such as Auth, from accessing your database. Still, as much as possible, you want to give your pooler freedom to grow its pool as needed to service demand.
@@ -153,7 +153,7 @@ It's important to note that the Storage server also uses Supavisor as a unique "
As a rule of thumb, if you're using the DB REST API or multiple app-based "user+db+mode" combinations, try to keep the pooler's usage under 40% of available connections. Otherwise, you can cautiously increase usage to around 80%. These percentages are flexible and depend on your application's usage and setup. Monitor connection usage to determine the optimal allocation without depriving other servers of necessary connections.
-### **How to monitor connections**
+## **How to monitor connections**
> EDIT: a more in-depth [troubleshooting guide](https://github.com/orgs/supabase/discussions/27141) for connection monitoring was published
@@ -161,7 +161,7 @@ Connection usage can be monitored with a Supabase Grafana Dashboard. It provides
You can check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local deployments or free cloud deployments on [Fly.io](http://fly.io/). Refer to Supabase [documentation](/docs/guides/monitoring-and-debugging/metrics) to learn more about the metrics endpoint.
-### **Can Supavisor really support a million connections?**
+## **Can Supavisor really support a million connections?**
It depends.
diff --git a/apps/docs/content/troubleshooting/tracking-postgres-role-activity-to-specific-dashboard-users-8d3715.mdx b/apps/docs/content/troubleshooting/tracking-postgres-role-activity-to-specific-dashboard-users-8d3715.mdx
index f679efeeaab0d..9a5618bfeb89b 100644
--- a/apps/docs/content/troubleshooting/tracking-postgres-role-activity-to-specific-dashboard-users-8d3715.mdx
+++ b/apps/docs/content/troubleshooting/tracking-postgres-role-activity-to-specific-dashboard-users-8d3715.mdx
@@ -6,7 +6,7 @@ keywords = []
When team members run SQL queries from the Dashboard SQL Editor, and if that query is logged in the Postgres Logs, it's not immediately clear who executed which query. This guide shows you how to track queries back to the specific team member who ran them.
-### **Understanding Dashboard query execution**
+## **Understanding Dashboard query execution**
First, it helps to understand how Dashboard queries are executed. When someone runs a query from the SQL editor, it's routed through the `postgres` role at the database level. The Supabase Dashboard automatically appends metadata comments to queries, specifically `-- user: [UUID]`, `-- source: dashboard`, and `-- date`.
@@ -14,7 +14,7 @@ By default, that role has `log_statement` set to `ddl`, which means Postgres log
So, if someone truncates a table, and you're relying on the default logging, you won't see it.
-### **Enabling data modification logging**
+## **Enabling data modification logging**
To make those operations visible, you can increase the logging level for the `postgres` role:
@@ -42,7 +42,7 @@ Notice that the log includes:
That UUID corresponds to the team member who logged in via the Supabase Dashboard and executed queries in the [SQL Editor](/dashboard/project/_/sql/new). But at this point, it's only an ID - not yet a name or email.
-### **Mapping UUIDs to team members**
+## **Mapping UUIDs to team members**
To map the UUID, you'll need to query the Management API. The process looks like this:
@@ -72,7 +72,7 @@ The response will include entries like:
Now you can directly match `user_id` values from the Postgres logs to the corresponding team members.
-### **Querying logs for specific operations**
+## **Querying logs for specific operations**
Navigate to the [Logs Explorer](/dashboard/project/_/logs/explorer) and query `postgres_logs`. Here's an example query that searches for data-modifying operations and maps user IDs to team members:
@@ -118,11 +118,11 @@ This query:
You can further refine your search by filtering for specific commands like `TRUNCATE` or `DELETE` where `parsed.user_name = 'postgres'`.
-### **Tracking external tools**
+## **Tracking external tools**
For external tools like n8n or other applications connecting to your database, you can identify the source of database changes by appending `?application_name=example_app_name` to your connection string. This ensures the source is clearly identified in the logs, making it easier to distinguish between Dashboard operations and external tool operations.
-### **Additional logging levels**
+## **Additional logging levels**
Postgres supports these `log_statement` values:
diff --git a/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx b/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx
index 3ee20895f7f29..0f945b1ce1cd1 100644
--- a/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx
+++ b/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx
@@ -9,7 +9,7 @@ database_id = "c6b6ae3c-1b5b-4ba8-a40f-5f2ca1f007e2"
For a detailed, step-by-step guide on restoring your database from the Supabase platform to a [self-hosted Supabase](/docs/guides/self-hosting) instance, see [Restore a Platform Project to Self-Hosted](/docs/guides/self-hosting/restore-from-platform).
-### Quick reference
+## Quick reference
Back up your cloud database:
diff --git a/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx b/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx
index 97cbe55b88e50..f10c66b88b13a 100644
--- a/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx
+++ b/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx
@@ -10,15 +10,15 @@ database_id = "0ce5b1e4-bd0a-439e-9fb1-8e27bad2ef10"
sdk = [ "explain" ]
---
-### Introduction
+## Introduction
This guide is designed to help you understand how to use the Postgres [EXPLAIN and EXPLAIN ANALYZE](https://www.postgresql.org/docs/current/sql-explain.html) commands to optimize and debug SQL queries. Understanding the output of these commands can help you improve the performance of your applications by optimizing database interactions.
-### What is explain?
+## What is explain?
The Postgres EXPLAIN command shows the execution plan of a SQL query. This plan describes how the Postgres database will execute the query, including how tables will be scanned—by using sequential scans, index scans, etc.—and how rows will be joined.
-### How to use explain in Supabase
+## How to use explain in Supabase
**Using EXPLAIN through the SQL Editor**
@@ -39,7 +39,7 @@ const { data, error } = await supabase
.explain({analyze:true,verbose:true})
```
-### Detailed breakdown of explain output components
+## Detailed breakdown of explain output components
### 1. Plan type
@@ -74,7 +74,7 @@ const { data, error } = await supabase

-### Detailed components in explain analyze
+## Detailed components in explain analyze
When running EXPLAIN ANALYZE, additional information is provided, including:
@@ -99,7 +99,7 @@ Planning Time: 0.135 ms
- **Planning Time: 0.135 ms:** Planning Time refers to the amount of time the Postgres query planner takes to analyze the query and create an execution plan. This time is measured in milliseconds.
-### You might be asking yourself now, why there is two different sets of metrics : `(cost=0.42..2.64 rows=1 width=164) (actual time=0.020..0.021 rows=1 loops=1)`?
+## You might be asking yourself now, why there is two different sets of metrics : `(cost=0.42..2.64 rows=1 width=164) (actual time=0.020..0.021 rows=1 loops=1)`?
To answer you, one is for the estimated cost and performance, and another for the actual performance of the query as explained above.
@@ -109,7 +109,7 @@ To answer you, one is for the estimated cost and performance, and another for th
**Identifying Bottlenecks**: If the actual time is significantly higher than expected, or if loops are more frequent than anticipated, these could be indicators of performance bottlenecks in the query.
-### How to read a complex explain output
+## How to read a complex explain output
First, you have to understand that a Postgres execution plan is a tree structure consisting of several nodes. The top node (the Aggregate above) is at the top, and lower nodes are indented and start with an arrow (->). Nodes with the same indentation are on the same level (for example, the two relations combined with a join).
@@ -133,7 +133,7 @@ Postgres executes a plan top down, that is, it starts with producing the first r
On top of that, you have to multiply the cost and the time with the number of “loops” to get the total time spent in a node.
-### Common nodes in Postgres explain output
+## Common nodes in Postgres explain output
| Node Type | Description |
| --------------------- | ------------------------------------------------------------------------------ |
@@ -155,7 +155,7 @@ On top of that, you have to multiply the cost and the time with the number of
| **Foreign Scan** | Fetches data from foreign data sources outside the local database. |
| **Function Scan** | Retrieves results from a set-returning function. |
-### What to focus on in explain analyze output
+## What to focus on in explain analyze output
- Find the nodes where most of the execution time was spent.
`Hash Join (cost=100.00..200.00 rows=1000 width=50) (actual time=50.012..150.023 rows=1000 loops=1)`
@@ -176,7 +176,7 @@ Seq Scan on products (cost=0.00..100.00 rows=300 width=50) (actual time=50.000.
Explanation:
This sequential scan took 50 to 100 milliseconds and filtered out 2997 of 3000 rows, indicating that only a few rows met the condition. This scenario is ideal for an index on the price column to optimize the performance by reducing the need for a full table scan.
-### Understanding the significance of milliseconds in Query Performance
+## Understanding the significance of milliseconds in Query Performance
Determining whether 100 milliseconds for e.g is noteworthy in the context of identifying performance bottlenecks depends on various factors:
@@ -190,12 +190,12 @@ For complex queries that involve multiple joins, subqueries, or aggregation func
So, the acceptable performance threshold can vary by application. For real-time systems or high-frequency trading platforms, even a few milliseconds can be critical, whereas for batch processing or data warehousing, longer execution times might be acceptable.
-### Tools to interpret explain analyze output
+## Tools to interpret explain analyze output
Since reading a longer execution plan is quite cumbersome, you can use the website https://explain.depesz.com/ to better visualize the query. If you paste the execution plan in the text area and hit “Submit”, you will get output like this:

-### Tips for optimizing queries
+## Tips for optimizing queries
- **Add Indexes:** Improve performance by adding indexes on columns that are frequently used in WHERE clauses or JOIN conditions.
@@ -203,7 +203,7 @@ Since reading a longer execution plan is quite cumbersome, you can use the websi
- **Update Statistics:** Ensure that statistics are up to date to help the optimizer make better choices.
-### Conclusion
+## Conclusion
Understanding EXPLAIN and EXPLAIN ANALYZE output can significantly enhance your ability to write efficient SQL queries. Regularly analyze query performance and make adjustments as your dataset grows and changes.
diff --git a/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx b/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx
index a88e2e726b27e..8315967977464 100644
--- a/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx
+++ b/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx
@@ -9,7 +9,7 @@ database_id = "10186830-8cce-4f10-8cb9-7cbf39310763"
Since each Supabase project uses Postgres as its underlying database engine, it’s common to adjust logging settings for various reasons—whether for debugging issues, monitoring database performance, or auditing actions. However, modifying logging levels improperly can lead to an excessive amount of log data being generated, which can fill up your disk space and cause significant performance degradation or even system failure.
-### 1. Overview of Postgres logging levels
+## 1. Overview of Postgres logging levels
Postgres provides multiple logging levels that allow you to control how much information gets logged. These include:
@@ -39,7 +39,7 @@ PANIC: database system shutdown requested
The default log level is set to **WARNING** through the log_min_messages setting, and we recommend keeping it that way.
-### 2. How high log levels can affect your database
+## 2. How high log levels can affect your database
When users alter a high level of log settings, the database can start generating an overwhelming number of log entries. This can escalate to issues such as:
@@ -49,7 +49,7 @@ When users alter a high level of log settings, the database can start generating
- Database Lockups: In extreme cases, if the disk is filled to capacity with logs, your database could lock up, leading to downtime or severe performance degradation.
-### 3. Common scenarios that cause log overload
+## 3. Common scenarios that cause log overload
Here are a few common scenarios where excessive logging can become a problem:
@@ -59,7 +59,7 @@ Here are a few common scenarios where excessive logging can become a problem:
- Frequent Write Operations: If your database processes a lot of write operations (such as inserts, updates, or deletes), even low-level logs (like NOTICE or INFO) can lead to significant log accumulation.
-### 4. How to manage Postgres log levels effectively
+## 4. How to manage Postgres log levels effectively
**a. Choose the Right Log Level**
For most users, setting Postgres logs to WARNING or ERROR is sufficient for regular operations. Here’s a general guideline:
@@ -97,11 +97,11 @@ ALTER ROLE postgres SET log_min_messages TO 'ERROR';
ALTER ROLE postgres RESET log_min_messages;
```
-### 5. Conclusion
+## 5. Conclusion
Postgres logs provide a powerful way to gain valuable insights into your database activity and performance when properly configured, but the key lies in finding the right balance. When set up properly, they can be incredibly useful.
-### 6. Other resources
+## 6. Other resources
**a. What Events Are Logged in Postgres**
For a detailed explanation of the types of events logged in your database (such as connection events, checkpoint events, long-running queries, cron jobs, and severity-based logging), you can refer to the official documentation here:
diff --git a/apps/docs/content/troubleshooting/vercel-integration-environment-variables-not-syncing-for-persistent-git-branches-b9191e.mdx b/apps/docs/content/troubleshooting/vercel-integration-environment-variables-not-syncing-for-persistent-git-branches-b9191e.mdx
index 59b4aa67c1501..8d7a48c63c59b 100644
--- a/apps/docs/content/troubleshooting/vercel-integration-environment-variables-not-syncing-for-persistent-git-branches-b9191e.mdx
+++ b/apps/docs/content/troubleshooting/vercel-integration-environment-variables-not-syncing-for-persistent-git-branches-b9191e.mdx
@@ -10,7 +10,7 @@ Vercel has three environments, which map to different stages of the deployment l
- **Preview** is used for all other Git branches, including pull requests, feature branches, and persistent branches like staging. If you deploy a staging branch, it still runs under the Preview environment unless you explicitly create a separate environment in Vercel and map that branch to it.
- **Development** is only used for local development via the Vercel CLI (`vercel dev`). It allows your local environment to pull env vars from Vercel, but it does not apply to Git branches or deployments on the platform.
-### Creating a staging environment
+## Creating a staging environment
On the Hobby plan, staging is implemented by scoping Preview environment variables to a branch. On the Pro plan, staging can be configured as a dedicated environment with its own settings.
diff --git a/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx b/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx
index 8503bf7943a65..0023513f7cc28 100644
--- a/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx
+++ b/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx
@@ -16,11 +16,11 @@ You might be surprised to know that gaps in sequence IDs are a normal aspect of
It's also important to understand the distinction that sequences guarantee **uniqueness**, but not **consecutiveness**, and this should not imply any issues relating to data integrity with your database.
-### How to check the name of your sequence
+## How to check the name of your sequence
If you don't know the name of your sequence, it's often formed based on a standard naming convention: table_name_id_seq, where table_name is the name of your table and id is the name of your serial column.
-### Common reasons for gaps in sequences
+## Common reasons for gaps in sequences
1. Rollbacks
One of the most common reasons for a gap is the rollback of a transaction. If you initiate a transaction that includes an insert operation, the sequence responsible for generating the ID for the new row increments. If, for any reason, the transaction doesn't complete successfully—perhaps due to a constraint violation or a deliberate decision to rollback—the insert operation is undone, but the sequence value used is not returned or reused. The documentation explains that as well:
@@ -36,7 +36,7 @@ If you don't know the name of your sequence, it's often formed based on a standa
4. Upserts
When an upsert is executed, it can still increase the sequence even if it is set to do nothing on conflicts
-### Checking for gaps
+## Checking for gaps
To check for gaps in the sequence of IDs in a Postgres table, you can use a SQL query that compares the sequence of IDs to a generated series of numbers that spans the same range:
@@ -52,7 +52,7 @@ WHERE
This query should help you pinpoint where gaps exist.
-### Encountering errors and adjusting sequences
+## Encountering errors and adjusting sequences
In operations involving sequences, you might encounter an error message such as:
@@ -74,6 +74,6 @@ And reset the sequence value to match the highest ID plus one:
Or, alternatively, adjust the sequence to a specific new value:
`ALTER SEQUENCE '{table}_{column}_seq' RESTART WITH new_value;`
-### Implementing a gapless ID sequence
+## Implementing a gapless ID sequence
If your application requires contiguous IDs and you decide to build a gapless sequence. Think twice about this, as it will serialize all transactions that use that “sequence” which will then deteriorate your data modification performance considerably. However, if your application truly requires gapless IDs, then you can use a Trigger: After a successful insert, use a database trigger to assign an ID based on a custom logic that finds the next available gapless ID.
diff --git a/apps/docs/features/directives/CodeTabs.components.tsx b/apps/docs/features/directives/CodeTabs.components.tsx
index c18c76aa31b5d..126974e17347b 100644
--- a/apps/docs/features/directives/CodeTabs.components.tsx
+++ b/apps/docs/features/directives/CodeTabs.components.tsx
@@ -1,20 +1,19 @@
import { type PropsWithChildren } from 'react'
-
import { cn } from 'ui'
export function NamedCodeBlock({ name, children }: PropsWithChildren<{ name: string }>) {
return (
-
{name}
-
+
{children}
)
diff --git a/apps/studio/components/interfaces/Database/Privileges/Privileges.utils.ts b/apps/studio/components/interfaces/Database/Privileges/Privileges.utils.ts
index 7bb3f0fe04a76..0b98f97fbd241 100644
--- a/apps/studio/components/interfaces/Database/Privileges/Privileges.utils.ts
+++ b/apps/studio/components/interfaces/Database/Privileges/Privileges.utils.ts
@@ -265,7 +265,15 @@ export function usePrivilegesState({
}
}
-export function useApplyPrivilegeOperations(callback?: () => void) {
+export function useApplyPrivilegeOperations({
+ schema,
+ table,
+ onSuccess,
+}: {
+ schema: string
+ table?: string
+ onSuccess?: () => void
+}) {
const { data: project } = useSelectedProjectQuery()
const queryClient = useQueryClient()
@@ -343,17 +351,19 @@ export function useApplyPrivilegeOperations(callback?: () => void) {
}
await Promise.all([
- queryClient.invalidateQueries({ queryKey: privilegeKeys.tablePrivilegesList(project.ref) }),
queryClient.invalidateQueries({
- queryKey: privilegeKeys.columnPrivilegesList(project.ref),
+ queryKey: privilegeKeys.tablePrivilegesList(project.ref, [schema]),
+ }),
+ queryClient.invalidateQueries({
+ queryKey: privilegeKeys.columnPrivilegesList(project.ref, schema, table),
}),
])
setIsLoading(false)
- callback?.()
+ onSuccess?.()
},
- [callback, project, queryClient]
+ [onSuccess, project, queryClient, schema, table]
)
return { apply, isLoading }
diff --git a/apps/studio/data/privileges/column-privileges-query.ts b/apps/studio/data/privileges/column-privileges-query.ts
index c5ef827d74422..716cf8a0ad89a 100644
--- a/apps/studio/data/privileges/column-privileges-query.ts
+++ b/apps/studio/data/privileges/column-privileges-query.ts
@@ -5,19 +5,24 @@ import { z } from 'zod'
import { executeSql } from '../sql/execute-sql-mutation'
import { privilegeKeys } from './keys'
import type { components } from '@/data/api'
+import { isScopedIntrospection, scopedIntrospectionReady } from '@/data/scoped-introspection'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type ColumnPrivilegesVariables = {
projectRef?: string
connectionString?: string | null
/**
- * [Joshen] Specifically requiring schema to prevent a heavy query on the DB
- * The only UI using this is the column privileges UI atm, so opting to be strict here
- * Ideally we'd be able to also filter based on table to be even more prudent, but
- * leaving out for now as it needs update to pg-meta + might not be worth the over-optimization
- * given this UI isn't a primary tool
+ * Required to keep this off the whole-catalog path: without it the query
+ * aclexplodes every relation in the database across every one of its columns.
*/
schema: string
+ /**
+ * Restricts to a single relation. The UI only ever renders one table at a
+ * time, so this keeps the query scoped to what's on screen -- without it the
+ * whole schema's columns are fetched and all but one table discarded. The
+ * query stays disabled until a table is selected.
+ */
+ table?: string
}
export type ColumnPrivilege = components['schemas']['PostgresColumnPrivileges']
@@ -26,13 +31,19 @@ const pgMetaColumnPrivilegesList = pgMeta.columnPrivileges.list()
type ColumnPrivilegesData = z.infer
export async function getColumnPrivileges(
- { projectRef, connectionString, schema }: ColumnPrivilegesVariables,
+ { projectRef, connectionString, schema, table }: ColumnPrivilegesVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
- const sql = pgMeta.columnPrivileges.list({ includedSchemas: [schema] }).sql
- const queryKey = ['column-privileges', schema]
+ // Cold-load race guard -- see the module comment on scoped-introspection.ts.
+ await scopedIntrospectionReady()
+ const sql = pgMeta.columnPrivileges.list({
+ includedSchemas: [schema],
+ relationName: table,
+ scoped: isScopedIntrospection(),
+ }).sql
+ const queryKey = ['column-privileges', schema, table]
const { result } = await executeSql({ projectRef, connectionString, sql, queryKey }, signal)
return result as ColumnPrivilegesData
}
@@ -46,11 +57,15 @@ export const useColumnPrivilegesQuery = (
...options
}: UseCustomQueryOptions = {}
) => {
- const { projectRef, schema } = vars
+ const { projectRef, schema, table } = vars
return useQuery({
- queryKey: privilegeKeys.columnPrivilegesList(projectRef, schema),
+ queryKey: privilegeKeys.columnPrivilegesList(projectRef, schema, table),
queryFn: ({ signal }) => getColumnPrivileges(vars, signal),
- enabled: enabled && typeof projectRef !== 'undefined' && typeof schema !== 'undefined',
+ enabled:
+ enabled &&
+ typeof projectRef !== 'undefined' &&
+ typeof schema !== 'undefined' &&
+ typeof table !== 'undefined',
...options,
})
}
diff --git a/apps/studio/data/privileges/keys.ts b/apps/studio/data/privileges/keys.ts
index 006f7a3e7a410..6e8bf5776c616 100644
--- a/apps/studio/data/privileges/keys.ts
+++ b/apps/studio/data/privileges/keys.ts
@@ -1,8 +1,15 @@
export const privilegeKeys = {
tablePrivilegesList: (projectRef: string | undefined, includedSchemas?: string[]) =>
['projects', projectRef, 'database', 'table-privileges', includedSchemas].filter(Boolean),
- columnPrivilegesList: (projectRef: string | undefined, schema?: string | undefined) =>
- ['projects', projectRef, 'database', 'column-privileges', schema].filter(Boolean),
+ columnPrivilegesList: (
+ projectRef: string | undefined,
+ schema?: string | undefined,
+ table?: string | undefined
+ ) => {
+ const base = ['projects', projectRef, 'database', 'column-privileges'].filter(Boolean)
+ if (table === undefined) return schema === undefined ? base : [...base, schema]
+ return [...base, schema ?? null, table]
+ },
exposedTablesInfinite: (projectRef: string | undefined, search?: string) =>
[
'projects',
diff --git a/apps/studio/pages/project/[ref]/database/column-privileges.tsx b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
index 841c79d275aeb..52c19b1c47551 100644
--- a/apps/studio/pages/project/[ref]/database/column-privileges.tsx
+++ b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
@@ -55,26 +55,26 @@ const PrivilegesPage: NextPageWithLayout = () => {
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
+ schema: selectedSchema,
})
+ // Default to (or fall back to) the first table of the selected schema. The
+ // fallback matters because the schema can also change via the URL, which
+ // leaves `selectedTable` pointing at a table from the previous schema.
useEffect(() => {
if (!isSuccessTables) return
- const tables = tableList
- .filter((table) => table.schema === selectedSchema)
- .map((table) => table.name)
- if (tables[0] && selectedTable === undefined) {
- setSelectedTable(tables[0])
+ const tableNames = tableList.map((table) => table.name)
+ if (selectedTable === undefined || !tableNames.includes(selectedTable)) {
+ setSelectedTable(tableNames[0])
}
- }, [isSuccessTables, tableList, selectedSchema, selectedTable])
+ }, [isSuccessTables, tableList, selectedTable])
const { data: allRoles, isPending: isLoadingRoles } = useDatabaseRolesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
- const tables = tableList
- ?.filter((table) => table.schema === selectedSchema)
- .map((table) => table.name)
+ const tables = tableList?.map((table) => table.name)
const {
data: allTablePrivileges,
@@ -104,38 +104,34 @@ const PrivilegesPage: NextPageWithLayout = () => {
const {
data: allColumnPrivileges,
- isPending: isLoadingColumnPrivileges,
+ isPending: isPendingColumnPrivileges,
isError: isErrorColumnPrivileges,
error: errorColumnPrivileges,
} = useColumnPrivilegesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
+ table: selectedTable,
})
+ // The query is disabled until a table is selected, and a disabled query stays
+ // `isPending` forever -- so don't report it as loading in that state, or a
+ // schema with no tables never gets past the skeleton.
+ const isLoadingColumnPrivileges = selectedTable !== undefined && isPendingColumnPrivileges
+
const columnPrivileges = useMemo(
() =>
- allColumnPrivileges
- ?.filter(
- (privilege) =>
- privilege.relation_schema === selectedSchema &&
- privilege.relation_name === selectedTable
- )
- .map((privilege) => ({
- ...privilege,
- privileges: privilege.privileges.filter(
- (privilege) => privilege.grantee === selectedRole
- ),
- })) ?? [],
- [allColumnPrivileges, selectedRole, selectedSchema, selectedTable]
+ allColumnPrivileges?.map((privilege) => ({
+ ...privilege,
+ privileges: privilege.privileges.filter((privilege) => privilege.grantee === selectedRole),
+ })) ?? [],
+ [allColumnPrivileges, selectedRole]
)
const rolesList = allRoles?.filter((role: PgRole) => EDITABLE_ROLES.includes(role.name)) ?? []
const roles = rolesList.map((role: PgRole) => role.name)
- const table = tableList?.find(
- (table) => table.schema === selectedSchema && table.name === selectedTable
- )
+ const table = tableList?.find((table) => table.name === selectedTable)
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const {
@@ -170,9 +166,10 @@ const PrivilegesPage: NextPageWithLayout = () => {
}
}
- const newTable = tableList?.find((table) => table.schema === schema)?.name
setSelectedSchema(schema)
- setSelectedTable(newTable)
+ // The table list is scoped to the schema, so the incoming schema's tables
+ // aren't loaded yet -- the effect above picks the first one once they are.
+ setSelectedTable(undefined)
}
const handleChangeTable = (table: string) => {
@@ -192,15 +189,17 @@ const PrivilegesPage: NextPageWithLayout = () => {
}
const { apply: applyColumnPrivileges, isLoading: isApplyingChanges } =
- useApplyPrivilegeOperations(
- useCallback(() => {
+ useApplyPrivilegeOperations({
+ schema: selectedSchema,
+ table: selectedTable,
+ onSuccess: useCallback(() => {
toast.success(
`Successfully updated privileges on ${selectedSchema}.${selectedTable} for ${selectedRole}`,
{ duration: 6000 }
)
resetOperations()
- }, [resetOperations, selectedRole, selectedSchema, selectedTable])
- )
+ }, [resetOperations, selectedRole, selectedSchema, selectedTable]),
+ })
function applyChanges() {
applyColumnPrivileges(operations)
diff --git a/apps/www/_blog/2026-07-31-introducing-supabase-evals.mdx b/apps/www/_blog/2026-07-31-introducing-supabase-evals.mdx
new file mode 100644
index 0000000000000..9c7bb6a5a916a
--- /dev/null
+++ b/apps/www/_blog/2026-07-31-introducing-supabase-evals.mdx
@@ -0,0 +1,79 @@
+---
+title: 'Introducing Supabase Evals'
+description: 'Our open-source benchmark for how well AI coding agents build with Supabase.'
+author: matt_rossman
+date: '2026-07-31'
+categories:
+ - product
+tags:
+ - ai
+ - evals
+ - developer-tools
+imgSocial: 'introducing-supabase-evals/og.png'
+imgThumb: 'introducing-supabase-evals/thumb.png'
+toc_depth: 2
+---
+
+Today we're open sourcing [`supabase/evals`](https://github.com/supabase/evals), our benchmark and framework for testing how well AI agents build using Supabase. It runs coding agents including Claude Code, Codex, and OpenCode against real Supabase tasks, for example, building a schema, debugging a failed Edge Function, or fixing a broken RLS policy, and then scores how well they performed. It powers both our [published benchmark](https://supabase.com/evals) and an internal regression suite we monitor daily. As more people ship Supabase projects through an agent instead of by hand, we wanted a way to measure that experience instead of guessing.
+
+
+
+> The results cited here reflect a snapshot at the time of writing. AI tooling changes rapidly, so check the [live page](https://supabase.com/evals) for current results.
+
+## Why we built this
+
+Agents are becoming a primary way people build with Supabase, interacting through our CLI, MCP server, agent skills, and docs. We needed a way to understand how agents perform across all of these Supabase surfaces, not just one tool in isolation. By tracking where agents stumble, we can make intentional fixes and verify they hold instead of regressing, and test new features confidently before shipping.
+
+## How we approached it
+
+We started by defining the dimensions we wanted to cover, including:
+
+- **Product** areas like Database and Auth
+- **Topics** agents need to handle well across any product, like the SDK and observability
+- **Stages** of a Supabase builder's journey, such as building an app or resolving issues
+
+Then we picked the smallest set of scenarios that touched each dimension at least once, grounded in real problems like support tickets, bug reports, or GitHub issues.
+
+We split scenarios into two suites, benchmark and regression:
+
+**Benchmark scenarios** aim for breadth, acting as a small but diverse set of scenarios that cover the real Supabase user journey. We run these against several harness configurations, including a mixture of more capable and smaller models, and [publish results on our site](https://supabase.com/evals).
+
+**Regression scenarios** aim for depth, covering specific known failure modes that we monitor more frequently without influencing benchmark scores. This gives us a space to triage bug reports or experiment with new features without skewing our published results.
+
+Every scenario runs against a real Supabase environment to measure what an agent would actually do in the wild. We built a framework that spins up both a hosted-like Supabase stack and a local CLI project in containers, so agents invoke our actual MCP server and CLI. We score their behavior with a combination of deterministic checks (whether a user can access certain data, or an Edge Function returns an expected result) and LLM-as-a-judge for anything that needs semantic judgment. To reduce false negatives while keeping runs sustainable, we let agents retry once after a failure before grading them. We run benchmark scenarios when assessing new changes or harnesses, and refresh regression results daily.
+
+Benchmark results are visualized in a web app, so anyone can browse and filter the scores or see details of each run.
+
+## What we've found so far
+
+Below are some areas we saw agents tripping up during benchmark development, and how we're addressing them:
+
+### Key findings
+
+#### Skills vs no-skills isn't as big a delta as we expected, and that's a good sign
+
+Across our benchmark, agents already pass most scenarios with no skill loaded at all. In the Build stage, for example, Opus 5 and Kimi K3 both scored 100% with no skill loaded. Skills closed the gap for the rest: Sonnet 5 went from 78% to 100%, GPT-5.6 Sol from 89% to 100%, and GPT-5.4 mini from 78% to 89%. That means agents are reasonably capable at broad Supabase tasks out of the box. The biggest impact we saw is that agents with our skills loaded checked Supabase docs more consistently and thoroughly, which helped skills earn their keep in the edge cases where models need to unlearn outdated pre-training knowledge. We still recommend using our skill to ensure your agents have the most up-to-date information on Supabase best practices and breaking changes, examples of which follow below.
+
+### Where agents struggle
+
+#### Agents don't reach for declarative schema workflows
+
+[Declarative schemas](https://supabase.com/docs/guides/local-development/declarative-database-schemas) are a developer-friendly way of describing the shape of your database in one place instead of reasoning across several migration files. We expect agents to prefer managing schemas from that single source of truth instead of piecing it together from a chain of migrations. But even in a project that already uses declarative schemas, we noticed agents try to hand-write migrations instead. As a result, we [updated our skill guidance](https://github.com/supabase/agent-skills/pull/120) to make it clearer when to pick each workflow and [verified the behavior correction](https://github.com/supabase/evals/pull/80) with our evals.
+
+#### Our newer libraries aren't sufficiently discoverable
+
+We [recently released](https://supabase.com/blog/introducing-supabase-server) `@supabase/server` to simplify boilerplate of writing secure Edge Functions. When tasked with writing edge functions though, agents still choose to verify auth by hand with `supabase-js`. We've since published a ["Which package to choose"](https://supabase.com/docs/guides/auth/choosing-a-server-package) guide to clarify when to use each package, as a reference for agents.
+
+#### Skill activation is uneven
+
+We track which skills agents loaded during a session compared to what was available. Our main `supabase` skill loads in every scenario where it's available. Our earlier skill, covering [Postgres best practices](https://supabase.com/blog/postgres-best-practices-for-ai-agents), originally loaded in only about one in ten of those same sessions. We rewrote our description of that skill to lead with clearer triggers, increasing activation to 60% across our benchmarks, though we still find OpenAI models more reliable at activating the skill.
+
+#### Docs usage is inconsistent across harnesses
+
+During runs we track when agents read our docs, either via the Supabase MCP `search_docs` tool or their native web tools. Codex-based agents check docs more than Claude Code agents do. The more capable OpenAI model checks docs most consistently, and Codex / GPT-5.6 reads the most pages, around 8 per scenario versus about 2 for Claude Code. Claude Code checks our docs in under 40% of scenarios even with skills loaded. We're working to make sure agents are checking docs in the situations they need to, and that they find all the information they need to make good decisions.
+
+## Check it out
+
+Browse the benchmark results at [supabase.com/evals](https://supabase.com/evals) and group by product, stage, or agent to see where agents are strong and where they're not. Or see the [GitHub repo](https://github.com/supabase/evals) for details on the benchmark and regression scenarios.
+
+This is a starting point. We'll expand coverage of edge cases within this problem space, and add new scenarios as agents and our product change. We're also working to make our scoring more rigorous and stable. As we build confidence in specific regression scenarios, we may graduate them into the published benchmark. Lastly, we're working on a new CLI command and MCP tool that will allow agents to submit feedback when they struggle, which will help us prioritize what's next.
diff --git a/apps/www/public/images/blog/introducing-supabase-evals/benchmark-results.png b/apps/www/public/images/blog/introducing-supabase-evals/benchmark-results.png
new file mode 100644
index 0000000000000..ab39e6d2ab0f2
Binary files /dev/null and b/apps/www/public/images/blog/introducing-supabase-evals/benchmark-results.png differ
diff --git a/apps/www/public/images/blog/introducing-supabase-evals/og.png b/apps/www/public/images/blog/introducing-supabase-evals/og.png
new file mode 100644
index 0000000000000..55690426490e7
Binary files /dev/null and b/apps/www/public/images/blog/introducing-supabase-evals/og.png differ
diff --git a/apps/www/public/images/blog/introducing-supabase-evals/thumb.png b/apps/www/public/images/blog/introducing-supabase-evals/thumb.png
new file mode 100644
index 0000000000000..4929a91860bf0
Binary files /dev/null and b/apps/www/public/images/blog/introducing-supabase-evals/thumb.png differ
diff --git a/e2e/docs/features/docs-pages.spec.ts b/e2e/docs/features/docs-pages.spec.ts
index 773ca40c9800b..3881a0b54d680 100644
--- a/e2e/docs/features/docs-pages.spec.ts
+++ b/e2e/docs/features/docs-pages.spec.ts
@@ -1,3 +1,4 @@
+import { AxeBuilder } from '@axe-core/playwright'
import { expect, test } from '@playwright/test'
import {
@@ -39,10 +40,6 @@ test.describe('Docs owned pages', () => {
const article = page.locator(articleSelector)
await expect(article, 'Page article should be present').toBeVisible()
- await expect(
- article.getByRole('heading', { level: 1 }),
- 'Page article should include an h1'
- ).toBeVisible()
const links = await collectDocsOwnedLinks(page, baseURL!, articleSelector)
const userAgent = await browserLikeUserAgent(page)
@@ -64,4 +61,22 @@ test.describe('Docs owned pages', () => {
}
})
}
+
+ for (const pagePath of pagePaths) {
+ test(`${pagePath} has a valid heading hierarchy @a11y`, async ({ page }) => {
+ const articleSelector = articleSelectorForPagePath(pagePath)
+ const response = await page.goto(pagePath)
+ expect(response?.ok(), `Expected a successful response for ${pagePath}`).toBeTruthy()
+
+ const axeResults = await new AxeBuilder({ page })
+ .include(articleSelector)
+ .withRules(['heading-order', 'page-has-heading-one'])
+ .analyze()
+
+ expect(
+ axeResults.violations,
+ `Heading hierarchy issues in ${articleSelector}:\n${JSON.stringify(axeResults.violations, null, 2)}`
+ ).toEqual([])
+ })
+ }
})
diff --git a/e2e/docs/package.json b/e2e/docs/package.json
index e54703326c02a..b1e0371f232cf 100644
--- a/e2e/docs/package.json
+++ b/e2e/docs/package.json
@@ -6,6 +6,7 @@
"scripts": {
"e2e:docs": "node --experimental-strip-types scripts/run-e2e-docs.ts",
"e2e:docs:all": "node --experimental-strip-types scripts/run-e2e-docs.ts --all",
+ "e2e:docs:a11y": "node --experimental-strip-types scripts/run-e2e-docs.ts --grep @a11y",
"e2e:ui": "node --experimental-strip-types scripts/run-e2e-docs.ts --ui",
"e2e:docs:local-smoke": "playwright test --config=playwright.local-smoke.config.ts",
"resolve-docs-scope": "node --experimental-strip-types scripts/resolve-docs-scope.ts"
@@ -14,6 +15,7 @@
"@playwright/test": "^1.59.1"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.12.1",
"@types/node": "catalog:"
}
}
diff --git a/packages/pg-meta/src/pg-meta-column-privileges.ts b/packages/pg-meta/src/pg-meta-column-privileges.ts
index ffddcc87f349e..0e9a2f473224f 100644
--- a/packages/pg-meta/src/pg-meta-column-privileges.ts
+++ b/packages/pg-meta/src/pg-meta-column-privileges.ts
@@ -10,7 +10,7 @@ import {
safeSql,
type SafeSqlFragment,
} from './pg-format'
-import { COLUMN_PRIVILEGES_SQL } from './sql/column-privileges'
+import { COLUMN_PRIVILEGES_SQL, getScopedColumnPrivilegesSql } from './sql/column-privileges'
const pgColumnPrivilegeGrant = z.object({
grantor: z.string(),
@@ -50,19 +50,60 @@ function list({
includedSchemas,
excludedSchemas,
columnIds,
+ relationName,
limit,
offset,
+ scoped = false,
}: {
includeSystemSchemas?: boolean
includedSchemas?: string[]
excludedSchemas?: string[]
columnIds?: string[]
+ /** Restricts to a single relation by name. Pair with `includedSchemas`. */
+ relationName?: string
limit?: number
offset?: number
+ scoped?: boolean
} = {}): {
sql: SafeSqlFragment
zod: typeof pgColumnPrivilegesArrayZod
} {
+ // Scoped path: the base query prunes pg_class to the requested relations
+ // before exploding ACLs across their columns, instead of exploding the whole
+ // catalog and filtering the aggregate.
+ if (scoped) {
+ const base = getScopedColumnPrivilegesSql({
+ includeSystemSchemas,
+ includedSchemas,
+ excludedSchemas,
+ relationName,
+ // `columnIds` are "." pairs. Their relation half narrows
+ // the base scan; the exact attnum half stays an outer predicate below.
+ relationIds: columnIds?.length
+ ? [...new Set(columnIds.map((columnId) => columnId.split('.')[0]))]
+ : undefined,
+ })
+
+ let sql = safeSql`
+ with column_privileges as (${base})
+ select *
+ from column_privileges
+ `
+ if (columnIds?.length) {
+ sql = safeSql`${sql} where column_id in (${joinSqlFragments(columnIds.map(literal), ',')})`
+ }
+ if (limit) {
+ sql = safeSql`${sql} limit ${literal(limit)}`
+ }
+ if (offset) {
+ sql = safeSql`${sql} offset ${literal(offset)}`
+ }
+ return {
+ sql,
+ zod: pgColumnPrivilegesArrayZod,
+ }
+ }
+
let sql = safeSql`
with column_privileges as (${COLUMN_PRIVILEGES_SQL})
select *
@@ -80,6 +121,10 @@ function list({
conditions.push(safeSql`relation_schema ${filter}`)
}
+ if (relationName) {
+ conditions.push(safeSql`relation_name = ${literal(relationName)}`)
+ }
+
if (columnIds?.length) {
conditions.push(safeSql`column_id in (${joinSqlFragments(columnIds.map(literal), ',')})`)
}
diff --git a/packages/pg-meta/src/sql/column-privileges.ts b/packages/pg-meta/src/sql/column-privileges.ts
index def165c2675d2..fd6b3e2813d2e 100644
--- a/packages/pg-meta/src/sql/column-privileges.ts
+++ b/packages/pg-meta/src/sql/column-privileges.ts
@@ -1,6 +1,12 @@
-import { safeSql } from '../pg-format'
+import { DEFAULT_SYSTEM_SCHEMAS } from '../constants'
+import { filterByList } from '../helpers'
+import { joinSqlFragments, literal, safeSql, type SafeSqlFragment } from '../pg-format'
export const COLUMN_PRIVILEGES_SQL = /* SQL */ safeSql`
+-- FROZEN legacy path: served while the pgMetaScopedIntrospection flag is off.
+-- Do not edit -- it must keep matching production behavior until the flag
+-- cleanup deletes it. getScopedColumnPrivilegesSql is the replacement.
+--
-- Lists each column's privileges in the form of:
--
-- [
@@ -147,3 +153,145 @@ group by column_id,
x.relname,
x.attname
`
+
+export type ColumnPrivilegesScope = {
+ /** Defaults to false, which excludes {@link DEFAULT_SYSTEM_SCHEMAS}. */
+ includeSystemSchemas?: boolean
+ includedSchemas?: Array
+ excludedSchemas?: Array
+ /** Restricts to a single relation by name. Pair with `includedSchemas`. */
+ relationName?: string
+ /** Restricts to specific relations by oid. */
+ relationIds?: Array
+}
+
+/**
+ * Scoped variant of {@link COLUMN_PRIVILEGES_SQL}: prunes pg_class/pg_namespace
+ * FIRST (in the `rel` CTE) so the scope predicate can drive an index scan,
+ * instead of aclexploding the whole catalog and filtering the aggregated result.
+ */
+export const getScopedColumnPrivilegesSql = ({
+ includeSystemSchemas = false,
+ includedSchemas,
+ excludedSchemas,
+ relationName,
+ relationIds,
+}: ColumnPrivilegesScope = {}): SafeSqlFragment => {
+ const conditions: Array = []
+
+ const schemaFilter = filterByList(
+ includedSchemas,
+ excludedSchemas,
+ !includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined
+ )
+ if (schemaFilter) {
+ conditions.push(safeSql`and nc.nspname ${schemaFilter}`)
+ }
+ if (relationName) {
+ conditions.push(safeSql`and c.relname = ${literal(relationName)}`)
+ }
+ if (relationIds?.length) {
+ conditions.push(safeSql`and c.oid in (${joinSqlFragments(relationIds.map(literal), ',')})`)
+ }
+ const scopeFilter = joinSqlFragments(conditions, '\n')
+
+ return safeSql`
+with rel as (
+ select
+ c.oid,
+ c.relname,
+ c.relowner,
+ c.relacl,
+ nc.nspname
+ from pg_class c
+ join pg_namespace nc
+ on nc.oid = c.relnamespace
+ where c.relkind = any (array['r', 'v', 'm', 'f', 'p'])
+ ${scopeFilter}
+),
+roles as (
+ select
+ r.oid,
+ r.rolname,
+ pg_has_role(r.oid, 'USAGE') as is_member
+ from pg_authid r
+),
+grantees as (
+ select oid, rolname, is_member, false as is_public from roles
+ union all
+ select (0)::oid as oid, 'PUBLIC', false, true
+),
+priv as (
+ -- Table-level ACLs apply to every live column of the relation.
+ select
+ a.attrelid,
+ a.attnum,
+ a.attname,
+ r.relname,
+ r.nspname,
+ p.grantor,
+ p.grantee,
+ p.privilege_type as prtype,
+ p.is_grantable as grantable
+ from rel r
+ cross join lateral aclexplode(coalesce(r.relacl, acldefault('r', r.relowner))) p
+ join pg_attribute a
+ on a.attrelid = r.oid
+ and a.attnum > 0
+ and not a.attisdropped
+ where p.privilege_type = any (array['INSERT', 'SELECT', 'UPDATE', 'REFERENCES'])
+
+ union
+
+ -- Column-level ACLs.
+ select
+ a.attrelid,
+ a.attnum,
+ a.attname,
+ r.relname,
+ r.nspname,
+ p.grantor,
+ p.grantee,
+ p.privilege_type,
+ p.is_grantable
+ from rel r
+ join pg_attribute a
+ on a.attrelid = r.oid
+ and a.attnum > 0
+ and not a.attisdropped
+ cross join lateral aclexplode(coalesce(a.attacl, acldefault('c', r.relowner))) p
+ where a.attacl is not null
+ and p.privilege_type = any (array['INSERT', 'SELECT', 'UPDATE', 'REFERENCES'])
+)
+select
+ (p.attrelid || '.' || p.attnum) as column_id,
+ p.nspname as relation_schema,
+ p.relname as relation_name,
+ p.attname as column_name,
+ coalesce(
+ jsonb_agg(
+ jsonb_build_object(
+ 'grantor', grantor.rolname,
+ 'grantee', grantee.rolname,
+ 'privilege_type', p.prtype,
+ 'is_grantable', p.grantable
+ )
+ ),
+ '[]'
+ ) as privileges
+from priv p
+join roles grantor
+ on grantor.oid = p.grantor
+join grantees grantee
+ on grantee.oid = p.grantee
+where grantor.is_member
+ or grantee.is_member
+ or grantee.is_public
+group by
+ p.attrelid,
+ p.attnum,
+ p.nspname,
+ p.relname,
+ p.attname
+`
+}
diff --git a/packages/pg-meta/test/column-privileges.test.ts b/packages/pg-meta/test/column-privileges.test.ts
index 9eb5ef2c93e5a..bf8ca0db94128 100644
--- a/packages/pg-meta/test/column-privileges.test.ts
+++ b/packages/pg-meta/test/column-privileges.test.ts
@@ -355,3 +355,80 @@ withTestDatabase(
)
}
)
+
+withTestDatabase('scoped list matches the legacy path row-for-row', async ({ executeQuery }) => {
+ await executeQuery(`
+ drop role if exists col_grantee_a;
+ drop role if exists col_grantee_b;
+ create role col_grantee_a;
+ create role col_grantee_b;
+ create table public.col_priv_demo (id int primary key, data text, extra text);
+ grant select, insert on public.col_priv_demo to col_grantee_a;
+ grant update (data) on public.col_priv_demo to col_grantee_b with grant option;
+ grant references (extra) on public.col_priv_demo to col_grantee_a;
+ grant select on public.col_priv_demo to public;
+ create view public.col_priv_view as select id, data from public.col_priv_demo;
+ grant select (id) on public.col_priv_view to col_grantee_b;
+ `)
+
+ // Neither path orders rows or the privileges array (see the note on the first
+ // test in this file), and the two plans emit them in different orders, so
+ // compare on normalized copies.
+ const normalize = }>(rows: Array) =>
+ rows
+ .map((row) => ({
+ ...row,
+ privileges: [...row.privileges].sort((a, b) =>
+ JSON.stringify(a).localeCompare(JSON.stringify(b))
+ ),
+ }))
+ .sort((a, b) => a.column_id.localeCompare(b.column_id))
+
+ for (const options of [
+ {},
+ { includedSchemas: ['public'] },
+ { excludedSchemas: ['public'] },
+ { includedSchemas: ['public'], relationName: 'col_priv_demo' },
+ { includedSchemas: ['public'], relationName: 'col_priv_view' },
+ { includedSchemas: ['public'], relationName: 'does_not_exist' },
+ { includeSystemSchemas: true, includedSchemas: ['public'] },
+ ]) {
+ const legacy = pgMeta.columnPrivileges.list(options)
+ const scoped = pgMeta.columnPrivileges.list({ ...options, scoped: true })
+ const legacyRes = legacy.zod.parse(await executeQuery(legacy.sql))
+ const scopedRes = scoped.zod.parse(await executeQuery(scoped.sql))
+ expect(normalize(scopedRes), `list options: ${JSON.stringify(options)}`).toEqual(
+ normalize(legacyRes)
+ )
+ // Row counts must match exactly, independent of the normalization above.
+ expect(scopedRes.length, `row count for options: ${JSON.stringify(options)}`).toBe(
+ legacyRes.length
+ )
+ }
+
+ // The scoped path must surface the PUBLIC grantee, and the column-level grants
+ // that only the second UNION arm can produce.
+ const { sql, zod } = pgMeta.columnPrivileges.list({
+ includedSchemas: ['public'],
+ relationName: 'col_priv_demo',
+ scoped: true,
+ })
+ const rows = zod.parse(await executeQuery(sql))
+ const dataColumn = rows.find((r) => r.column_name === 'data')!
+ expect(rows.map((r) => r.column_name).sort()).toEqual(['data', 'extra', 'id'])
+ expect(dataColumn.privileges.some((p) => p.grantee === 'PUBLIC')).toBe(true)
+ expect(
+ dataColumn.privileges.some(
+ (p) => p.grantee === 'col_grantee_b' && p.privilege_type === 'UPDATE' && p.is_grantable
+ )
+ ).toBe(true)
+
+ // columnIds: the scoped path prunes pg_class by relation oid and keeps the
+ // exact attnum predicate outside, so it must still match legacy exactly.
+ const columnIds = rows.map((r) => r.column_id).slice(0, 2)
+ const legacyByIds = pgMeta.columnPrivileges.list({ columnIds })
+ const scopedByIds = pgMeta.columnPrivileges.list({ columnIds, scoped: true })
+ expect(normalize(scopedByIds.zod.parse(await executeQuery(scopedByIds.sql)))).toEqual(
+ normalize(legacyByIds.zod.parse(await executeQuery(legacyByIds.sql)))
+ )
+})
diff --git a/packages/pg-meta/test/sql/studio/catalog-plan-guard.test.ts b/packages/pg-meta/test/sql/studio/catalog-plan-guard.test.ts
index dab67cf19fad1..beeb163d76bf4 100644
--- a/packages/pg-meta/test/sql/studio/catalog-plan-guard.test.ts
+++ b/packages/pg-meta/test/sql/studio/catalog-plan-guard.test.ts
@@ -13,6 +13,7 @@ import {
getTablesPaginatedSql,
getViewDefinitionSql,
} from '../../../src'
+import columnPrivileges from '../../../src/pg-meta-column-privileges'
import tablePrivileges from '../../../src/pg-meta-table-privileges'
import * as tables from '../../../src/pg-meta-tables'
import * as types from '../../../src/pg-meta-types'
@@ -374,6 +375,62 @@ test('tablePrivileges.retrieve: scoped plan stays scoped for a single relation',
assertPlanWithinBudget(result, TABLE_PRIVILEGES_BUDGET)
}, 60_000)
+// ── columnPrivileges.list (sql/column-privileges.ts) — per-table ─────────────
+// The Studio hot path: the column-level privileges page renders exactly one
+// table, so the query is scoped to schema+relname. Scoped prunes pg_class in the
+// `rel` CTE before the aclexplode laterals / UNION / GROUP BY, so pg_class is
+// reached via its (relname, relnamespace) index and pg_attribute via
+// pg_attribute_relid_attnum_index.
+//
+// Only pg_authid is seq-scanned, once: the `roles` CTE is referenced by both the
+// grantor join and the `grantees` UNION, so it materializes and pg_has_role() is
+// evaluated once per role instead of twice per privilege row. (Table privileges
+// needs two scans here because it joins the pg_roles view twice.)
+const COLUMN_PRIVILEGES_TABLE_SCOPED_BUDGET = {
+ allowedSeqScans: {
+ pg_authid: {
+ max: 1,
+ reason:
+ 'the `roles` CTE materializes once and feeds both the grantor join and the grantee-with-PUBLIC union; pg_authid scales with role count, not schema size',
+ },
+ },
+}
+
+test('columnPrivileges.list: scoped plan stays scoped for a single table', async () => {
+ const result = await explainAnalyze(
+ db,
+ columnPrivileges.list({
+ includedSchemas: ['stress'],
+ relationName: 't_1000',
+ scoped: true,
+ }).sql
+ )
+ assertPlanWithinBudget(result, COLUMN_PRIVILEGES_TABLE_SCOPED_BUDGET)
+}, 60_000)
+
+// ── columnPrivileges.list (sql/column-privileges.ts) — per-schema listing ────
+// Studio always passes a relation (above); a schema-wide call is still supported
+// for pg-meta consumers. Here one filtered pg_class seq scan is the right plan
+// rather than a regression: the whole `stress` schema is most of the catalog.
+const COLUMN_PRIVILEGES_SCHEMA_SCOPED_BUDGET = {
+ allowedSeqScans: {
+ ...COLUMN_PRIVILEGES_TABLE_SCOPED_BUDGET.allowedSeqScans,
+ pg_class: {
+ max: 1,
+ reason:
+ 'schema-wide listing selects nearly every relation in the schema, so one filtered pg_class scan beats per-relation index lookups; pg_attribute stays index-driven',
+ },
+ },
+}
+
+test('columnPrivileges.list: scoped plan stays scoped for a schema', async () => {
+ const result = await explainAnalyze(
+ db,
+ columnPrivileges.list({ includedSchemas: ['stress'], scoped: true }).sql
+ )
+ assertPlanWithinBudget(result, COLUMN_PRIVILEGES_SCHEMA_SCOPED_BUDGET)
+}, 60_000)
+
// ── tables.retrieve (pg-meta-tables.ts / sql/tables.ts) — single table ───────
// Scoped pushes the target OID (a literal, or an initplan scalar subquery
// resolved via pg_class's (relname, relnamespace) index) into the base scan and
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 99d33c593762a..55dcc827d8724 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2021,6 +2021,9 @@ importers:
specifier: ^1.59.1
version: 1.59.1
devDependencies:
+ '@axe-core/playwright':
+ specifier: ^4.12.1
+ version: 4.12.1(playwright-core@1.59.1)
'@types/node':
specifier: 'catalog:'
version: 22.13.14
@@ -3133,6 +3136,11 @@ packages:
resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==}
engines: {node: '>=18.0.0'}
+ '@axe-core/playwright@4.12.1':
+ resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==}
+ peerDependencies:
+ playwright-core: '>= 1.0.0'
+
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -9549,6 +9557,10 @@ packages:
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
engines: {node: '>=4'}
+ axe-core@4.12.1:
+ resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==}
+ engines: {node: '>=4'}
+
axios@1.18.1:
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
@@ -18498,6 +18510,11 @@ snapshots:
'@aws/lambda-invoke-store@0.2.4': {}
+ '@axe-core/playwright@4.12.1(playwright-core@1.59.1)':
+ dependencies:
+ axe-core: 4.12.1
+ playwright-core: 1.59.1
+
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -25765,6 +25782,8 @@ snapshots:
axe-core@4.10.3: {}
+ axe-core@4.12.1: {}
+
axios@1.18.1(supports-color@8.1.1):
dependencies:
follow-redirects: 1.16.0