launch-quickly
Decisions

0009 — A calendar date is a date, not a timestamp

The best-evidenced decision in the codebase. Two shipped bugs, and both lq check and the generated tests passed throughout.

Context

A due date, an issue date, a publication date. These are calendar dates: the same day for everyone reading them. timestamptz is the reflexive choice and it is wrong for this, in a way that only shows up for readers in a different timezone from whoever wrote the code.

Decision

date('column', { mode: 'string' }). A real date column, and the value stays a 'YYYY-MM-DD' string from the column all the way to <input type="date">.

The two bugs

This record exists because both of these shipped.

Off by one day. Stored as a timestamp, a date rendered as the previous day for any reader west of whoever entered it. Nothing errored.

A form that silently cleared the field it was editing. <input type="date"> accepts only 'YYYY-MM-DD'. Handed a Date, the browser rejects the value and renders the field empty — so opening a record to edit it and saving blanked the date. No error, no warning; the user's data quietly gone.

And the line that earns this page its place:

Both lq check and the generated tests passed throughout.

That is corroborated independently in for-agents: of the worst bugs found while building products on this template, "two of the worst passed both lq check and the generated tests. A required text field did not compile, and a date form silently blanked the field it was editing."

Consequences

The decision does not stop at the column. Both halves of the round trip have to agree or the bug comes back:

formatDate parses and formats in UTC — precisely the bug mode: 'string' exists to prevent, reintroduced at the render layer if it did not.

todayForDateInput is deliberately not toISOString().slice(0, 10). That is UTC, so anywhere behind it the form offers tomorrow's date all evening.

There is deliberately no timestamp field type in the generator. A stored instant renders as the wrong day for a reader in another timezone, and the generator should not make that easy to reach for.

Why it is the strongest record here

Every other page argues from a design principle. This one argues from two user-visible data-loss bugs that every automated gate approved. If you read one record to understand why this codebase enforces things at the type and schema level rather than by convention, read this one.

On this page