SuitsLearn LogoSuitsLearn

PostgreSQL

PostgreSQL Connection Errors with Special Characters in Passwords: URL Encoding vs Separate Credentials

Learn why PostgreSQL connection strings can fail when passwords contain special characters, how to safely URL-encode credentials, and when to use separate database connection parameters with Node.js, pg, and Drizzle ORM.

By Ishan Wickremasuriya

Published 2026-08-119 min read

AlmaLinuxConnection StringDatabaseDrizzle ORMNext.jsNode.jsPostgreSQLTypeScriptURL EncodingencodeURIComponent

Recently, while deploying a Next.js application using Drizzle ORM and PostgreSQL to my AlmaLinux production server, I ran into a database connection problem that took quite a bit of troubleshooting to identify.

The application worked perfectly on my Windows development machine. However, after deployment, the application failed to establish a connection to PostgreSQL.

After testing different configurations, I eventually identified the problem: the PostgreSQL password contained special characters such as @, /, +, and = and I was using that password directly inside a database connection URL.

Once I URL-encoded the password, the connection worked.

I also tested another approach: instead of using a single connection URL, I provided the database host, port, username, password, and database name as separate configuration values. That worked as well, without URL-encoding the password.

This led me to investigate why these two approaches behave differently.

In this article, we'll look at both approaches and understand:

  • Why special characters can cause problems in database connection URLs
  • Why a password containing special characters is not inherently invalid
  • How to safely URL-encode a password
  • Why encodeURIComponent() is appropriate for this situation
  • The difference between encodeURI() and encodeURIComponent()
  • How to configure PostgreSQL using separate connection parameters
  • Why URL encoding is not required when the password is provided separately
  • Which approach is appropriate for a Next.js + Drizzle + PostgreSQL application
  • Important security considerations for both approaches

Why can special characters cause problems?

A PostgreSQL connection URL commonly looks like this:

postgresql://user:password@host:port/database_name

For example:

postgresql://postgres:pass123@localhost:5432/database_name

This works because the values are straightforward and don't introduce ambiguity into the URL structure.

However, consider a password such as:

G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM=

The password itself is perfectly valid.

The problem is that we're putting this value inside a URI, where some characters have special syntactic meanings.

For example:

  • @ separates the user-information portion from the host
  • / is a reserved URI character
  • + has special significance in some URL/query-string contexts
  • = is commonly used as a delimiter in URL-related formats

So if we construct the connection string like this:

DATABASE_URL=postgresql://user:G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM=@localhost:5432/database_name

the URI parser has to determine which @ belongs to the password and which @ marks the end of the credentials.

The parser cannot simply assume that every character between : and the next @ is arbitrary password data.

The important distinction

This does not mean that PostgreSQL passwords cannot contain these characters.

They can.

The problem is specifically related to representing those characters inside a connection URI.

In other words:

The password is valid. The problem is how that password is represented inside the URI.

This distinction is important because the same password can work perfectly when passed to the PostgreSQL client as a separate password configuration value.


Approach 1: Use a PostgreSQL Connection URL

The first approach is to use a single DATABASE_URL environment variable.

For example:

DATABASE_URL=postgresql://user:password@localhost:5432/database_name

This is a very common approach and is supported by many database tools and frameworks.

With Drizzle and the Node.js PostgreSQL driver (pg), the connection can be configured like this:

import 'dotenv/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const db = drizzle(pool);

The problem occurs when the password contains characters that need to be represented differently inside the URI.

URL-encode the password

The solution is to encode the password before placing it inside the connection URL.

For example:

const password = 'G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM=';

const encodedPassword = encodeURIComponent(password);

console.log(encodedPassword);

The result is:

G9trN%2FOM%2B67YgiOUTm2cb%40b67JDkhH760HwLeM%3D

Some of the important conversions are:

CharacterEncoded value
/%2F
+%2B
@%40
=%3D

We can then construct the connection URL using the encoded password:

DATABASE_URL=postgresql://user:G9trN%2FOM%2B67YgiOUTm2cb%40b67JDkhH760HwLeM%3D@localhost:5432/database_name

Notice that the @ separating the credentials from the hostname remains unchanged:

...%40...%3D@localhost...
           ^
           URI delimiter

The @ inside the password has become %40, so the URI parser can distinguish the password from the host correctly.


Using the Encoded URL with Drizzle

For my Next.js application, I use a small environment configuration module:

/**
 * @file src/config/env/db-env.ts
 */

function optional(name: string, defaultValue: string): string {
  return process.env[name] ?? defaultValue;
}

const defaultConnectionString =
  'postgresql://user:encoded-password@localhost:5432/database_name';

export const dbEnv = {
  connectionString: optional(
    'DATABASE_URL',
    defaultConnectionString
  ),
};

Then the PostgreSQL pool can consume the connection string:

/**
 * @file src/services/init/drizzle.ts
 */

import 'dotenv/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';

import { dbEnv as env } from '@/config/env';

import * as tables from '@/persistent/schema';
import * as relations from '@/persistent/relations';

