Bitemporal
Semantics

Every fact in Substrate exists in two time dimensions. Understanding this model is essential for using the system correctly.

Two dimensions of time

Valid Time

"When was this true in reality?"

Valid time represents when a fact was actually true in the real world. If Alice became the owner of the payments service on January 15th, that's the valid-time start.

Example: A contract was signed on Dec 1, 2025. The valid_from is Dec 1, 2025 - when the contract became legally binding.

System Time

"When did the system know this?"

System time represents when the fact entered the system. If the ownership change was recorded on January 18th, that's the system-time. The 3-day gap is normal - systems always learn about reality with delay.

Example: The contract was entered into the system on Dec 5, 2025. The sys_from is Dec 5 - when we knew about it.

Why both matter

With two time dimensions, you can answer questions that are impossible with a single timestamp:

"What was true on December 15th?"

Query at valid-time = Dec 15, system-time = now. Returns what we currently know about reality at that point. Uses latest knowledge.

AS-OF VALID Dec-15 AT SYSTEM HEAD()

"What did we believe on December 15th?"

Query at system-time = Dec 15. Returns what the system knew at that point - even if we later learned it was wrong.

AT SYSTEM Dec-15

"What changed in our knowledge this week?"

Diff query on system-time. Shows all facts that were added, corrected, or retracted - regardless of their valid-time.

BETWEEN SYSTEM Jan-8 AND Jan-15

"What did we think was true then that we now know was wrong?"

Compare system-time = then vs system-time = now for the same valid-time. Reveals corrections and updated understanding.

DIFF AT SYSTEM Jan-1 vs HEAD() FOR VALID Dec-15

Bitemporal record structure

Every versioned fact in Substrate carries both time dimensions:

bitemporal_version:
  subject      → entity_id or edge_id
  field        → property key or relation predicate
  value        → the actual data

  valid_from   → when this became true in reality
  valid_to     → when this stopped being true (∞ if still true)

  sys_from     → when the system recorded this
  sys_to       → when this was superseded (∞ if current)

  assertion_id → provenance reference
  integrity    → hash for verification

Interval semantics

  • Intervals are half-open: [from, to)
  • valid_to = +∞ means "still true"
  • sys_to = +∞ means "current version"
  • System intervals never reopen once closed

Core operations

All operations are append-only. Nothing is ever mutated or deleted from the truthlog.

Insert

Add a new fact about reality

Creates a new bitemporal version with sys_from = now, sys_to = ∞. The valid interval is specified by the caller.

INSERT: Alice owns payments
  valid_from: 2025-12-01
  valid_to:   ∞
  sys_from:   2025-12-05 (when we learned)
  sys_to:     ∞

Correct

We learned an earlier record was wrong

Closes the old version's system interval and inserts a new version with the corrected data. History is preserved - you can still see what we believed before.

CORRECT: Actually Bob owns payments

Old version:
  sys_to: 2025-12-10 (closed)

New version:
  valid_from: 2025-12-01
  valid_to:   ∞
  sys_from:   2025-12-10 (correction time)
  sys_to:     ∞

Backfill

Late-arriving data about the past

Same as insert, but valid-time is in the past. Common when ingesting historical data or receiving delayed updates.

BACKFILL: Found old incident from August
  valid_from: 2025-08-15
  valid_to:   2025-08-16
  sys_from:   2025-12-10 (when ingested)
  sys_to:     ∞

Retract

We no longer assert this claim

Closes the system interval and may insert a "unknown" sentinel. The retraction event is itself recorded for audit.

RETRACT: We're not sure who owns payments anymore

Old version:
  sys_to: 2025-12-15 (retracted)

Retraction event recorded with reason

Merge

Entity resolution - two records are the same thing

Creates a same_as link between entities. Neither entity is deleted. Query layer can canonicalize via resolution rules.

MERGE: service-123 and svc-123 are the same

Creates: same_as(service-123, svc-123)
Both IDs remain valid, queries can resolve

Query semantics

The AS-OF clause specifies which versions to return:

AS-OF(valid=t, system=s) returns versions where:
  valid_from <= t < valid_to
  AND
  sys_from <= s < sys_to

Current state (default)

valid=now, system=now - What's true right now according to our current knowledge.

Historical valid-time

valid=past, system=now - What was true then, with everything we know now (including corrections).

Historical system-time

valid=now, system=past - What we believed was true at that point (might include errors we later corrected).

Full time-travel

valid=past, system=past - What we believed at time S about what was true at time V.

Invariants

These properties are guaranteed and enforced by the system:

I1

Append-only

Once a commit is written, it is immutable. System intervals only close, never reopen. Corrections append new versions.

I2

No overlapping system intervals

For a given lineage (subject + field), system intervals do not overlap. At any system-time, exactly one version is "current".

I3

Monotonic commit IDs

Commit IDs strictly increase within a tenant. Ordering is total and deterministic.

I4

Integrity verification

Every version's integrity hash can be verified against its canonical encoding. Hash chain connects commits.

I5

Provenance required

Every version has exactly one assertion_id linking to its provenance record.

Continue reading