MinusNow Documentation

1. Overview & Architecture

In a two-node deployment, the application and database run on separate machines. This provides:

Architecture Diagram

┌──────────────────────────────────────────────────────────────────────────────────┐ Two-Node MinusNow Deployment ├──────────────────────────────────────────────────────────────────────────────────┤ ┌─────────────────────────┐ ┌─────────────────────────┐ │ NODE 1: App Server │ │ NODE 2: DB Server │ │ ───────────────────── │ │ ───────────────────── │ │ IP: 192.168.1.10 │ │ IP: 192.168.1.20 │ │ │ │ │ │ Node.js 20 LTS │ TCP 5432 │ PostgreSQL 15+ │ │ Express.js 4.x │──────────▶│ (Primary) │ │ React 19 + Vite 7 │ │ │ │ MinusNow App │ │ Databases: │ │ Port: 5000 │ │ - minusnow │ │ │ │ │ │ Monitoring Agent │ │ Monitoring Agent │ └─────────────────────────┘ └────────────┬────────────┘ WAL Stream ┌─────────────────────────┐ │ REPLICA (Optional) │ │ ───────────────────── │ │ IP: 192.168.1.30 │ │ PostgreSQL 15+ (Read) │ │ Streaming Replication │ └─────────────────────────┘ Users ──▶ Node 1 :5000 (HTTP) ──▶ Node 2 :5432 (PostgreSQL) └──────────────────────────────────────────────────────────────────────────────────┘

Node Roles Summary

Node 1

Application Server

  • Node.js 20 LTS runtime
  • MinusNow Express.js app
  • React 19 frontend (served by Express)
  • Listens on port 5000
  • Connects to remote PostgreSQL
  • No local database needed
Node 2

Primary Database

  • PostgreSQL 15+ server
  • Listens on port 5432
  • Accepts connections from Node 1 IP
  • Stores all application data
  • Handles WAL archiving
  • Streams to replica (optional)
Replica

Read Replica (Optional)

  • PostgreSQL 15+ standby
  • Receives WAL stream from primary
  • Read-only queries / reporting
  • Can be promoted to primary
  • Hot standby for failover

2. Prerequisites

Hardware Requirements

RequirementNode 1 (App Server)Node 2 (DB Server)
CPU2+ cores2+ cores (4 recommended)
RAM4 GB minimum (8 recommended)4 GB minimum (8+ recommended)
Disk20 GB (SSD recommended)50 GB+ SSD (for data + WAL)
NetworkGigabit EthernetGigabit Ethernet
OSWindows Server 2019+ / Ubuntu 20.04+ / RHEL 8+Same OS options

Software Requirements

Node 1 — App Server

Required Software

  • Node.js 20 LTS (includes npm)
  • Git (to clone source code)
  • MinusNow source code or artifact ZIP
  • psql client (to test DB connection — optional)
ℹ️

Node 1 does NOT need PostgreSQL installed. The app connects to Node 2's database over the network. You only need psql if you want to test the connection manually.

Node 2 — DB Server

Required Software

  • PostgreSQL 15+ (15, 16, 17, or 18)
  • pg_hba.conf editing access (admin)
  • postgresql.conf editing access (admin)
ℹ️

Node 2 does NOT need Node.js. This machine only runs PostgreSQL. It doesn't need any JavaScript tooling.

Network Requirements

3. Network Planning

Before starting installation, plan your IP addresses and hostnames. Use this table as a template:

RoleHostnameIP AddressOSPorts
Node 1 App Serverminusnow-app192.168.1.10Your choice5000 (HTTP)
Node 2 DB Primaryminusnow-db192.168.1.20Your choice5432 (PostgreSQL)
Replica DB Standbyminusnow-db-replica192.168.1.30Your choice5432 (PostgreSQL)
⚠️

Replace the example IPs (192.168.1.10, 192.168.1.20, 192.168.1.30) with your actual network IPs throughout this guide. These IPs are used as placeholders.

Step 3a: Verify Network Connectivity

From Node 1, verify you can reach Node 2:

🖥️ Windows (Node 1)
PowerShell# Ping the DB server
ping 192.168.1.20

# Test TCP port 5432 connectivity (after PostgreSQL is configured)
Test-NetConnection -ComputerName 192.168.1.20 -Port 5432
🐧 Linux (Node 1)
Bash# Ping the DB server
ping -c 4 192.168.1.20

# Test TCP port 5432 connectivity (after PostgreSQL is configured)
nc -zv 192.168.1.20 5432

Step 3b: Set Up Hostname Resolution (Optional but Recommended)

Adding hostnames to /etc/hosts (Linux) or C:\Windows\System32\drivers\etc\hosts (Windows) makes configuration cleaner:

🖥️ Windows (both nodes)

Open Notepad as Administrator → File → Open → C:\Windows\System32\drivers\etc\hosts

hosts# MinusNow Two-Node Deployment
192.168.1.10    minusnow-app
192.168.1.20    minusnow-db
192.168.1.30    minusnow-db-replica
🐧 Linux (both nodes)
Bash# Add to /etc/hosts on BOTH nodes
sudo tee -a /etc/hosts <<EOF
# MinusNow Two-Node Deployment
192.168.1.10    minusnow-app
192.168.1.20    minusnow-db
192.168.1.30    minusnow-db-replica
EOF

