From 1ae7fcc287fec936143b6589dc2e9de16144388d Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 13 Jul 2026 10:36:56 -0500 Subject: [PATCH 1/2] fix(db): parse timestamp columns as UTC regardless of server timezone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All `timestamp without time zone` columns store UTC by convention, but node-pg's default parser interprets them in the server's local timezone. Every sync path assumes UTC via toISOString()/getTime() — QRZ/LoTW ADIF date/time emission, both +/-15-minute confirmation matchers, and the .tq8 signer — so a self-hosted install with TZ != UTC shipped skewed QSO times and mismatched confirmations. On Vercel (TZ=UTC) this is a no-op. Register a pg type parser for OID 1114 in getPool() that reads timestamps back as UTC, and document the convention in drizzle/schema.ts. Co-Authored-By: Claude Fable 5 --- drizzle/schema.ts | 5 +++++ src/lib/db.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 0bd95b5..810463d 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,6 +1,11 @@ import { pgTable, index, unique, check, serial, varchar, boolean, timestamp, jsonb, foreignKey, integer, numeric, text, date, inet, time, customType } from "drizzle-orm/pg-core" import { sql } from "drizzle-orm" +// Convention: every `timestamp` (without time zone) column stores UTC. +// src/lib/db.ts registers a pg type parser that reads them back as UTC, +// so the app behaves identically on Vercel (TZ=UTC) and self-hosted +// servers with a local timezone. + // Postgres `bytea` — not first-class in drizzle-orm/pg-core, defined here so // the p12 cert columns get a real TS type instead of `unknown`. const bytea = customType<{ data: Buffer }>({ diff --git a/src/lib/db.ts b/src/lib/db.ts index 7dbd8a8..102af5c 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,4 +1,4 @@ -import { Pool, PoolClient, QueryResult } from 'pg'; +import { Pool, PoolClient, QueryResult, types } from 'pg'; import { logger } from './logger'; // Global connection pool @@ -9,6 +9,14 @@ let pool: Pool; */ function getPool(): Pool { if (!pool) { + // All `timestamp without time zone` columns store UTC by convention + // (contacts.datetime, sync log timestamps, ...). node-pg's default parser + // interprets them in the server's local timezone, which silently skews + // QRZ/LoTW date/time emission and confirmation matching on self-hosted + // installs with TZ != UTC (on Vercel TZ is UTC, so this is a no-op). + // OID 1114 = timestamp without time zone. + types.setTypeParser(1114, (value: string) => new Date(value.replace(' ', 'T') + 'Z')); + const sslConfig = process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false; From 29a0088958cd1ebe883321dd403b5fde62f21db6 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 13 Jul 2026 12:19:40 -0500 Subject: [PATCH 2/2] fix(db): serialize Date params as UTC to match the UTC timestamp parser node-pg's default Date serialization emits local-time strings, and timestamp-without-time-zone columns ignore the trailing offset, so on a non-UTC Node process Dates written by Contact.ts / adif.ts would be stored as local wall-clock and then misread as UTC by the new parser. pg.defaults.parseInputDatesAsUTC = true makes Date params serialize as UTC (+00:00), which is also lossless for timestamptz columns. Addresses Copilot review feedback on #223. Co-Authored-By: Claude Fable 5 --- src/lib/db.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/db.ts b/src/lib/db.ts index 102af5c..cd8a31d 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,4 +1,4 @@ -import { Pool, PoolClient, QueryResult, types } from 'pg'; +import { Pool, PoolClient, QueryResult, types, defaults } from 'pg'; import { logger } from './logger'; // Global connection pool @@ -17,6 +17,13 @@ function getPool(): Pool { // OID 1114 = timestamp without time zone. types.setTypeParser(1114, (value: string) => new Date(value.replace(' ', 'T') + 'Z')); + // The write side must match: node-pg serializes Date params as *local* + // time strings by default, and `timestamp` columns ignore the trailing + // offset — so on a non-UTC Node process a Date would be stored as local + // wall-clock and then misread as UTC by the parser above. This flag makes + // pg serialize Date params as UTC (lossless for timestamptz too). + defaults.parseInputDatesAsUTC = true; + const sslConfig = process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false;