← All posts
Engineering

Why we made the data model bitemporal

The two-time-axis design choice that makes every other audit-grade feature possible.

By · Apr 25, 2026

Bitemporal in one sentence

Every fact has a valid time (when it was true) and a recorded time (when we wrote it down). They're independent. Most databases collapse them; we don't.

Why it matters

Every CMS sooner or later has to handle: late corrections.

Someone writes an article. It publishes. Three weeks later, legal flags a typo as of the publish date. You correct it. Now what does history() show?

In a single-time-axis system: the original is gone. Replaced by the corrected version. history() shows you "edited at week 3", but if an auditor asks "what did the public site say between weeks 1 and 3?" — you have nothing.

In a bitemporal system: the original is preserved. The correction is a new fact that says "this was true from publish date, but we observed it three weeks later". history() shows both. Auditors get the whole picture.

The technical shape

MATCH (n:Article {slug: 'launching-staticowl'})
WHERE n._valid_from <= '2026-04-22T15:00:00Z'
  AND (n._valid_to IS NULL OR n._valid_to > '2026-04-22T15:00:00Z')
  AND n._recorded_from <= '2026-04-25T00:00:00Z'
  AND (n._recorded_to IS NULL OR n._recorded_to > '2026-04-25T00:00:00Z')
RETURN n

Read: "what did this article look like as of April 22 at 3pm, observed as of April 25". Long version of AT syntax — same thing under the hood.

What this enables

What it costs

Storage: ~2x. Each property change appends a new version row. Compaction recycles slack but doesn't delete history.

Mental load: editorial users don't see any of this. Engineers querying the graph have to know whether they want AT (specific point in time) or just current (_valid_to IS NULL). The CMS API hides this; raw Cypher exposes it.

We think it's worth it. So do our design partners. Read the architecture doc →