4. Node 2: Primary Database Setup

ℹ️

Why Node 2 first? We set up the database server before the application server because Node 1 needs a running database to connect to during setup.

Step 4a: Install PostgreSQL

🖥️ Windows (Node 2)
1

Download PostgreSQL Installer

Go to postgresql.org/download/windows and download PostgreSQL 15, 16, 17, or 18.

2

Run the Installer

  • Set a strong password for the postgres superuser (write it down)
  • Keep the default port 5432
  • Accept default data directory (e.g., C:\Program Files\PostgreSQL\18\data)
3

Add PostgreSQL to PATH

The installer often doesn't add the bin folder to PATH. Fix this:

PowerShell (Admin)# Adjust the version number to match your installation (15, 16, 17, or 18)
$pgBin = "C:\Program Files\PostgreSQL\18\bin"

# Add to system PATH permanently
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
if ($currentPath -notlike "*$pgBin*") {
    [Environment]::SetEnvironmentVariable("Path", "$currentPath;$pgBin", "Machine")
    Write-Host "PostgreSQL bin added to PATH. Close and reopen PowerShell."
} else {
    Write-Host "PostgreSQL bin already in PATH."
}

# Verify (in a NEW PowerShell window)
psql --version
4

Verify PostgreSQL is Running

PowerShell# Check service status
Get-Service -Name "postgresql*"

# Should show: Running
🐧 Linux (Node 2)

Ubuntu / Debian

Bash# Install PostgreSQL
sudo apt update
sudo apt install -y postgresql postgresql-contrib

# Start and enable
sudo systemctl start postgresql
sudo systemctl enable postgresql

# Verify
sudo systemctl status postgresql
psql --version

RHEL / CentOS / Rocky Linux

Bash# Install PostgreSQL 15 module
sudo dnf module enable postgresql:15
sudo dnf install -y postgresql-server postgresql-contrib

# Initialize and start
sudo postgresql-setup --initdb
sudo systemctl start postgresql
sudo systemctl enable postgresql

# Verify
sudo systemctl status postgresql

Step 4b: Create the MinusNow Database and User

🖥️ Windows (Node 2)
PowerShell# Connect as superuser
psql -U postgres

# Inside psql, run these commands:
CREATE DATABASE minusnow;
CREATE USER minusnow_user WITH ENCRYPTED PASSWORD 'YourStrongPassword123!';
GRANT ALL PRIVILEGES ON DATABASE minusnow TO minusnow_user;
ALTER DATABASE minusnow OWNER TO minusnow_user;

-- PostgreSQL 15+ requires explicit schema permissions
\c minusnow
GRANT ALL ON SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO minusnow_user;
\q
🔴

Replace YourStrongPassword123! with a real strong password. This password will be used in Node 1's DATABASE_URL environment variable. Use a mix of uppercase, lowercase, numbers, and symbols. Avoid @, #, / characters as they can break the connection URL.

🐧 Linux (Node 2)
Bash# Switch to postgres user and create database
sudo -u postgres psql <<SQL
CREATE DATABASE minusnow;
CREATE USER minusnow_user WITH ENCRYPTED PASSWORD 'YourStrongPassword123!';
GRANT ALL PRIVILEGES ON DATABASE minusnow TO minusnow_user;
ALTER DATABASE minusnow OWNER TO minusnow_user;
SQL

# Grant schema permissions (required for PostgreSQL 15+)
sudo -u postgres psql -d minusnow <<SQL
GRANT ALL ON SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO minusnow_user;
SQL

Step 4c: Configure PostgreSQL to Accept Remote Connections

⚠️

This is the critical step. By default, PostgreSQL only listens on localhost (127.0.0.1). For Node 1 to connect over the network, you must configure PostgreSQL to listen on all interfaces (or the specific IP) and allow the connection in pg_hba.conf.

Step 4c-i: Edit postgresql.conf

Find and edit the postgresql.conf file:

OSTypical Location
WindowsC:\Program Files\PostgreSQL\18\data\postgresql.conf
Ubuntu/Debian/etc/postgresql/15/main/postgresql.conf
RHEL/CentOS/var/lib/pgsql/data/postgresql.conf

Find the listen_addresses line and change it:

postgresql.conf# BEFORE (default — only localhost)
#listen_addresses = 'localhost'

# AFTER — listen on all interfaces
listen_addresses = '*'

# OR — listen on specific IP only (more secure)
listen_addresses = '192.168.1.20'
ℹ️

Explanation:

  • '*' = listen on ALL network interfaces (easiest but less secure)
  • '192.168.1.20' = listen ONLY on this specific IP (more secure)
  • 'localhost,192.168.1.20' = listen on localhost AND the specific IP (recommended)

Step 4c-ii: Edit pg_hba.conf

Find and edit the pg_hba.conf file (same directory as postgresql.conf):

