postgreSQL integration

This commit is contained in:
Huzaifa Inam 2026-08-07 14:38:37 +05:00
parent e61ddaa8b0
commit 057c2ca5d1
7 changed files with 251 additions and 59 deletions

View file

@ -2,7 +2,11 @@
PORT=3000 PORT=3000
NODE_ENV=production NODE_ENV=production
# Database & Data Persistence Path # PostgreSQL Connection String (Coolify / Managed Postgres / Docker)
DATABASE_URL=postgresql://postgres:postgres_secure_pass_2026@postgres:5432/balance_sheet_db
POSTGRES_PASSWORD=postgres_secure_pass_2026
# Fallback File Storage Path (Used if DATABASE_URL is omitted or offline)
DATA_PATH=/app/data/portal-data.json DATA_PATH=/app/data/portal-data.json
# Optional SMTP Email Configuration (If using real SMTP server) # Optional SMTP Email Configuration (If using real SMTP server)

View file

@ -1,69 +1,53 @@
# Deployment Guide for Coolify # Coolify & PostgreSQL Deployment Guide
This repository is optimized for deployment on **Coolify** using Docker or Docker Compose. This application consists of:
1. **Server Side**: Node.js + Express backend (`server.ts`) serving REST APIs, managing authentication, ledger reconciliation, audit logs, FX rates, and financial reports.
2. **Database**: PostgreSQL (via `pg` connection pool) with automatic JSON fall-back backup.
3. **Frontend**: React SPA served directly via Express in production.
--- ---
## Quick Deployment Options in Coolify ## 1. Quick Coolify Deployment Options
### Option A: Deployment via Git Repository (Recommended) ### Option A: Docker Compose Deployment (Application + PostgreSQL in One Stack)
1. **Push Code to Git**: Push this repository to GitHub, GitLab, or your self-hosted Git service. 1. **In Coolify Dashboard**:
2. **Add New Resource in Coolify**:
- Go to your Coolify dashboard.
- Click **+ Add Resource** -> **Public Repository** or **Private Repository**.
- Paste your repository URL and select the `main` branch.
3. **Select Build Pack**:
- Select **Dockerfile**. Coolify will automatically detect the `Dockerfile` in the root directory.
4. **Configure Port & Network**:
- **Port**: Set `3000`.
5. **Configure Persistent Volume**:
- Under **Storage / Volumes**, add a persistent volume mapping to prevent data loss on container redeployments:
- **Destination Path**: `/app/data`
6. **Deploy**:
- Click **Deploy**. Coolify will build the multi-stage Docker image and start your application.
---
### Option B: Deployment via Docker Compose
1. **Add New Resource in Coolify**:
- Click **+ Add Resource** -> **Docker Compose**. - Click **+ Add Resource** -> **Docker Compose**.
2. **Source Code**: - Point to your Git Repository or paste the contents of `docker-compose.yml`.
- Select your Git Repository or paste the contents of `docker-compose.yml`. 2. **Environment Variables**:
3. **Environment Variables**: Add the following in Coolify:
- Add the following environment variables in Coolify UI: ```env
NODE_ENV=production
PORT=3000
POSTGRES_PASSWORD=your_custom_secure_password
```
3. **Deploy**:
- Coolify will build the app container and spin up a dedicated `postgres:16-alpine` database container with health checks and persistent volume storage (`balance_sheet_postgres_data`).
---
### Option B: Coolify Managed PostgreSQL + Standalone App Container
If you prefer using Coolify's built-in managed PostgreSQL database resource:
1. **Create Database in Coolify**:
- Click **+ Add Resource** -> **PostgreSQL**.
- Coolify will provide an internal database connection URL (e.g., `postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db`).
2. **Deploy App Service**:
- Add your Git repo as a **Dockerfile** application.
- Set environment variable in Coolify UI:
```env ```env
NODE_ENV=production DATABASE_URL=postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db
PORT=3000 PORT=3000
DATA_PATH=/app/data/portal-data.json
``` ```
4. **Deploy**: 3. **Deploy**:
- Click **Deploy**. Coolify will spin up the container and attach the `portal_data` volume automatically. - The Express server will automatically connect to the PostgreSQL database, create the necessary table structures on boot, and store all portal state in PostgreSQL.
--- ---
## Local Testing with Docker ## 2. Server-Side Architecture Details
To build and test locally before deploying to Coolify: - **Automatic Table Creation**: On server boot, the backend runs `initPgDatabase()` to verify/create `app_portal_state`.
- **Automatic State Recovery**: If PostgreSQL contains existing data, the portal state is loaded directly from PostgreSQL. If empty, baseline seed data is written to PostgreSQL.
### Using Docker Compose: - **Resilient Fallback**: If `DATABASE_URL` is omitted, the app gracefully operates using persistent JSON storage (`/app/data/portal-data.json`).
```bash - **Health Check**: Express exposes `GET /api/health` for Docker container monitoring.
docker-compose up --build -d
```
Access the app at: `http://localhost:3000`
### Using Docker directly:
```bash
docker build -t balance-sheet-portal .
docker run -p 3000:3000 -v portal_data:/app/data balance-sheet-portal
```
---
## Health Check Endpoint
The container includes a built-in health check targeting:
`GET /api/health`
It returns `{"status": "ok"}` when the Express server and Vite static engine are healthy.

