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.

  • Dollar-quoted passwords. ALTER ROLE r PASSWORD $$secret$$ or PASSWORD $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 DO block or function runs with EXECUTE can appear in the error context if that inner statement fails.
  • Statements captured by extended logging. If you raise log_statement to all, 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.

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 connection
commit;

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 local applies only to the current transaction and is restored automatically on commit, on rollback, 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 local is the correct choice, because a plain set could carry over to another client that reuses the connection.
  • log_error_verbosity can’t be changed on managed projects, so log_min_messages is 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 an INSERT.
  • 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 a psql session, use the \password command.
  • Don’t place a password or secret into a DO block or a function that runs it with EXECUTE.

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 ROLE statements.
  • 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.