Store UTC Dates in Postgres with JavaScript

Here, we'll look at a way to store UTC dates in Postgres with JavaScript.

UTC

It's all about the UTCs, baby. It's still not great. You might still get it wrong. But you're making things easier on yourself.

Column Type

You need a place to store the date. Postgres uses tables with columns. Usually, there's one piece of data per column. The columns are typed.

For UTC dates, use the column type TIMESTAMP WITHOUT TIME ZONE (aka, just TIMESTAMP) (as opposed to TIMESTAMPTZ).

By doing this, Postgres will not adjust your data at all as it is inserted or selected from the table. It will save it just as it comes in. It'll come out the same way. UTC all the way down.

Here's a clarifying explanation of the difference in the column types on StackOverflow.

Create the Table

=># create table test(date timestamp without time zone);
CREATE TABLE
=># \d
                List of relations
 Schema |        Name         |   Type   | Owner
--------+---------------------+----------+-------
 public | test                | table    | jt
(1 rows)

=># \d test
                          Table "public.test"
 Column |            Type             | Collation | Nullable | Default
--------+-----------------------------+-----------+----------+---------
 date   | timestamp without time zone |           |          |

Create a Date in the Database

You can generate "now" in Postgres:

=># insert into test (date) values (CURRENT_TIMESTAMP);
INSERT 0 1
=># select * from test;
            date
----------------------------
 2023-04-11 22:18:21.614613
(1 row)

Query the Date

When the date column is queried with a library like node-postgres, the Date will be instantiated with a UTC ISO string.

import * as dotenv from "dotenv";
import pg from "pg";

dotenv.config();

const pool = new pg.Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  connectionTimeoutMillis: 5000,
});
const db = await pool.connect();

const res = await db.query("select * from test2");

console.log(res.rows[0].data.toISOString());

And sure enough, it's UTC (see that 'Z' on the end):

2023-04-12T04:18:21.614Z

Insert UTC Date from JavaScript

And what if we change the code to insert a date instead of generating a date from the database?

Local Date

Insert a new Date, implicitly of the timezone of the runtime:

const res = await db.query({
  text: "insert into test (date) values ($1) returning *",
  values: [new Date()],
});

It is automatically converted from that local date to a UTC date by node-postgres. Queried and logged in JS, it is:

2023-04-11T22:55:24.773Z

That works.

UTC Date

Insert a new UTC Date, made so by using a propery ISO string ending 'Z' when instantiating the date:

const res = await db.query({
  text: "insert into test (date) values ($1) returning *",
  values: [new Date("2023-04-11T23:00:00.000Z")],
});

It passes the UTC Date straight into Postgres as-is. Queried and logged in JS, it is:

2023-04-11T23:00:00.000Z

It works.

UTC ISO String

Instead of a Date, does the ISO string work (no instantiation):

const res = await db.query({
  text: "insert into test (date) values ($1) returning *",
  values: ["2023-04-11T23:33:00.000Z"],
});

Queried and logged:

2023-04-12T05:33:00.000Z

It looks like UTC. And it is. But it has been adjusted by node-postgres, and not in a good way. The runtime timezone is MDT (-06:00), so the "UTC" time that it has stored has attempted to add 6 hrs and zero out the offset, going from 23:33 on the 11th to 5:33 on the 12th.This doesn't work. Instantiate Dates instead.

What else? Any other gotchas we should document while using JavaScript and Postgres to store dates?