View file

@ -7,6 +7,7 @@
"dependencies": { "dependencies": {
"@google/genai": "^2.4.0", "@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/pg": "^8.20.4",
"@types/three": "^0.185.4", "@types/three": "^0.185.4",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -14,6 +15,7 @@
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"nodemailer": "^9.0.4", "nodemailer": "^9.0.4",
"pg": "^8.22.0",
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"recharts": "^3.10.1", "recharts": "^3.10.1",
@ -292,6 +294,8 @@
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="], "@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
"@types/pg": ["@types/pg@8.20.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg=="],
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
@ -572,6 +576,22 @@
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
"pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
"pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
@ -580,6 +600,14 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
@ -636,6 +664,8 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
@ -678,6 +708,8 @@
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], "ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],

View file

@ -1,6 +1,26 @@
version: '3.8' version: '3.8'
services: services:
# PostgreSQL Database Service
postgres:
image: postgres:16-alpine
container_name: balance-sheet-postgres
restart: unless-stopped
environment:
POSTGRES_DB: balance_sheet_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_secure_pass_2026}
ports:
- "5432:5432"
volumes:
- postgres_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d balance_sheet_db"]
interval: 10s
timeout: 5s
retries: 5
# Express + React Application Web Service
balance-sheet-portal: balance-sheet-portal:
build: build:
context: . context: .
@ -8,11 +28,15 @@ services:
image: balance-sheet-portal:latest image: balance-sheet-portal:latest
container_name: balance-sheet-portal container_name: balance-sheet-portal
restart: unless-stopped restart: unless-stopped
expose: depends_on:
- "3000" postgres:
condition: service_healthy
ports:
- "${PORT:-3000}:3000"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-postgres_secure_pass_2026}@postgres:5432/balance_sheet_db
- DATA_PATH=/app/data/portal-data.json - DATA_PATH=/app/data/portal-data.json
volumes: volumes:
- portal_data:/app/data - portal_data:/app/data
@ -27,3 +51,6 @@ volumes:
portal_data: portal_data:
name: balance_sheet_portal_data name: balance_sheet_portal_data
driver: local driver: local
postgres_db_data:
name: balance_sheet_postgres_data
driver: local

View file

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@google/genai": "^2.4.0", "@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/pg": "^8.20.4",
"@types/three": "^0.185.4", "@types/three": "^0.185.4",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -21,6 +22,7 @@
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"nodemailer": "^9.0.4", "nodemailer": "^9.0.4",
"pg": "^8.22.0",
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"recharts": "^3.10.1", "recharts": "^3.10.1",

View file

