7 July 2026

One Field, Two States: Modeling Draft and Published with a Single Nullable Date

Most CMS-shaped data models reach for two fields to represent publication state: a boolean published and a publishedAt timestamp. It reads naturally, but it also opens the door to a state that shouldn't exist: published: true with no date, or published: false with a date already in the past.

The single-field version

The Post model in dev-hub collapses both into one nullable column:

model Post {
  publishedAt DateTime?
}

null means draft. Any datetime means "published from that point on." There's no second flag to fall out of sync with the first, because there's only one source of truth.

What it buys at query time

The public listing becomes a single filter, centralized in src/lib/posts.ts instead of repeated at every call site:

where: { publishedAt: { lte: new Date() } }

That one line does double duty: it excludes drafts (null never satisfies lte) and it excludes future-scheduled posts. A separate boolean would need an and of two conditions, checked everywhere the query is written, with every new call site a chance to forget one half.

The scheduling side-effect

Because "published" is really "published as of a timestamp," scheduling a post for the future costs nothing extra — set publishedAt to a future date and it simply isn't visible yet. Combined with the site's existing 60-second ISR revalidate window, a scheduled post appears on its own, with no cron job watching the clock.

The lesson generalizes past this one model: when two fields are always supposed to agree, look for the single field that makes disagreement impossible instead of the validation that catches it after the fact.