✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

EcosystemIntermediate●5 min read

"SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string" in Strapi

●September 9, 2026
"SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string" in Strapi

Strapi refuses to start and throws this from deep inside node_modules:

node_modules/pg/lib/sasl.js:24
  throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string')

The wording sends people hunting for SASL configuration, authentication mechanisms, and PostgreSQL pg_hba.conf settings. None of that is the problem.

What the error actually means

PostgreSQL 10+ uses SCRAM-SHA-256 for password authentication. When the pg Node driver starts that handshake, it needs to hash the password, and hashing requires a string. The driver checks the type first and throws this error if it gets anything else.

In practice, "anything else" is almost always undefined.

So the real message is: Strapi passed undefined as the database password. The password is missing, not wrong. A wrong password produces a different error (password authentication failed for user "...").

This is a type check failing before any network traffic reaches your database.

Confirm it in ten seconds

Add a temporary log line at the top of config/database:

export default ({ env }) => {
  console.log('DB password type:', typeof env('DATABASE_PASSWORD'));
  console.log('DB password set:', Boolean(env('DATABASE_PASSWORD')));

  return {
    connection: {
      client: 'postgres',
      connection: {
        host: env('DATABASE_HOST', '127.0.0.1'),
        port: env.int('DATABASE_PORT', 5432),
        database: env('DATABASE_NAME', 'strapi'),
        user: env('DATABASE_USERNAME', 'strapi'),
        password: env('DATABASE_PASSWORD'),
        ssl: env.bool('DATABASE_SSL', false),
      },
    },
  };
};

If that prints undefined / false, you have confirmed the diagnosis. Remove the log lines before committing, and never log the value itself, only its type.

The causes, roughly in order of frequency

1. DATABASE_PASSWORD is not set in the environment

The most common case. Your config/database reads env('DATABASE_PASSWORD') with no fallback, and the variable does not exist.

Check that .env exists at the project root, sits next to package.json, and contains the key:

DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=your-actual-password

Common variations on this mistake:

  • The file is named .env.local, env, or .env.txt. Strapi loads .env.
  • The file is in a subdirectory. Strapi loads it from the project root only.
  • You are running the process from a different working directory, so the relative path to .env does not resolve.

2. The Postgres role genuinely has no password

Fresh local installs of PostgreSQL often create a postgres superuser with no password set, relying on peer authentication for local socket connections. Strapi connects over TCP, which requires a password.

sudo -u postgres psql
\password postgres

Then set the same value in your .env.

3. A deployed environment where the variable was never added

Everything works locally and the deploy fails. The .env file is (correctly) gitignored, and nobody added the variables to the platform.

  • Docker Compose: env_file: .env or an explicit environment: block. A .env next to docker-compose.yml populates interpolation in the compose file, which is not the same as being present inside the container.
  • Dockerfile: .dockerignore almost certainly excludes .env, which is correct. Inject the values at runtime.
  • Kubernetes: the Secret exists but is not referenced by envFrom or env in the pod spec.
  • PaaS (Render, Railway, Fly, Heroku): the variable was set in one environment and not the other.

4. The connection string overrides your individual fields

If DATABASE_URL is set, and your config uses connectionString, it overrides every other connection.connection property. A DATABASE_URL that is malformed or has no password section produces exactly this error while your correctly-set DATABASE_PASSWORD sits there being ignored.

postgresql://user:password@host:5432/dbname

Check for a missing :password, or special characters that need percent-encoding: @ → %40, # → %23, / → %2F, : → %3A.

5. env.int() or another typed helper on the password

password: env.int('DATABASE_PASSWORD'),   // wrong
password: env('DATABASE_PASSWORD'),       // right

A numeric password read through env.int() becomes a Number, and the driver rejects it for exactly the reason the error states. Rare, but it produces the most confusing version of this bug, because the variable is set.

6. An environment-specific config file that shadows the base one

config/env/production/database.ts completely replaces config/database.ts when NODE_ENV=production. If the production variant was written with different variable names, such as DB_PASSWORD instead of DATABASE_PASSWORD, you get an app that works in development and fails on deploy.

Check whether the directory exists:

ls -la config/env/

Making the failure legible next time

Two habits keep this from costing an afternoon.

Fail fast with a clear message. Validate required variables at the top of the config:

export default ({ env }) => {
  const password = env('DATABASE_PASSWORD');

  if (typeof password !== 'string' || password.length === 0) {
    throw new Error(
      'DATABASE_PASSWORD is missing or empty. Set it in .env (local) or in your platform environment (deployed).'
    );
  }

  return {
    connection: {
      client: 'postgres',
      connection: {
        host: env('DATABASE_HOST', '127.0.0.1'),
        port: env.int('DATABASE_PORT', 5432),
        database: env('DATABASE_NAME', 'strapi'),
        user: env('DATABASE_USERNAME', 'strapi'),
        password,
        ssl: env.bool('DATABASE_SSL', false),
      },
    },
  };
};

Keep a committed .env.example listing every key with placeholder values, so a new environment has a checklist rather than an archaeology exercise.

ErrorMeaning
client password must be a stringPassword is undefined or not a string
password authentication failed for user "x"Password is present but wrong
role "x" does not existUsername is wrong or the role was never created
database "x" does not existDatabase name is wrong or was never created
ECONNREFUSEDNothing is listening on that host and port
Knex: Timeout acquiring a connectionUsually a firewall dropping packets; see the pool timeout guide
Cannot destructure property 'client' of 'db.config.connection'The database config file itself did not load

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

The True TCO of Self-Hosting a Headless CMS
Ecosystem·12 min read

The True TCO of Self-Hosting a Headless CMS

Discover the real cost of self-hosting Strapi 5: infrastructure, DevOps labor, security, migrations, and hidden costs—with a practical TCO checklist.

·September 24, 2026
How Do You Preserve SEO Rankings During a CMS Migration?
Ecosystem·13 min read

How Do You Preserve SEO Rankings During a CMS Migration?

A developer's CMS migration SEO checklist: URL mapping, 301 redirects, on-page parity, robots.txt, redirect testing, and post-launch monitoring.

·September 24, 2026
What Is a Content Agent? Agentic AI for CMS Workflows Explained
Ecosystem·24 min read

What Is a Content Agent? Agentic AI for CMS Workflows Explained

Learn what a content agent is, how the observe-plan-act-evaluate loop works, and when agentic AI makes sense for your CMS workflows and content operations.

·September 24, 2026