Provisioning Postgres role credentials without exposing them in logs
When you set a Postgres role password or register a secret value with SQL, the value can be written to your project logs. Supabase masks the password in the normal statement log for standard password statements, but some ways of writing the statement, and some failure cases, place the value in your logs in clear text.
The reliable way to handle this is to avoid putting the secret into a SQL statement in the first place. This page gives the practices that prevent the exposure, a way to check your own project, and a fallback for the cases where you must run the statement in SQL.
This guidance applies to anything that issues SQL against your database, including automated tools and AI agents that provision roles or store secrets on your behalf.
Overview
What is and isn’t kept out of your logs
Supabase masks the password for the standard, single-quoted password statements, so the normal statement log shows PASSWORD '{REDACTED}':
CREATE USER ... WITH PASSWORD '...'CREATE ROLE ... PASSWORD '...'ALTER USER ... WITH PASSWORD '...'ALTER ROLE ... PASSWORD '...'ALTER ROLE ... ENCRYPTED PASSWORD '...'
The value is not kept out of your logs in the cases below. Treat a value as exposed if it is set in any of these ways, and rotate it if it was a real credential.
Masking applies only to the standard single-quoted statement log line. It does not extend to the cases below, where the value is written in clear.
- Dollar-quoted passwords.
ALTER ROLE r PASSWORD $$secret$$orPASSWORD $tag$secret$tag$is written to the statement log in clear. Only the single-quoted form is masked. - Any statement that fails. When a password statement fails (for example, the role does not exist, already exists, or has a syntax error), the accompanying error entry includes the statement text, and the value in that entry is in clear.
- A value in an error detail. When a statement fails with a detail that quotes a value — for example a unique-violation detail of the form
Key (column)=(value) already exists— that value is written in clear, even when the statement itself used a parameter. - A password built into dynamic SQL. A password placed into a string that a
DOblock or function runs withEXECUTEcan appear in the error context if that inner statement fails. - Statements captured by extended logging. If you raise
log_statementtoall, an inline literal value in a successful statement is written to the log. A value passed as a query parameter is written as a placeholder instead.
pg_stat_statements keeps the password text of CREATE/ALTER ROLE/USER statements. Values passed as query parameters are normalized to a placeholder there.
Diagnose the problem
Check what your project logs today
To see the logging settings that determine whether a value reaches your logs, and whether pgAudit is active, run the following SQL using the SQL Editor in the Supabase Dashboard or with psql over a direct connection. To connect with psql, see Direct connection.
select current_setting('log_statement') as log_statement, current_setting('log_min_error_statement') as log_min_error_statement, current_setting('log_min_messages') as log_min_messages, current_setting('pgaudit.log', true) as pgaudit_log;Reproduce the exposure with a non-secret value
To see the behavior without a real credential, use a value that is not a secret, then search your logs for it. Never use a real password or verifier for this test.
Run the following SQL using the SQL Editor in the Supabase Dashboard or with psql over a direct connection. To connect with psql, see Direct connection.
create table public.canary_check (id int generated always as identity, token text unique);insert into public.canary_check(token) values ('SYNTHETIC-CANARY');insert into public.canary_check(token) values ('SYNTHETIC-CANARY');The second insert fails with a unique violation, and the detail records Key (token)=(SYNTHETIC-CANARY) already exists. Open the Logs Explorer and search for SYNTHETIC-CANARY to see it. Drop the table when you finish:
drop table public.canary_check;Solve the problem
The practices below are ordered so that the first ones remove the exposure entirely. Prefer them in order. Reach the transaction-scoped fallback only when none of the earlier practices apply.
Set project and role passwords without SQL
Set or rotate the database password through the Dashboard or the Management API rather than with an ALTER ROLE statement. When there is no password statement, there is nothing to appear in a log. See Changing your project password.
Don’t reconstruct a password into SQL text inside a DO block or a function. That moves the value into a statement your own code builds, where the masking doesn’t apply and a failure can place the value in the error context.
Use temporary access instead of issuing a password
When the goal is short-lived access rather than a lasting login, use Temporary Access. Authorized project members assume an existing role with an expiring token, so no durable password is created or set. This avoids the password-handling step altogether. See Temporary Access.
Store application secrets with Supabase Vault
When you need to keep a secret that your application reads — an API key, a token, a connection string — store it with Supabase Vault rather than inserting it into a table of your own. Create the secret by calling vault.create_secret, which takes the secret as an argument.
Run the following SQL using the SQL Editor in the Supabase Dashboard or with psql over a direct connection. To connect with psql, see Direct connection.
select vault.create_secret('your-secret-value', 'secret_name', 'optional description');Passing the secret as an argument to vault.create_secret keeps it out of the statement log, out of an error detail if the call fails, and out of pg_stat_statements. Vault also encrypts the secret at rest. Don’t place a secret as an inline literal in your own INSERT; that is the pattern this avoids. See Supabase Vault.
If you set a password in SQL, write it so it can’t leak
When a password must be set with SQL, two rules keep it out of the normal statement log:
- Use the single-quoted form,
PASSWORD 'your-password'. Don’t use the dollar-quoted form ($$...$$or$tag$...$tag$), which is not masked. - Pass values as query parameters rather than building SQL by concatenating the value into the statement text. A parameter is written to the log as a placeholder; an inline literal is written in clear.
If you set the password interactively, use the psql \password command. It prompts for the new password, computes the hash on the client, and sends a standard single-quoted ALTER ROLE statement, so the password doesn’t enter your shell history and is masked in the statement log. To also cover the case where the change fails, run it inside the transaction below. See the Postgres documentation on \password.
These rules only mask the normal statement log. They don’t cover the case where the statement fails; for that, use the transaction below.
Fallback: suppress logging for a single transaction
When you must run a password change or a registration statement that could fail, and the practices above don’t apply, wrap it in one transaction that turns off the relevant logging for that transaction only. This prevents the entry from being written.
log_min_messages = 'fatal' silences all error logging for the transaction, not only the statement you are protecting. Keep the transaction scoped to the single statement so you don’t lose error visibility for anything else. Your application still receives the error either way.
Run the following SQL using the SQL Editor in the Supabase Dashboard or with psql over a direct connection. To connect with psql, see Direct connection.
begin;set local log_statement = 'none';set local log_min_error_statement = 'panic';set local log_min_messages = 'fatal';set local pg_stat_statements.track = 'none';set local pgaudit.log = 'none';-- run your password change or registration statement here, on this connectioncommit;Notes on this transaction:
log_min_error_statement = 'panic'alone removes the statement text from an error, but not an error detail.log_min_messages = 'fatal'is what suppresses a detail such as a unique-violation detail.set localapplies only to the current transaction and is restored automatically oncommit, onrollback, and when the connection ends. There is nothing to reset by hand.- Use one explicit transaction on a single connection. On the transaction pooler,
set localis the correct choice, because a plainsetcould carry over to another client that reuses the connection. log_error_verbositycan’t be changed on managed projects, solog_min_messagesis the setting to rely on for the detail case.- Include
set local pgaudit.log = 'none'only if you use pgAudit.
For automated tools and AI agents
If a routine provisions roles or stores secrets on your behalf, encode these as rules so it avoids the exposure by construction:
- To set or rotate a database password, call the Management API. Don’t emit
ALTER ROLE ... PASSWORD. - For temporary access, use Temporary Access rather than creating a role with a password.
- To store a secret, call
vault.create_secret(...). Never place a secret as an inline literal in anINSERT. - If a password must be set in SQL, use the single-quoted
PASSWORD '...'form and pass values as parameters. Never use dollar-quoting, and never build SQL by concatenating a secret into the statement. When setting it from apsqlsession, use the\passwordcommand. - Don’t place a password or secret into a
DOblock or a function that runs it withEXECUTE.
Confirm the problem is solved
Repeat the test in Reproduce the exposure with a non-secret value using the practice you adopted and a different non-secret value — for example, store the value with vault.create_secret, or wrap the failing statement in the transaction above. Then search the Logs Explorer for that value. When the practice is applied, the value doesn’t appear.
If you used the transaction, confirm the settings reverted afterward:
show log_statement;show log_min_messages;Both return their defaults, because set local applies only to the transaction.
Prevent the problem
- Set passwords through the Dashboard or the Management API, not with hand-written
ALTER ROLEstatements. - Use Temporary Access when you need short-lived access rather than a lasting password.
- Store application secrets with
vault.create_secret, so a value is never an inline literal that a failing statement can log. - When you do write password SQL, use the single-quoted form and pass values as parameters.
- Keep secrets out of the places the database doesn’t control: shell history, SQL Editor history, migration files, CI logs, and any external tooling you operate.
Related topics
- Postgres log configuration
- Logs
- Interpret and explore the Postgres logs
- Reset your database password