export const schema = {
  ...tables,
  ...relations,
};

const globalForDb = globalThis as unknown as {
  pool: Pool | undefined;
};

const pool =
  globalForDb.pool ??
  new Pool({
    connectionString: env.connectionString,
    max: 20,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000,
  });

if (process.env.NODE_ENV !== 'production') {
  globalForDb.pool = pool;
}

export const db = drizzle(pool, { schema });

type DbInstance = typeof db;

export type Transaction =
  Parameters<Parameters<DbInstance['transaction']>[0]>[0];

export type DB = DbInstance | Transaction;

With this approach, the important part is that the password is already correctly encoded in DATABASE_URL.


encodeURI() vs encodeURIComponent()

This is an important distinction.

JavaScript provides both:

encodeURI()

and:

encodeURIComponent()

They are designed for different purposes.

encodeURI()

encodeURI() is intended for encoding an entire URI while preserving characters that are meaningful to the URI structure.

For example:

encodeURI('https://example.com/user?id=123')

It does not encode every reserved URI character because those characters may be required to preserve the structure of the URI.

Therefore, it is not the appropriate function for encoding a password that will be inserted into a URI.

encodeURIComponent()

encodeURIComponent() is intended for encoding an individual URI component.

A password is exactly that in this situation.

Therefore:

const encodedPassword = encodeURIComponent(password);

is the appropriate choice.

A simple rule to remember is:

Encode the password with encodeURIComponent(), not encodeURI().


Approach 2: Provide Database Credentials Separately

There is another way to configure the PostgreSQL connection.

Instead of constructing one connection URL, we can provide each connection parameter separately.

For example:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=database_user
DATABASE_PASSWORD='G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM='
DATABASE_NAME=database_name

The configuration can then expose these values individually:

/**
 * @file src/config/env/db-env.ts
 */

function optional(name: string, defaultValue: string): string {
  return process.env[name] ?? defaultValue;
}

export const dbEnv = {
  host: optional('DATABASE_HOST', 'localhost'),
  port: optional('DATABASE_PORT', '5432'),
  user: optional('DATABASE_USER', 'database_user'),
  password: optional(
    'DATABASE_PASSWORD',
    'strong-password-with-special-characters'
  ),
  databaseName: optional('DATABASE_NAME', 'database_name'),
};

The PostgreSQL pool can then be configured like this:

/**
 * @file src/services/init/drizzle.ts
 */

import 'dotenv/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';

import { dbEnv as env } from '@/config/env';

import * as tables from '@/persistent/schema';
import * as relations from '@/persistent/relations';

export const schema = {
  ...tables,
  ...relations,
};

const globalForDb = globalThis as unknown as {
  pool: Pool | undefined;
};

const pool =
  globalForDb.pool ??
  new Pool({
    host: env.host,
    port: Number(env.port),
    user: env.user,
    password: env.password,
    database: env.databaseName,
    max: 20,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000,
    ssl: false,
  });

if (process.env.NODE_ENV !== 'production') {
  globalForDb.pool = pool;
}

export const db = drizzle(pool, { schema });

type DbInstance = typeof db;

export type Transaction =
  Parameters<Parameters<DbInstance['transaction']>[0]>[0];

export type DB = DbInstance | Transaction;

Notice the important difference:

password: env.password

There is no:

encodeURIComponent(env.password)

This is intentional.

The password is being passed to pg as a separate configuration value, rather than being embedded inside a connection URI.

Therefore, URL encoding is not necessary.


Why does the second approach work without encoding?

This is the key difference between the two approaches.

With the connection URL approach, the password is part of a URI:

postgresql://user:password@host:port/database

The URI parser has to interpret the complete string and determine the boundaries between its components.

With the second approach, the PostgreSQL driver receives something conceptually closer to:

{
  host: 'localhost',
  port: 5432,
  user: 'database_user',
  password: 'G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM=',
  database: 'database_name'
}

The password is already a discrete value.

There is no need for a URI parser to determine where the password starts or ends.

Therefore, characters such as:

@ / + =

remain ordinary password characters.


Is One Approach More Secure?

Not inherently.

This is an important point because it would be misleading to claim that separate credentials are automatically more secure than a connection URL.

Both approaches can be secure when implemented correctly.

For example, you should not commit either of these to source control:

DATABASE_URL=postgresql://user:password@host:5432/database

or:

DATABASE_PASSWORD=password

Instead, credentials should be provided through appropriate environment configuration or a dedicated secrets-management mechanism.

The primary difference between these two approaches is representation and parsing, not fundamental password security.

With a connection URL:

postgresql://user:encoded-password@host:5432/database

the password must be correctly percent-encoded because it is part of a URI.

With individual parameters:

{
  user,
  password,
  host,
  port,
  database
}

the password does not need URI encoding because it is supplied separately.


What About Linux and Bash?

Initially, it may be tempting to conclude that the problem is caused by Linux or Bash because the application worked on Windows and failed after deployment to AlmaLinux.