OSTypical Location
WindowsC:\Program Files\PostgreSQL\18\data\pg_hba.conf
Ubuntu/Debian/etc/postgresql/15/main/pg_hba.conf
RHEL/CentOS/var/lib/pgsql/data/pg_hba.conf

Add these lines at the end of the file:

pg_hba.conf# Allow Node 1 (App Server) to connect to MinusNow database
# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    minusnow        minusnow_user   192.168.1.10/32         scram-sha-256

# If you have a replica, also allow it:
host    replication     replicator      192.168.1.30/32         scram-sha-256
⚠️

Understanding pg_hba.conf entries:

FieldValueMeaning
hostTCP/IP connection (not Unix socket)
minusnowDatabase nameOnly allow connection to this database
minusnow_userUsernameOnly this user can connect
192.168.1.10/32CIDR address/32 = exact IP match (most secure)
scram-sha-256Auth methodPassword-based authentication (encrypted)

Do NOT use trust as the method — that allows connections without a password!

Step 4c-iii: Restart PostgreSQL

🖥️ Windows (Node 2)
PowerShell (Admin)# Restart PostgreSQL service
Restart-Service -Name "postgresql*"

# Verify it's running
Get-Service -Name "postgresql*"
🐧 Linux (Node 2)
Bash# Restart PostgreSQL
sudo systemctl restart postgresql

# Verify it's running
sudo systemctl status postgresql

# Check it's listening on the right address
sudo ss -tlnp | grep 5432
# Should show: 0.0.0.0:5432 or 192.168.1.20:5432

5. Node 2: Firewall Configuration

The database server's firewall must allow incoming connections on port 5432 from the application server. Only allow the specific Node 1 IP — never open port 5432 to the entire network.

