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..cd8a31d 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, defaults } from 'pg'; import { logger } from './logger'; // Global connection pool @@ -9,6 +9,21 @@ 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')); + + // 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;