16 KiB
Implementation Plan: PostgreSQL Migration
Overview
Migrate the CVE Dashboard backend from SQLite to PostgreSQL 16. Replace the monolithic findings_json blob (2.6MB) with individual indexed rows in ivanti_findings, enable per-BU closed counts, and eliminate the single-writer lock. All work on the feature/multi-tenancy branch. Docker container steam-postgres on port 5433, test backend on port 3003, production on port 3001.
Tasks
-
1. Infrastructure setup and connection pool
-
1.1 Install
pgdependency and configure environment- Run
npm install pginbackend/ - Add
DATABASE_URL=postgresql://steam:<password>@localhost:5433/cve_dashboardtobackend/.env - Add
DATABASE_URLplaceholder tobackend/.env.example - Requirements: 1.5, 5.1, 5.2
- Run
-
1.2 Create
backend/db.jsconnection pool module- Import
pgand create aPoolinstance reading fromDATABASE_URL - Set
max: 10,idleTimeoutMillis: 30000,connectionTimeoutMillis: 5000 - Add
pool.on('error')handler logging unexpected errors - Track active connections; log warning when count reaches 8
- Export the pool instance for use by all route files
- Requirements: 5.1, 5.2, 5.3, 5.4, 5.5
- Import
-
1.3 Create Docker run command documentation
- Document the
docker runcommand forsteam-postgrescontainer (port 5433:5432, volumesteam-pgdata,--restart unless-stopped, Postgres 16 Alpine) - Verify container creates
cve_dashboarddatabase withsteamuser - Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6
- Document the
-
-
2. Schema creation
-
2.1 Create
backend/db-schema.sqlwith complete Postgres DDL- Define all tables with proper Postgres types: SERIAL, TIMESTAMPTZ, BOOLEAN, NUMERIC, TEXT[], DATE
- Include
ivanti_findingstable with TEXT PRIMARY KEY (id), all columns per design, CHECK constraint onstate - Include
ivanti_sync_statetable (single-row pattern, replacesivanti_findings_cachemetadata) - Include all other tables: users (with
bu_teams), sessions, audit_logs, cves, documents, required_documents, jira_tickets, archer_tickets, knowledge_base, compliance_uploads, compliance_items, compliance_notes, ivanti_counts_cache, ivanti_counts_history, ivanti_finding_archives, ivanti_archive_transitions, ivanti_sync_anomaly_log, ivanti_finding_bu_history, ivanti_fp_submissions, ivanti_fp_submission_history, ivanti_todo_queue, atlas_action_plans_cache - Include all indexes: findings (state, bu_ownership, severity, state+bu_ownership composite), sessions (session_id, user_id, expires_at), audit_logs (created_at), todo_queue (user_id+status)
- Include all foreign key relationships and CHECK constraints
- Use
CREATE TABLE IF NOT EXISTSandCREATE INDEX IF NOT EXISTSfor idempotence - Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.3
-
* 2.2 Write property test for schema creation idempotence
- Property 8: Schema Creation Idempotence
- Run schema DDL N times against the same database, verify no errors and same resulting schema each time
- Validates: Requirements 2.5
-
2.3 Create schema initialization in
backend/setup.js- Read
db-schema.sqland execute via pool.query - Make callable on server startup or as standalone script
- Seed
ivanti_sync_staterow (id=1) andivanti_counts_cacherow (id=1) if not exists - Requirements: 2.1, 2.5
- Read
-
-
3. Checkpoint — Verify infrastructure
- Ensure Docker container is running on port 5433, pool connects successfully, schema creates without errors. Ask the user if questions arise.
-
[-] 4. Migrate auth and session system
-
4.1 Update
backend/middleware/auth.js- Replace
db.get()callback withpool.query()async/await - Change
?placeholders to$1, $2, ...numbered params - Join sessions and users tables, check
expires_at > NOW() - Return 401 for missing/expired sessions, 500 for query errors
- Requirements: 6.1, 6.4, 6.5, 10.3
- Replace
-
4.2 Update
backend/routes/auth.js- Replace all
db.get/db.all/db.runwithpool.query - Login: query user by username, create session with
RETURNING - Logout: delete session by session_id
- Password change: update password_hash
- Profile/me endpoint: query user by session
- Use
$1, $2...placeholders throughout - Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 10.3
- Replace all
-
4.3 Update
backend/routes/users.js- Replace all sqlite3 calls with pool.query
- CRUD operations: list users, create user, update user, delete user
- Use
RETURNINGclause for inserts/updates where row data is needed - Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
-
4.4 Update
backend/server.jsdatabase initialization- Remove sqlite3 database opening and
dbobject creation - Import pool from
backend/db.js - Remove passing
dbparameter to route factory functions - Update inline CVE/document/vendor routes to use pool.query
- Requirements: 6.1, 6.4, 6.5
- Remove sqlite3 database opening and
-
-
5. Checkpoint — Verify auth and core routes
- Ensure login, logout, session validation, user CRUD, and CVE routes work on port 3003. Ask the user if questions arise.
-
[-] 6. Migrate remaining route files
-
6.1 Update
backend/routes/jiraTickets.js- Replace sqlite3 calls with pool.query, update placeholders
- Requirements: 6.1, 6.4, 6.5
-
6.2 Update
backend/routes/archerTickets.js- Replace sqlite3 calls with pool.query, update placeholders
- Requirements: 6.1, 6.4, 6.5
-
6.3 Update
backend/routes/knowledgeBase.js- Replace sqlite3 calls with pool.query, update placeholders
- Requirements: 6.1, 6.4, 6.5
-
6.4 Update
backend/routes/compliance.js- Replace sqlite3 calls with pool.query, update placeholders
- Handle compliance_items, compliance_uploads, compliance_notes queries
- Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
-
6.5 Update
backend/routes/auditLog.jsandbackend/helpers/auditLog.js- Replace sqlite3 db.run/db.all with pool.query
- Update the audit logging helper to use async pool.query
- Requirements: 6.1, 6.4, 6.5
-
6.6 Update
backend/routes/ivantiWorkflows.js- Replace sqlite3 calls with pool.query, update placeholders
- Requirements: 6.1, 6.4, 6.5
-
6.7 Update
backend/routes/ivantiFpWorkflow.js- Replace sqlite3 calls with pool.query, update placeholders
- Handle fp_submissions and fp_submission_history tables
- Requirements: 6.1, 6.4, 6.5
-
6.8 Update
backend/routes/ivantiTodoQueue.js- Replace sqlite3 calls with pool.query, update placeholders
- Requirements: 6.1, 6.4, 6.5
-
6.9 Update
backend/routes/ivantiArchive.js- Replace sqlite3 calls with pool.query, update placeholders
- Handle archive and transition table queries
- Requirements: 6.1, 6.4, 6.5
-
6.10 Update
backend/routes/atlas.js- Replace sqlite3 calls with pool.query, update placeholders
- Requirements: 6.1, 6.4, 6.5
-
6.11 Update
backend/routes/cardApi.jsandbackend/routes/feedback.js- Replace any sqlite3 calls with pool.query if present
- Requirements: 6.1, 6.4, 6.5
-
-
7. Checkpoint — Verify all route migrations
- Ensure all non-findings routes work correctly on port 3003. Ask the user if questions arise.
-
[-] 8. Rewrite Ivanti findings sync and read logic
-
8.1 Rewrite sync logic in
backend/routes/ivantiFindings.js- Replace JSON blob serialization with batch upsert of individual rows
- Use
INSERT INTO ivanti_findings (...) VALUES ... ON CONFLICT (id) DO UPDATE SET ...in batches of 100 - Sync both open and closed findings as individual rows with correct
statevalue - Update
ivanti_sync_statewith total count, synced_at, sync_status after sync - Preserve existing note and override values during upsert (do not overwrite user-set fields)
- Requirements: 3.4, 3.5, 6.6, 9.3
-
* 8.2 Write property test for upsert idempotence
- Property 1: Upsert Idempotence
- Generate random findings, upsert each N times with same ID, verify exactly one row per ID with most recent data
- Validates: Requirements 3.5, 6.6
-
8.3 Rewrite read endpoints in
backend/routes/ivantiFindings.js- GET /findings:
SELECT * FROM ivanti_findings WHERE state = 'open'with optional BU filter viabu_ownership ILIKE - Return response shape:
{ findings, total, synced_at, sync_status, error_message } - GET /findings/counts: derive open/closed counts from
ivanti_findingswith optional BU filter - Support
teamsquery parameter for per-BU scoped counts usingILIKE ANY(ARRAY[...]) - GET /findings/counts/history: unchanged (reads from
ivanti_counts_history) - Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 9.1, 9.2, 10.1, 10.2
- GET /findings:
-
* 8.4 Write property test for finding state and BU preservation
- Property 2: Finding Storage Preserves State and BU Ownership
- Generate findings with random state/bu_ownership, store and retrieve by ID, verify equality
- Validates: Requirements 3.4, 4.1
-
* 8.5 Write property test for count query accuracy
- Property 3: Count Query Accuracy
- Generate random finding sets, insert into DB, run count queries with random BU filters, verify counts match actual row counts
- Validates: Requirements 4.2, 4.3, 4.5
-
8.6 Update note and override endpoints
- PUT /findings/:id/note:
UPDATE ivanti_findings SET note = $1 WHERE id = $2 - PUT /findings/:id/override:
UPDATE ivanti_findings SET override_host_name = $1, override_dns = $2 WHERE id = $3 - Requirements: 6.1, 6.4
- PUT /findings/:id/note:
-
-
9. Checkpoint — Verify findings redesign
- Ensure sync creates individual rows, read endpoints return correct shape, counts work with and without BU filter. Ask the user if questions arise.
-
[-] 10. Data migration script
-
10.1 Create
backend/scripts/migrate-to-postgres.js- Open SQLite database in read-only mode (
OPEN_READONLY) - Connect to Postgres via
DATABASE_URL - Run schema creation (idempotent) before inserting data
- Copy all simple tables: users, sessions, cves, documents, required_documents, jira_tickets, archer_tickets, knowledge_base, audit_logs, compliance_uploads, compliance_items, compliance_notes, ivanti_counts_history, ivanti_finding_archives, ivanti_archive_transitions, ivanti_sync_anomaly_log, ivanti_finding_bu_history, atlas_action_plans_cache, ivanti_fp_submissions, ivanti_fp_submission_history, ivanti_todo_queue
- Handle type conversions: SQLite 0/1 → Postgres boolean, DATETIME strings → TIMESTAMPTZ
- Use batch inserts with
ON CONFLICTfor idempotency (safe to re-run) - Requirements: 7.1, 7.2, 7.3, 7.7, 7.9, 7.10
- Open SQLite database in read-only mode (
-
10.2 Implement findings migration with note/override merging
- Parse
ivanti_findings_cache.findings_jsonblob into individual objects - Insert each finding as a row in
ivanti_findingswithstate = 'open' - Query
ivanti_finding_notesand merge each note into the correspondingivanti_findings.notecolumn - Query
ivanti_finding_overridesand merge intoivanti_findings.override_host_name/override_dns - Requirements: 7.4, 7.5, 7.6
- Parse
-
10.3 Add verification and summary reporting
- After migration, query row counts for every table in both SQLite and Postgres
- Print comparison table showing source count vs destination count
- Flag any discrepancies with warnings
- Exit with code 0 on success, code 1 on any errors
- Requirements: 7.8, 7.9
-
* 10.4 Write property test for migration data preservation (findings)
- Property 4: Migration Data Preservation (Findings)
- Generate random findings JSON with associated notes and overrides, run migration logic, verify merged output matches expected values
- Validates: Requirements 7.4, 7.5, 7.6
-
* 10.5 Write property test for migration table copy preservation
- Property 5: Migration Table Copy Preservation
- Generate random table rows, copy via migration logic, verify row counts and data equivalence (accounting for type conversions)
- Validates: Requirements 7.7, 7.8
-
* 10.6 Write property test for migration idempotence
- Property 6: Migration Idempotence
- Run migration logic N times on same input data, verify final Postgres state equals single-run state (no duplicates)
- Validates: Requirements 7.3, 7.9
-
* 10.7 Write property test for migration source safety
- Property 7: Migration Source Safety
- Checksum SQLite file before and after migration script execution, verify bytes unchanged
- Validates: Requirements 7.10
-
-
11. Checkpoint — Verify data migration
- Run migration script against test Postgres instance, verify all row counts match, verify findings have merged notes/overrides. Ask the user if questions arise.
-
12. Frontend updates for per-BU closed counts
-
12.1 Update ReportingPage donut chart to show per-BU closed counts
- Remove "N/A" fallback for closed count when BU filter is active
- Display actual closed count from the updated
/api/ivanti/findings/countsendpoint - Pass
teamsparameter to counts endpoint for server-side BU filtering - Requirements: 4.3, 4.4
-
* 12.2 Write property test for API response shape preservation
- Property 9: API Response Shape Preservation
- For various valid API requests, verify response JSON structure (top-level keys and value types) matches expected contract
- Validates: Requirements 10.1
-
-
13. Testing and verification
-
13.1 Run backend on port 3003 and verify all endpoints
- Set
PORT=3003in test environment withDATABASE_URLpointing to Postgres - Verify auth endpoints (login, logout, me, password change)
- Verify findings endpoints (list, counts, sync, notes, overrides)
- Verify all other routes (CVEs, Jira, Archer, compliance, knowledge base, audit log)
- Compare response shapes against production SQLite backend on port 3001
- Requirements: 9.1, 9.2, 10.1, 10.2, 10.3, 10.4, 11.1, 11.2, 11.3
- Set
-
13.2 Performance verification
- Confirm GET /api/ivanti/findings responds in <500ms with full dataset
- Confirm GET /api/ivanti/findings/counts responds in <100ms
- Confirm sync does not block concurrent read requests
- Requirements: 9.1, 9.2, 9.3, 9.4
-
13.3 Run existing test suite against Postgres backend
- Verify all existing property tests and unit tests pass
- Requirements: 10.1, 10.3
-
-
14. Cutover to production
-
14.1 Execute cutover procedure
- Run final Ivanti sync on SQLite production backend
- Run migration script to copy latest data to Postgres
- Stop production backend on port 3001
- Update production
.envwithDATABASE_URL - Start new backend on port 3001 with Postgres
- Verify frontend loads and API responds correctly
- Requirements: 8.1, 8.2, 8.3
-
14.2 Preserve SQLite backup and document rollback
- Keep
backend/cve_database.dbas permanent backup (do not delete) - Document rollback procedure: stop backend → remove DATABASE_URL from .env → restart
- Requirements: 8.3, 8.4
- Keep
-
-
15. Cleanup
- 15.1 Remove SQLite dependency and legacy code
- Run
npm uninstall sqlite3frombackend/ - Remove any remaining sqlite3 imports or
dbparameter passing - Remove the old
ivanti_findings_cacheandivanti_finding_notes/ivanti_finding_overridestable references - Update
backend/.env.exampleand README with Postgres prerequisites - Requirements: 6.5, 11.4
- Run
- 15.1 Remove SQLite dependency and legacy code
-
16. Final checkpoint — Ensure all tests pass
- Ensure all tests pass, production is stable on Postgres, and SQLite backup is preserved. Ask the user if questions arise.
Notes
- Tasks marked with
*are optional and can be skipped for faster MVP - Each task references specific requirements for traceability
- Checkpoints ensure incremental validation between major phases
- Property tests validate universal correctness properties from the design document
- All work on
feature/multi-tenancybranch - Docker container
steam-postgreson port 5433 (NOT 5432 — that belongs to another project) - Test backend on port 3003 during development; production stays on port 3001 with SQLite until cutover
- The
pgpackage connection pool handles concurrent reads during sync writes (MVCC) - Batch upserts in chunks of 100 for sync performance
- SQLite file is never modified — opened read-only during migration