🖥️ Windows (Node 2)
PowerShell (Admin)# Allow PostgreSQL port from Node 1 only
New-NetFirewallRule `
  -DisplayName "MinusNow - PostgreSQL from App Server" `
  -Direction Inbound `
  -Protocol TCP `
  -LocalPort 5432 `
  -RemoteAddress 192.168.1.10 `
  -Action Allow `
  -Profile Domain,Private

# Verify the rule
Get-NetFirewallRule -DisplayName "MinusNow*" | Format-Table Name, DisplayName, Enabled
ℹ️

The -RemoteAddress flag restricts this rule to only allow connections from Node 1's IP address. This is much more secure than allowing all IPs.

🐧 Linux (Node 2)

Using UFW (Ubuntu/Debian)

Bash# Allow PostgreSQL from Node 1 ONLY
sudo ufw allow from 192.168.1.10 to any port 5432 proto tcp comment "MinusNow App Server"

# If using a replica, also allow it:
sudo ufw allow from 192.168.1.30 to any port 5432 proto tcp comment "MinusNow DB Replica"

# Verify
sudo ufw status numbered

Using firewalld (RHEL/CentOS)

Bash# Create a rich rule for Node 1 only
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4"
  source address="192.168.1.10/32"
  port protocol="tcp" port="5432"
  accept'

# For replica:
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4"
  source address="192.168.1.30/32"
  port protocol="tcp" port="5432"
  accept'

sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rules

Step 5b: Test the Firewall from Node 1

After configuring the firewall, go to Node 1 and test connectivity:

🖥️ Windows (Node 1)
PowerShell# Test TCP connectivity to PostgreSQL port
Test-NetConnection -ComputerName 192.168.1.20 -Port 5432

# Expected output:
# TcpTestSucceeded : True
🐧 Linux (Node 1)
Bash# Test TCP connectivity
nc -zv 192.168.1.20 5432

# Expected: Connection to 192.168.1.20 5432 port [tcp/postgresql] succeeded!

6. Node 1: Application Server Setup

Step 6a: Install Node.js 20 LTS

🖥️ Windows (Node 1)
1

Download Node.js

Go to nodejs.org and download Node.js 20 LTS (Windows Installer .msi).

2

Verify Installation

PowerShellnode --version    # Should show v20.x.x
npm --version     # Should show 10.x.x
🐧 Linux (Node 1)
Bash# Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# RHEL/CentOS
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs

# Verify
node --version    # Should show v20.x.x
npm --version     # Should show 10.x.x

Step 6b: Deploy MinusNow Application Code

🖥️ Windows (Node 1)

Option A: From source code ZIP (downloaded from Download Center)

PowerShell# Create project directory
mkdir D:\MinusNow
cd D:\MinusNow

# Extract the source code ZIP
Expand-Archive -Path ".\minusnow-itsm-source.zip" -DestinationPath "." -Force

Option B: From Git repository

PowerShellcd D:\
git clone <your-repo-url> MinusNow
cd MinusNow
🐧 Linux (Node 1)

Option A: From source code archive

Bash# Create project directory
sudo mkdir -p /opt/minusnow
sudo chown $USER:$USER /opt/minusnow
cd /opt/minusnow

# Extract (if .zip)
unzip minusnow-itsm-source.zip -d .

Option B: From Git

Bashcd /opt
sudo git clone <your-repo-url> minusnow
sudo chown -R $USER:$USER /opt/minusnow
cd /opt/minusnow

Step 6c: Install Dependencies

Both platforms# Navigate to project directory
cd D:\MinusNow          # Windows
cd /opt/minusnow        # Linux

# Clean install (removes any stale global packages)
npm install
⚠️

Global drizzle-kit conflict: If you have an old version of drizzle-kit installed globally, it will override the local one and cause npm run db:push to fail with "unknown command 'push'". Fix:

PowerShell / Bash# Check for global drizzle-kit
npm list -g drizzle-kit

# If found, remove it
npm uninstall -g drizzle-kit

# Use local version (installed via npm install)
npx drizzle-kit --version    # Should show 0.31.x

7. Node 1: Connect App to Remote Database

🔴

This is the most important step. The DATABASE_URL environment variable tells the MinusNow app where to find the PostgreSQL database. In a two-node setup, this URL points to Node 2's IP address instead of localhost.

Step 7a: Understand the DATABASE_URL Format

The connection URL follows this format:

Formatpostgresql://USERNAME:PASSWORD@HOST:PORT/DATABASE
                │        │       │     │     │
                │        │       │     │     └─ Database name: minusnow
                │        │       │     └─────── Port: 5432 (default)
                │        │       └───────────── Node 2's IP: 192.168.1.20
                │        └───────────────────── Password you created in Step 4b
                └────────────────────────────── Username: minusnow_user

Single-Node vs Two-Node Comparison

DeploymentDATABASE_URL
Single-Node (same machine)postgresql://minusnow_user:Pass@localhost:5432/minusnow
Two-Node (remote DB)postgresql://minusnow_user:Pass@192.168.1.20:5432/minusnow

Step 7b: Create the .env File on Node 1

🖥️ Windows (Node 1)
PowerShellcd D:\MinusNow

# Generate a random SESSION_SECRET
$secret = -join ((65..90)+(97..122)+(48..57) | Get-Random -Count 48 | ForEach-Object {[char]$_})

# Create .env file — NOTE: Host is Node 2's IP, NOT localhost!
@"
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow
SESSION_SECRET=$secret
PORT=5000
NODE_ENV=development
"@ | Out-File -Encoding utf8 .env

# Verify the file
Get-Content .env
🐧 Linux (Node 1)
Bashcd /opt/minusnow

# Generate a random SESSION_SECRET
SECRET=$(openssl rand -base64 36)

# Create .env file — NOTE: Host is Node 2's IP, NOT localhost!
cat > .env <<EOF
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow
SESSION_SECRET=$SECRET
PORT=5000
NODE_ENV=development
EOF

# Secure the file
chmod 600 .env

# Verify
cat .env
⚠️

Common Mistakes in DATABASE_URL:

  • Using localhost — there is no database on Node 1!
  • Wrong password — copy exactly from Step 4b
  • Forgetting the port :5432
  • Using @ or # characters in the password (breaks URL parsing)
  • postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow

Step 7c: Test Database Connection from Node 1

Before pushing the schema, verify the connection works:

🖥️ Windows (Node 1)
PowerShell# If you have psql installed on Node 1 (optional):
psql -h 192.168.1.20 -U minusnow_user -d minusnow -c "SELECT version();"

# If psql is NOT installed, use Node.js to test:
node -e "
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow' });
pool.query('SELECT NOW() as time, version() as ver')
  .then(r => { console.log('Connected!', r.rows[0]); pool.end(); })
  .catch(e => { console.error('FAILED:', e.message); pool.end(); });
"
🐧 Linux (Node 1)
Bash# Test with psql (if available):
PGPASSWORD='YourStrongPassword123!' psql -h 192.168.1.20 -U minusnow_user -d minusnow -c "SELECT version();"

# Or test with Node.js:
node -e "
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.query('SELECT NOW() as time')
  .then(r => { console.log('Connected!', r.rows[0]); pool.end(); })
  .catch(e => { console.error('FAILED:', e.message); pool.end(); });
"

If the connection fails, check these items in order:

  1. PostgreSQL is running on Node 2 (Get-Service postgresql* / systemctl status postgresql)
  2. listen_addresses in postgresql.conf is set to '*' or includes Node 2's IP
  3. pg_hba.conf has an entry allowing Node 1's IP
  4. Firewall on Node 2 allows port 5432 from Node 1
  5. Password is correct and doesn't contain URL-breaking characters

Step 7d: Push Database Schema

Once the connection is verified, push the MinusNow schema to the remote database:

Both platforms# Push schema to remote PostgreSQL on Node 2
npm run db:push

If successful, you'll see drizzle-kit output showing the tables being created on Node 2's database. The schema is identical whether the database is local or remote.

Step 7e: Build and Start the Application

Both platforms# Build for production
npm run build

# Start the application
npm run start

# Or for development mode:
npm run dev

The application should now be running on http://NODE1_IP:5000 and connecting to PostgreSQL on Node 2.

8. Verify End-to-End Connectivity

After starting the application, verify that everything is working across both nodes:

Step 8a: Check Application Health

From any machine# Health check endpoint
curl http://192.168.1.10:5000/api/health

# Expected response:
# {"status":"healthy","timestamp":"2026-02-13T...","uptime":...}

Step 8b: Verify Database Connectivity from App

From Node 1# Check that the app can query the database
curl http://localhost:5000/api/health

# The health check should return "healthy" — this confirms:
# 1. Node.js is running on Node 1
# 2. Express is serving on port 5000
# 3. PostgreSQL connection to Node 2 is working
# 4. Database schema is properly set up

Step 8c: Open in Browser

Navigate to http://192.168.1.10:5000 (or http://minusnow-app:5000 if you set up hostnames) in your browser. You should see the MinusNow login page.

Step 8d: Verify from Node 2 (Database Server)

On Node 2, verify the application has connected and created tables:

Node 2# Connect to the database
# Windows: psql -U minusnow_user -d minusnow
# Linux:   sudo -u postgres psql -d minusnow

# List all tables created by MinusNow
\dt

# Check active connections from Node 1
SELECT client_addr, state, query
FROM pg_stat_activity
WHERE datname = 'minusnow' AND client_addr IS NOT NULL;

Success indicators:

  • \dt shows MinusNow tables (users, tickets, incidents, etc.)
  • pg_stat_activity shows connections from 192.168.1.10 (Node 1)
  • Browser shows the MinusNow login page
  • Health endpoint returns "healthy"

9. Replica Database Setup (Optional)

ℹ️

When do you need a replica?

  • High availability — automatic failover if the primary goes down
  • Read scaling — offload read-heavy reporting queries to the replica
  • Disaster recovery — an up-to-date copy on a separate machine/location
  • Zero-downtime upgrades — promote replica, upgrade old primary, re-add as replica

If you don't need these features, skip to Section 12: Monitoring.

Step 9a: Prepare the Replica Machine

On the replica machine (IP: 192.168.1.30), install PostgreSQL — same version as the primary:

🖥️ Windows (Replica)

Install PostgreSQL using the same installer as Node 2. Stop the PostgreSQL service after installation — we'll configure it as a standby before starting.

PowerShell (Admin)# Stop PostgreSQL (we need to replace its data directory)
Stop-Service -Name "postgresql*"
🐧 Linux (Replica)
Bash# Install same PostgreSQL version
sudo apt install -y postgresql postgresql-contrib    # Ubuntu/Debian
# OR
sudo dnf install -y postgresql-server                 # RHEL/CentOS

# Stop PostgreSQL — we'll configure it as standby
sudo systemctl stop postgresql

Step 9b: Create Replication User on Primary (Node 2)

On Node 2 (primary), create a dedicated user for replication:

Node 2 — psql# Connect as superuser
# Windows: psql -U postgres
# Linux:   sudo -u postgres psql

CREATE ROLE replicator WITH REPLICATION LOGIN ENCRYPTED PASSWORD 'ReplicaPassword456!';

# Verify
\du replicator
⚠️

Make sure you already added the pg_hba.conf line for the replica in Section 4c-ii. If not, add it now and restart PostgreSQL on the primary.

10. PostgreSQL Streaming Replication

How Streaming Replication Works

Primary (Node 2) Replica (192.168.1.30) │ │ │ 1. Client writes data │ ▼ │ ┌─────────┐ │ │ WAL │ 2. Write-Ahead Log │ │ Buffer │ records the change │ └────┬─────┘ │ │ │ │ 3. WAL records are streamed ──────▶ │ │ over TCP to the replica │ │ ▼ │ ┌──────────┐ │ │ WAL │ │ │ Receiver │ │ └─────┬─────┘ │ │ │ │ 4. Replica applies │ │ WAL records │ ▼ │ ┌──────────┐ │ │ Data │ │ │ Files │ │ └──────────┘ │ Nearly real-time replication (typically < 1 second lag)

Step 10a: Configure Primary for Replication (Node 2)

Edit postgresql.conf on Node 2:

postgresql.conf (Node 2)# ---- Replication Settings ----
wal_level = replica                  # Required for streaming replication
max_wal_senders = 5                  # Max number of replicas that can connect
wal_keep_size = 1024                 # Keep 1 GB of WAL for replicas (MB)
hot_standby = on                     # Allow read queries on standby

# Optional: Synchronous replication (higher safety, lower performance)
# synchronous_standby_names = 'minusnow_replica'

Restart PostgreSQL on Node 2 after saving:

Node 2# Windows:
Restart-Service -Name "postgresql*"

# Linux:
sudo systemctl restart postgresql

Step 10b: Create Base Backup on Replica

On the replica machine, use pg_basebackup to clone the primary's data:

🖥️ Windows (Replica)
PowerShell (Admin)# Stop PostgreSQL if running
Stop-Service -Name "postgresql*"

# Remove the default data directory contents
$dataDir = "C:\Program Files\PostgreSQL\18\data"
Remove-Item "$dataDir\*" -Recurse -Force

# Create base backup from primary
pg_basebackup -h 192.168.1.20 -U replicator -D "$dataDir" -P -Xs -R

# The -R flag creates standby.signal and sets primary_conninfo automatically!
# Verify standby.signal was created
Test-Path "$dataDir\standby.signal"    # Should be True

# Start PostgreSQL
Start-Service -Name "postgresql*"
🐧 Linux (Replica)
Bash# Stop PostgreSQL
sudo systemctl stop postgresql

# Remove existing data directory contents
# Ubuntu/Debian: data is in /var/lib/postgresql/15/main
# RHEL/CentOS:   data is in /var/lib/pgsql/data
DATA_DIR="/var/lib/postgresql/15/main"   # Adjust for your OS/version
sudo rm -rf $DATA_DIR/*

# Create base backup from primary (as postgres user)
sudo -u postgres pg_basebackup \
  -h 192.168.1.20 \
  -U replicator \
  -D $DATA_DIR \
  -P -Xs -R

# The -R flag automatically creates:
#   - standby.signal (tells PostgreSQL this is a standby)
#   - Adds primary_conninfo to postgresql.auto.conf

# Verify
sudo ls -la $DATA_DIR/standby.signal    # Should exist

# Start PostgreSQL
sudo systemctl start postgresql

# Check replication status
sudo -u postgres psql -c "SELECT * FROM pg_stat_wal_receiver;"
ℹ️

What the -R flag does:

  • Creates standby.signal file in the data directory
  • Adds primary_conninfo = 'host=192.168.1.20 user=replicator password=...' to postgresql.auto.conf
  • This tells PostgreSQL to start in standby mode and stream WAL from the primary

Step 10c: Verify Replication is Working

On the Primary (Node 2) — check that the replica is connected:

Node 2 — psqlSELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;

-- Expected output:
--  client_addr    | state     | sent_lsn    | write_lsn   | ...
-- ----------------+-----------+-------------+-------------+-----
--  192.168.1.30   | streaming | 0/3000D60   | 0/3000D60   | ...

On the Replica — check replication status:

Replica — psqlSELECT status, received_lsn, latest_end_lsn
FROM pg_stat_wal_receiver;

-- Expected: status = 'streaming'

-- Verify it's in recovery mode:
SELECT pg_is_in_recovery();
-- Expected: t (true)

Step 10d: Test Read-Only Queries on Replica

Replica — psql# Connect to the minusnow database on the replica
psql -U minusnow_user -d minusnow -h 192.168.1.30

# Run a read query — should work:
SELECT count(*) FROM users;

# Try a write query — should FAIL (read-only):
INSERT INTO users (name) VALUES ('test');
-- ERROR: cannot execute INSERT in a read-only transaction

Replication is working if:

  • pg_stat_replication on primary shows state = 'streaming'
  • pg_stat_wal_receiver on replica shows status = 'streaming'
  • Read queries work on the replica
  • Write queries are rejected on the replica
  • Data written on the primary appears on the replica within seconds

11. Failover Procedures

If the primary database (Node 2) goes down, you can promote the replica to become the new primary. This is a manual process — use it as an emergency procedure.

Scenario: Primary Database Goes Down

BEFORE (Primary down): Node 1 (App) ──▶ Node 2 (Primary) ✗WAL Stream (broken) │ Replica (still has recent data) AFTER (Failover): Node 1 (App) ──▶ Replica (promoted to Primary) ✓ IP: 192.168.1.30

Step 11a: Promote the Replica

🖥️ Windows (Replica)
PowerShell (Admin)# Promote the replica to primary
# Option 1: Using pg_ctl
& "C:\Program Files\PostgreSQL\18\bin\pg_ctl" promote -D "C:\Program Files\PostgreSQL\18\data"

# Option 2: Using SQL (connect to the replica)
psql -U postgres -c "SELECT pg_promote();"

# Verify it's no longer in recovery:
psql -U postgres -c "SELECT pg_is_in_recovery();"
# Expected: f (false) = it's now a primary!
🐧 Linux (Replica)
Bash# Promote the replica
# Option 1: Using pg_ctl
sudo -u postgres pg_ctlcluster 15 main promote

# Option 2: Using SQL
sudo -u postgres psql -c "SELECT pg_promote();"

# Verify
sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
# Expected: f (false)

Step 11b: Update Node 1's DATABASE_URL

On Node 1 (App Server), update the .env file to point to the promoted replica:

.env on Node 1# BEFORE (pointing to dead primary):
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow

# AFTER (pointing to promoted replica):
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.30:5432/minusnow

Then restart the application:

Node 1# If running as a service:
# Windows: Restart-Service MinusNow
# Linux:   sudo systemctl restart minusnow

# If running manually, stop and restart:
npm run start

Step 11c: Verify Application is Working with New Primary

From any machine# Health check
curl http://192.168.1.10:5000/api/health

# Should return "healthy" with the database now on 192.168.1.30
⚠️

After failover, remember to:

  1. Investigate why the original primary went down
  2. Once fixed, you can reconfigure it as a new replica of the promoted primary
  3. Update your network planning table with the new primary IP
  4. Update firewall rules if needed
  5. Test the complete flow again

12. Monitoring Both Nodes

Install monitoring agents on both nodes to collect system metrics (CPU, memory, disk, uptime) and report them to the MinusNow application.

Step 12a: Install Agent on Node 1 (App Server)

🖥️ Windows (Node 1)
PowerShell (Admin)# Download the agent installer from MinusNow
Invoke-WebRequest -Uri "http://192.168.1.10:5000/documentation/downloads/agent-install-windows.ps1" `
  -OutFile agent-install-windows.ps1

