mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
[bot] Sync from supabase/troubleshooting (#44967)
This PR syncs the latest troubleshooting guides from the supabase/troubleshooting repository. --------- Co-authored-by: github-docs-bot <github-docs-bot@supabase.com> Co-authored-by: Chris Chinchilla <chris.ward@supabase.io> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>
This commit is contained in:
6 files changed
+260
No files matched your search
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title = "'Disk size not shrinking after deleting data'"
|
||||
topics = [ "database", "storage" ]
|
||||
keywords = []
|
||||
---
|
||||
|
||||
If you've deleted a significant amount of data (e.g., by dropping large tables), you may observe that your project's disk size does not automatically shrink. This is expected behavior as Postgres reclaims freed space for internal reuse but does not return it to the operating system. Furthermore, cloud storage volumes (such as AWS EBS) do not support in-place shrinking, which means the disk size cannot be directly reduced via the dashboard UI.
|
||||
|
||||
To reclaim disk space and reduce your project's disk allocation, you will need to initiate a Postgres version upgrade. This process provisions a new instance with a smaller disk attached, migrates your data, and involves brief downtime during the upgrade and switchover. You can start this process from the [Supabase Dashboard](/dashboard/project/_/settings/database).
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title = "Resolving 'cannot execute UPDATE in a read-only transaction' on transaction pooler connections"
|
||||
topics = [ "database", "supavisor" ]
|
||||
keywords = []
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
**Backend Connection**
|
||||
The actual physical process on the Postgres server that executes your queries. In pooling environments, one backend connection will serve many different database clients over its lifetime.
|
||||
|
||||
**Session-Level State**
|
||||
That backend connection has _state_. Postgres connections carry settings like timezone, memory limits, search path, read-only mode etc. In a normal setup where each client has its own connection, this doesn't matter. When the client disconnects, the connection and all its state go away. In a pooled environment, the connection doesn't go away. It goes back into the pool, settings and all, and the next client who gets it inherits whatever was left behind.
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
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?
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Search your application code, migration scripts, and maintenance tasks for the following session-level commands:
|
||||
|
||||
- `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`
|
||||
- `SET default_transaction_read_only = on;`
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
- **Avoid:** `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;` or `SET default_transaction_read_only = on;` (these contaminate the connection pool)
|
||||
- **Use:** `BEGIN TRANSACTION READ ONLY;` or `BEGIN; SET TRANSACTION READ ONLY;` (these only affect the current transaction)
|
||||
|
||||
If you have existing scripts that use session-level settings and cannot be immediately refactored:
|
||||
|
||||
- **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
|
||||
|
||||
Ensure your application is using the intended pooler.
|
||||
|
||||
- **Shared Transaction Pooler (Supavisor):** `postgresql://postgres.PROJECT_REF:[YOUR-PASSWORD]@aws-X-REGION.pooler.supabase.com:6543/postgres`
|
||||
- **Dedicated Transaction Pooler (PgBouncer):** `postgresql://postgres:[YOUR-PASSWORD]@db.PROJECT_REF.supabase.co:6543/postgres`
|
||||
|
||||
---
|
||||
|
||||
### 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.%
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title = "Identifying Dashboard SQL Editor Activity by User"
|
||||
topics = [ "database" ]
|
||||
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**
|
||||
|
||||
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`.
|
||||
|
||||
By default, that role has `log_statement` set to `ddl`, which means Postgres logs only schema-level changes such as `CREATE`, `ALTER`, and `DROP`. It does not log data-modifying statements such as `INSERT`, `UPDATE`, `DELETE` or `TRUNCATE`.
|
||||
|
||||
So, if someone truncates a table, and you're relying on the default logging, you won't see it.
|
||||
|
||||
### **Enabling data modification logging**
|
||||
|
||||
To make those operations visible, you can increase the logging level for the `postgres` role:
|
||||
|
||||
```sql
|
||||
ALTER ROLE postgres SET log_statement='mod';
|
||||
```
|
||||
|
||||
**Note:** This step is only necessary if you need to track data-modifying statements like `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`. If you're only looking to track DDL statements (such as `CREATE`, `ALTER`, `DROP`), the default `log_statement='ddl'` setting is already sufficient.
|
||||
|
||||
Setting it to `mod` tells Postgres to log all data-modifying statements. Once that's in place, try running something like a `TRUNCATE` from the Dashboard. In the logs, you'll see an entry similar to:
|
||||
|
||||
```
|
||||
statement: TRUNCATE TABLE public.data;
|
||||
-- source: dashboard
|
||||
-- user: f8c2e1a9-3b4d-4f7e-8c9a-1d2e3f4a5b6c
|
||||
-- date: 2026-04-02T11:41:22.158Z
|
||||
```
|
||||
|
||||
Notice that the log includes:
|
||||
|
||||
- The full statement
|
||||
- The timestamp
|
||||
- A `user` field, which is actually the Supabase user UUID
|
||||
- The source (dashboard)
|
||||
|
||||
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 just an ID - not yet a name or email.
|
||||
|
||||
### **Mapping UUIDs to team members**
|
||||
|
||||
To map the UUID, you'll need to query the Management API. The process looks like this:
|
||||
|
||||
**1. Create a Personal Access Token (PAT)**
|
||||
|
||||
Generate a token from your [account settings](/dashboard/account/tokens).
|
||||
|
||||
**2. Call the Organization Members Endpoint**
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.supabase.com/v1/organizations/your-org-slug/members" \
|
||||
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
**3. Match the UUID**
|
||||
|
||||
The response will include entries like:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "f8c2e1a9-3b4d-4f7e-8c9a-1d2e3f4a5b6c",
|
||||
"user_name": "john@supabase.io",
|
||||
"email": "john@supabase.io",
|
||||
"role_name": "Administrator"
|
||||
}
|
||||
```
|
||||
|
||||
Now you can directly match `user_id` values from the Postgres logs to the corresponding team members.
|
||||
|
||||
### **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:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
DATETIME(postgres_logs.timestamp) AS time,
|
||||
parsed.session_id,
|
||||
postgres_logs.identifier,
|
||||
parsed.user_name AS db_role,
|
||||
CASE
|
||||
WHEN REGEXP_CONTAINS(postgres_logs.event_message, 'f8c2e1a9-3b4d-4f7e-8c9a-1d2e3f4a5b6c')
|
||||
THEN 'john@example.com'
|
||||
WHEN REGEXP_CONTAINS(postgres_logs.event_message, 'insert another-uuid-here')
|
||||
THEN 'jane@example.io'
|
||||
ELSE 'unknown'
|
||||
END AS detected_user,
|
||||
parsed.error_severity,
|
||||
postgres_logs.event_message
|
||||
FROM postgres_logs
|
||||
CROSS JOIN UNNEST(metadata) AS metadata
|
||||
CROSS JOIN UNNEST(parsed) AS parsed
|
||||
WHERE postgres_logs.timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
|
||||
AND (
|
||||
REGEXP_CONTAINS(postgres_logs.event_message, '(?i)DELETE|TRUNCATE|UPDATE|ALTER|DROP')
|
||||
OR REGEXP_CONTAINS(parsed.query, '(?i)DELETE|TRUNCATE|UPDATE|ALTER|DROP')
|
||||
)
|
||||
ORDER BY postgres_logs.timestamp DESC
|
||||
LIMIT 500;
|
||||
```
|
||||
|
||||
This query:
|
||||
|
||||
- Searches the last 7 days of logs
|
||||
- Filters for common data-modifying operations (`DELETE`, `TRUNCATE`, `UPDATE`, `ALTER`, `DROP`)
|
||||
- Uses a `CASE` statement to map known UUIDs to team member emails
|
||||
- Returns results ordered by timestamp (most recent first)
|
||||
|
||||
**Example Output:**
|
||||
| db_role | detected_user | error_severity | event_message | identifier |
|
||||
| -------- | ------------- | -------------- | -------------------------------------------------------------------------------- | ---------- |
|
||||
{/* supa-mdx-lint-disable-next-line Rule003Spelling */}
|
||||
| postgres | support | LOG | statement: TRUNCATE TABLE public.data; -- source: dashboard -- user: f8c2e1a9... | ... |
|
||||
|
||||
You can further refine your search by filtering for specific commands like `TRUNCATE` or `DELETE` where `parsed.user_name = 'postgres'`.
|
||||
|
||||
### **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**
|
||||
|
||||
Postgres supports these `log_statement` values:
|
||||
|
||||
- `none`: No statements are logged
|
||||
- `ddl`: Log data definition statements (`CREATE`, `ALTER`, `DROP`) - this is the default for the `postgres` role
|
||||
- `mod`: Log data modification statements plus all DDL (includes `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`)
|
||||
- `all`: Log all statements (including `SELECT` queries)
|
||||
|
||||
**Note:** Setting to `all` can generate very large log volumes. Use it only when necessary and for limited periods. Test the performance impact of `log_statement='mod'` in your specific environment, as the impact depends on your query volume and workload.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title = "`Unexpected behavior with 'auth.updateUser({ phone })': Phone linked to incorrect user ID`"
|
||||
topics = [ "auth" ]
|
||||
keywords = []
|
||||
---
|
||||
|
||||
When using `auth.updateUser({ phone: '...' })`, you might observe that a phone number is unexpectedly linked to a different `auth.users` record than the currently authenticated user during the phone verification process, even if `auth.getUser()` reports the correct user ID beforehand.
|
||||
|
||||
**Why does this happen?**
|
||||
Supabase phone verification identifies the user by searching for the provided phone number in the `phone_change` column, rather than relying solely on the active session. Unlike the `phone` column, the `phone_change` column does not enforce uniqueness. If multiple `auth.users` records contain the same phone number in `phone_change` due to uncompleted or abandoned verification attempts, the system may update an unintended user's `phone` field upon successful OTP verification. This occurs because the system finds and updates the first matching record in `phone_change`, which might not belong to the currently authenticated user.
|
||||
|
||||
**How to prevent/resolve this:**
|
||||
To prevent ambiguous lookups from abandoned verification attempts, implement application-level cleanup to remove stale `phone_change` values from your `auth.users` records.
|
||||
|
||||
1. **Define a grace period:** Establish a reasonable time frame after which an unconfirmed phone verification attempt is considered stale.
|
||||
2. **Identify stale records:** Periodically query `auth.users` to find accounts where `phone_verified` is `false` and the `phone_change` value has been present beyond your defined grace period.
|
||||
3. **Clear `phone_change`:** For identified stale records, clear their `phone_change` value. This ensures that only active and unique `phone_change` entries are considered during verification.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title = "Vercel Integration: Environment variables explained"
|
||||
topics = [ "platform" ]
|
||||
keywords = []
|
||||
---
|
||||
|
||||
Vercel has three environments, which map to different stages of the deployment lifecycle:
|
||||
|
||||
- **Production** is used for the branch configured as the production branch (usually main). Deployments from that branch go to the live site.
|
||||
- **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
|
||||
|
||||
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.
|
||||
|
||||
You can create a dedicated environment (Vercel Pro):
|
||||
|
||||
1. Go to `Project` → `Settings` → `Environments`
|
||||
2. Click `Create Environment`
|
||||
3. Name it `staging`
|
||||
4. Enable `Branch Tracking` and select the `staging` branch
|
||||
5. Add environment variables scoped to this environment
|
||||
|
||||
With this setup, the staging branch deploys to its own environment and is fully separate from Preview and Production.
|
||||
@@ -254,6 +254,7 @@ allow_list = [
|
||||
"LlamaIndex",
|
||||
"Llamafile",
|
||||
"Logflare",
|
||||
"[Ll]ookups?",
|
||||
"Lovable",
|
||||
"Lovable Cloud",
|
||||
"Lua",
|
||||
|
||||
Reference in new issue
Block a user