@ -38,6 +38,7 @@ import {
MakerCheckerStatus MakerCheckerStatus
} from './src/types.js'; } from './src/types.js';
import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js'; import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js';
import { initPgDatabase, loadStateFromPg, saveStateToPg } from './src/db/postgres.js';
const app = express(); const app = express();
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
@ -45,7 +46,7 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
app.use(express.json()); app.use(express.json());
// In-Memory Database / Persistent File Storage // In-Memory Database / Persistent File Storage
interface DB { export interface DB {
users: User[]; users: User[];
submissions: Record<string, Record<BranchId, BranchSubmission>>; // period -> branchId -> submission submissions: Record<string, Record<BranchId, BranchSubmission>>; // period -> branchId -> submission
smtpConfig: SmtpConfig; smtpConfig: SmtpConfig;
@ -186,10 +187,30 @@ function saveDB(data: DB) {
} catch (err) { } catch (err) {
console.error('Error saving portal-data.json:', err); console.error('Error saving portal-data.json:', err);
} }
// Asynchronously push state to PostgreSQL if configured
saveStateToPg(data).catch((err) => {
console.error('Error syncing state to PostgreSQL:', err);
});
} }
let db = loadDB(); let db = loadDB();
// Initialize PostgreSQL if DATABASE_URL or POSTGRES_HOST is present
(async () => {
const pgReady = await initPgDatabase();
if (pgReady) {
const pgState = await loadStateFromPg();
if (pgState) {
db = pgState;
console.log('[PostgreSQL] DB state successfully synced from PostgreSQL database.');
} else {
console.log('[PostgreSQL] Initializing PostgreSQL database with baseline state...');
await saveStateToPg(db);
}
}
})();
// ------------------------------------------------------------- // -------------------------------------------------------------
// AUTH ENDPOINTS // AUTH ENDPOINTS
// ------------------------------------------------------------- // -------------------------------------------------------------

122
src/db/postgres.ts Normal file
View file

@ -0,0 +1,122 @@
import pg from 'pg';
import type { DB } from '../../server';
const { Pool } = pg;
let pool: pg.Pool | null = null;
export function getPgPool(): pg.Pool | null {
if (pool) return pool;
const connectionString =
process.env.DATABASE_URL ||
process.env.POSTGRES_URL ||
process.env.DATABASE_PRIVATE_URL;
const host = process.env.POSTGRES_HOST || process.env.SQL_HOST;
const user = process.env.POSTGRES_USER || process.env.SQL_USER;
const password = process.env.POSTGRES_PASSWORD || process.env.SQL_PASSWORD;
const database = process.env.POSTGRES_DB || process.env.SQL_DB_NAME || 'balance_sheet_db';
const port = parseInt(process.env.POSTGRES_PORT || '5432', 10);
if (connectionString) {
console.log('[PostgreSQL] Initializing connection pool via DATABASE_URL');
pool = new Pool({
connectionString,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
} else if (host && user) {
console.log(`[PostgreSQL] Initializing connection pool via host ${host}:${port}, db: ${database}`);
pool = new Pool({
host,
port,
user,
password,
database,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
} else {
console.log('[PostgreSQL] No DATABASE_URL or POSTGRES_HOST provided. Operating in file-backed mode.');
return null;
}
pool.on('error', (err) => {
console.error('[PostgreSQL] Unexpected idle client error:', err);
});
return pool;
}
/**
* Initializes PostgreSQL schema table if needed
*/
export async function initPgDatabase(): Promise<boolean> {
const p = getPgPool();
if (!p) return false;
try {
const client = await p.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS app_portal_state (
id VARCHAR(50) PRIMARY KEY,
data JSONB NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`);
console.log('[PostgreSQL] Database table app_portal_state ensured.');
return true;
} finally {
client.release();
}
} catch (err) {
console.error('[PostgreSQL] Initialization failed:', err);
return false;
}
}
/**
* Load state from PostgreSQL
*/
export async function loadStateFromPg(): Promise<DB | null> {
const p = getPgPool();
if (!p) return null;
try {
const res = await p.query(`SELECT data FROM app_portal_state WHERE id = $1`, ['main_state']);
if (res.rows.length > 0 && res.rows[0].data) {
console.log('[PostgreSQL] Successfully restored portal state from PostgreSQL database.');
return res.rows[0].data as DB;
}
return null;
} catch (err) {
console.error('[PostgreSQL] Error loading state:', err);
return null;
}
}
/**
* Save state to PostgreSQL asynchronously
*/
export async function saveStateToPg(state: DB): Promise<void> {
const p = getPgPool();
if (!p) return;
try {
await p.query(
`
INSERT INTO app_portal_state (id, data, updated_at)
VALUES ($1, $2, NOW())
ON CONFLICT (id) DO UPDATE
SET data = EXCLUDED.data, updated_at = NOW();
`,
['main_state', JSON.stringify(state)]
);
} catch (err) {
console.error('[PostgreSQL] Error saving state:', err);
}
}