# Run it
.\agent-install-windows.ps1 -Server "http://localhost:5000" -HostName "minusnow-app" -Interval 60
🐧 Linux (Node 1)
Bash# Download the agent
curl -o agent-install-linux.sh http://192.168.1.10:5000/documentation/downloads/agent-install-linux.sh
chmod +x agent-install-linux.sh

# Install
sudo ./agent-install-linux.sh --server "http://localhost:5000" --hostname "minusnow-app" --interval 60

Step 12b: Install Agent on Node 2 (DB Server)

🖥️ Windows (Node 2)
PowerShell (Admin)# Download the agent from Node 1
Invoke-WebRequest -Uri "http://192.168.1.10:5000/documentation/downloads/agent-install-windows.ps1" `
  -OutFile agent-install-windows.ps1

# Run it — NOTE: Server points to Node 1 (where the app runs)
.\agent-install-windows.ps1 -Server "http://192.168.1.10:5000" -HostName "minusnow-db" -Interval 60
🐧 Linux (Node 2)
Bash# Download the agent from Node 1
curl -o agent-install-linux.sh http://192.168.1.10:5000/documentation/downloads/agent-install-linux.sh
chmod +x agent-install-linux.sh

# Install — NOTE: Server points to Node 1
sudo ./agent-install-linux.sh --server "http://192.168.1.10:5000" --hostname "minusnow-db" --interval 60
ℹ️

Node 2's agent needs firewall access to Node 1's port 5000. If Node 2 cannot reach Node 1 on port 5000, add a firewall rule on Node 1:

# Windows (Node 1):
New-NetFirewallRule -DisplayName "MinusNow HTTP" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow

# Linux (Node 1):
sudo ufw allow 5000/tcp comment "MinusNow App"

Step 12c: Monitor Replication Lag

If you have a replica, monitor the replication lag from the primary:

Node 2 (Primary) — psql# Check replication lag in bytes
SELECT
  client_addr,
  state,
  pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes,
  pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS lag_pretty
FROM pg_stat_replication;

-- Healthy output: lag_bytes close to 0, lag_pretty = "0 bytes" or "XX kB"
-- Warning: If lag_pretty shows MB or more, the replica is falling behind

You can create a cron job or scheduled task to alert on high lag:

Bash (Node 2)# Add to crontab: check replication lag every 5 minutes
*/5 * * * * sudo -u postgres psql -t -c "SELECT pg_wal_lsn_diff(sent_lsn, replay_lsn) FROM pg_stat_replication;" | awk '{if($1 > 104857600) print "ALERT: Replication lag > 100MB: " $1 " bytes"}' >> /var/log/minusnow-replication.log 2>&1

13. Backup Strategy (Two-Node)

In a two-node setup, you need to back up both nodes:

Node 1

App Server Backup

Back up the application codebase, .env file, and any uploaded files:

Windows.\backup-windows.ps1 -OutputDir D:\backups
Linux./backup-linux.sh --output-dir /backups

Frequency: Weekly (code doesn't change often)

Node 2

Database Server Backup

Back up the PostgreSQL database using pg_dump:

Windows.\backup-windows.ps1 -IncludeDB -PgDatabase minusnow
Linux./backup-linux.sh --include-db --pg-database minusnow

Frequency: Daily (data changes frequently)

Step 13a: Automated Database Backup Schedule

🖥️ Windows (Node 2)
PowerShell (Admin)# Create a Scheduled Task for daily database backup at 2 AM
$action = New-ScheduledTaskAction `
  -Execute "powershell.exe" `
  -Argument "-File D:\MinusNow\documentation\downloads\backup-windows.ps1 -IncludeDB -PgDatabase minusnow -OutputDir D:\backups -RetainDays 30"
$trigger = New-ScheduledTaskTrigger -Daily -At "02:00AM"
Register-ScheduledTask -TaskName "MinusNow-DB-Backup" -Action $action -Trigger $trigger -RunLevel Highest

# Verify
Get-ScheduledTask -TaskName "MinusNow-DB-Backup"
🐧 Linux (Node 2)
Bash# Add to root's crontab: daily DB backup at 2 AM, retain 30 days
sudo crontab -e

# Add this line:
0 2 * * * /opt/minusnow/documentation/downloads/backup-linux.sh --include-db --pg-database minusnow --output-dir /backups --retain-days 30 >> /var/log/minusnow-backup.log 2>&1

Step 13b: Restore from Backup

If you need to restore the database on Node 2:

Node 2# Restore a SQL dump
# Windows:
psql -U postgres -d minusnow -f "D:\backups\minusnow-db-20260213-020000.sql"

# Linux:
sudo -u postgres psql -d minusnow -f /backups/minusnow-db-20260213-020000.sql

14. Security Hardening

14.1 Network Security

RuleNode 1 (App)Node 2 (DB)
Port 5000 (HTTP)Open to users/LANClosed
Port 5432 (PostgreSQL)ClosedOpen ONLY from Node 1 IP
Port 22 (SSH)Admin onlyAdmin only
All other portsClosedClosed

14.2 PostgreSQL Security

14.3 Application Security

14.4 Backup Security

15. Troubleshooting

15.1 Cannot Connect to Remote Database from Node 1

Symptom: ECONNREFUSED, Connection timed out, or no pg_hba.conf entry

⚠️

Check these items in order:

  1. PostgreSQL is running on Node 2? Check Get-Service postgresql* or systemctl status postgresql
  2. listen_addresses in postgresql.conf is set to '*' or includes Node 2's IP?
  3. pg_hba.conf has a host line for Node 1's IP?
  4. Firewall on Node 2 allows port 5432 from Node 1?
  5. Network connectivity: Can you ping Node 2 from Node 1?
  6. Password correct? Test with psql -h NODE2_IP -U minusnow_user -d minusnow

15.2 "permission denied for schema public"

Cause: PostgreSQL 15+ removed default CREATE privilege on the public schema.

Fix (run on Node 2)# Connect to the minusnow database as superuser
psql -U postgres -d minusnow

GRANT ALL ON SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO minusnow_user;

15.3 drizzle-kit "unknown command 'push'"

Cause: A global drizzle-kit (v0.18.x) overrides the local version (v0.31.x) needed by MinusNow.

Fix (run on Node 1)# Check for global version
npm list -g drizzle-kit

# If found, remove it
npm uninstall -g drizzle-kit

# Re-install local deps
cd D:\MinusNow    # or /opt/minusnow
rm -rf node_modules package-lock.json
npm install

# Verify correct version
npx drizzle-kit --version    # Should show 0.31.x

15.4 Replication Not Starting

Symptom: pg_stat_replication on primary shows no rows.

⚠️
  1. standby.signal exists in the replica's data directory?
  2. primary_conninfo in postgresql.auto.conf has the correct primary IP, user, and password?
  3. Firewall on primary allows port 5432 from the replica's IP?
  4. pg_hba.conf on primary has a replication line for the replica's IP?
  5. wal_level = replica in primary's postgresql.conf?
  6. Check replica's PostgreSQL log for specific errors

15.5 High Replication Lag

Symptom: Data on replica is minutes or hours behind the primary.

15.6 Application Shows "Connection Refused" After Failover

Cause: Node 1's .env still points to the old primary (Node 2) instead of the promoted replica.

Fix# Update .env on Node 1 to point to the new primary
# Change: @192.168.1.20 → @192.168.1.30

# Then restart the app:
# Windows: Restart-Service MinusNow
# Linux:   sudo systemctl restart minusnow

15.7 psql Not Found on Windows

Cause: PostgreSQL installer doesn't add bin folder to PATH automatically.

Fix (PowerShell as Admin)# Add to system PATH (adjust version number)
$pgBin = "C:\Program Files\PostgreSQL\18\bin"
$current = [Environment]::GetEnvironmentVariable("Path","Machine")
[Environment]::SetEnvironmentVariable("Path","$current;$pgBin","Machine")

# Close and reopen PowerShell, then verify:
psql --version

15.8 Port 5000 Already in Use

Fix# Windows: Find and kill the process
Get-NetTCPConnection -LocalPort 5000 | Select-Object OwningProcess
Stop-Process -Id <PID> -Force

# Linux: Find and kill
sudo lsof -i :5000
sudo kill <PID>

15.9 Blank Page After Login


Appendix A: Quick Reference & Cheat Sheet

IP Address Reference (Replace with your actual IPs)

NodeIPPortsServices
App Server (Node 1)192.168.1.105000Node.js, Express, MinusNow
DB Primary (Node 2)192.168.1.205432PostgreSQL (primary)
DB Replica (optional)192.168.1.305432PostgreSQL (standby)

Key Files to Edit on Node 2 (DB Server)

FilePurposeKey Settings
postgresql.confServer configurationlisten_addresses, wal_level, max_wal_senders
pg_hba.confClient authenticationAllow Node 1's IP with scram-sha-256

Key File on Node 1 (App Server)

FileKey SettingExample
.envDATABASE_URLpostgresql://minusnow_user:Pass@192.168.1.20:5432/minusnow
.envSESSION_SECRET48+ character random string
.envPORT5000
.envNODE_ENVproduction

Essential Commands Cheat Sheet

🖥️ Node 1 (App Server)

Commands# Install dependencies
npm install

# Push schema to remote DB
npm run db:push

# Build for production
npm run build

# Start production server
npm run start

# Check health
curl http://localhost:5000/api/health

🗄️ Node 2 (DB Server)

Commands# Check PostgreSQL status
# Win: Get-Service postgresql*
# Lin: systemctl status postgresql

# Restart PostgreSQL
# Win: Restart-Service postgresql*
# Lin: sudo systemctl restart postgresql

# Check active connections
psql -U postgres -c "SELECT client_addr, state FROM pg_stat_activity WHERE datname='minusnow';"

# Check replication (if replica)
psql -U postgres -c "SELECT client_addr, state FROM pg_stat_replication;"

# Backup database
pg_dump -U postgres minusnow > backup.sql

Deployment Checklist