That isn't quite accurate.

The fundamental issue in this particular case is URI parsing.

However, production environments can introduce additional layers where special characters have their own meanings. For example, values may pass through:

  • shell commands
  • deployment scripts
  • .env files
  • systemd configuration
  • CI/CD variables
  • secret managers
  • application configuration
  • database connection-string parsers

Each layer can potentially have its own parsing or escaping rules.

Therefore, moving an application from local development to a Linux production environment can expose configuration problems that weren't obvious during local development.

But in this particular PostgreSQL connection issue, the core problem was the representation of the password inside the connection URI.


Which Approach Should You Use?

Both approaches are valid.

Use a connection URL when

A single connection string is convenient for your environment or tooling:

DATABASE_URL=postgresql://user:password@host:5432/database

This approach is common and works well.

If the credentials contain characters that have special meaning in a URI, make sure the relevant URI components are properly percent-encoded.

For a password:

encodeURIComponent(password)

is the appropriate JavaScript function.

Use separate connection parameters when

You prefer to keep the database configuration as independent values:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=database_user
DATABASE_PASSWORD=your-password
DATABASE_NAME=database_name

and pass them directly to pg:

new Pool({
  host,
  port,
  user,
  password,
  database,
});

This avoids having to construct and parse a connection URI and therefore avoids URI-specific encoding requirements for the password.

Neither approach is universally better. The appropriate choice depends on your application's configuration conventions, deployment environment, and how your infrastructure manages database credentials.


A Note About .env Quotes

You may sometimes see passwords written like this:

DATABASE_PASSWORD='G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM='

The quotes are related to how the environment-file parser interprets the value; they are not PostgreSQL password escaping and should not be confused with URL encoding.

For example, don't take the following approach:

DATABASE_PASSWORD='%40myPassword%3D'

when using the separate-parameter approach merely because the password contains special characters.

If the actual password is:

@myPassword=

then the application should receive:

@myPassword=

as the password.

URL encoding is required when representing that value as a component of a URI, not when passing it as a standalone password value.


My Production Troubleshooting Experience

This issue was particularly interesting because everything worked during local development.

The application was running correctly on my Windows machine with Next.js, Drizzle ORM, and PostgreSQL.

The same application was then deployed to an AlmaLinux server, where the database connection failed.

After testing the PostgreSQL server, credentials, permissions, network configuration, and application configuration, I eventually isolated the problem to the connection string.

The original password contained several special characters:

G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM=

When that value was inserted directly into the connection URL, the URI became ambiguous.

After encoding the password:

G9trN%2FOM%2B67YgiOUTm2cb%40b67JDkhH760HwLeM%3D

the connection succeeded.

I then tested the alternative configuration using separate connection parameters:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=database_user
DATABASE_PASSWORD='G9trN/OM+67YgiOUTm2cb@b67JDkhH760HwLeM='
DATABASE_NAME=database_name

That also worked without URL-encoding the password.

This was a good reminder that a database connection string is not simply a collection of arbitrary text values. It is a structured URI, and the values placed inside it must follow URI encoding rules.


Takeaways

The main lessons from this issue are:

1. Special characters are valid in PostgreSQL passwords

Characters such as:

@ / + =

do not automatically make a PostgreSQL password invalid.

The problem occurs when those characters are placed inside a structured connection URI.

2. Connection URLs require proper URI encoding

If you put a password inside:

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

make sure the password is properly encoded when necessary.

For JavaScript and TypeScript:

encodeURIComponent(password)

is the appropriate function for encoding the password component.

3. Don't use encodeURI() for the password

encodeURI() is intended for encoding a complete URI while preserving its structural characters.

encodeURIComponent() is intended for an individual URI component.

For a password inside a connection URL, use:

encodeURIComponent(password)

4. Separate connection parameters don't require URL encoding

When using:

new Pool({
  host,
  port,
  user,
  password,
  database,
});

the password is supplied separately.

It is not being parsed as part of the connection URI, so URI percent-encoding isn't required.

5. The problem isn't fundamentally a Linux problem

The difference between Windows development and Linux production can make configuration problems more visible, but the fundamental issue here is URI parsing.

Additional layers such as shells, deployment scripts, .env files, systemd, CI/CD systems, and secret managers can introduce their own escaping rules, so production configuration should always be tested carefully.

6. Neither approach is inherently more secure

Both connection URLs and separate connection parameters can be used securely.

The important things are:

  • Don't commit database credentials to source control.
  • Use environment variables or a proper secrets-management system.
  • Protect production environment configuration.
  • Avoid logging database passwords or complete connection strings.
  • Be aware of how your deployment platform handles environment variables and secrets.

Final thought

A problem like this can be surprisingly difficult to diagnose because the database itself may be perfectly healthy and the credentials may be perfectly valid.

Sometimes the problem is not the database password—it is how the password is represented while travelling through the configuration and connection layers.

Understanding the difference between a password as raw data and a password embedded inside a URI makes these problems much easier to identify and troubleshoot.