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-11 • 9 min read
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()andencodeURIComponent() - 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:
| Character | Encoded 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(), notencodeURI().
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
.envfiles- 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.