Skip to content
Docs for briefcase-ai v3.3.0see what’s new.

Bitemporal Storage

Bitemporal Storage

Bitemporal storage tracks two independent time dimensions for every record: valid time (when a fact was true in the world) and transaction time (when the system learned about it) — so a later correction never erases what you actually knew at decision time.

bitemporal For: governance & audit

Valid time vs transaction time

Traditional storage overwrites: when a value changes, the old value is lost and the record of what you believed disappears with it. Bitemporal storage separates the two clocks so a backdated correction and the original belief can coexist. Writes are append-only — a correction is a new record, not an edit.

DimensionAnswersWhy it matters
Valid timeWhen was this fact true in the world?Lets a correction apply to a past date without rewriting history.
Transaction timeWhen did the system learn it?Distinguishes “what we knew on May 2” from “what we know now” — the key to a defensible audit.

How it works

  1. Record a fact with a valid_time and a transaction_time via BitemporalRecord.new.

  2. Append a backdated correction with append_correction — same valid time, a fresh transaction time, so both versions coexist.

  3. Reconstruct the past by clamping reads through an AsOfView at the decision’s transaction time.

flowchart LR
    A["BitemporalRecord.new<br/>(valid_time, transaction_time)"] --> B["store.append"]
    C["append_correction<br/>(new transaction_time)"] --> B
    B --> D["BitemporalStore<br/>(append-only)"]
    D --> E["store.latest / history"]
    D --> F["AsOfView(transaction_time)<br/>clamped read"]
    F --> G["reconstruct past belief"]

Install

Terminal window
pip install briefcase-ai[bitemporal]
from briefcase.bitemporal import (
BitemporalRecord,
InMemoryBitemporalStore,
AsOfView,
append_correction,
)

Record a Fact

from datetime import datetime, timezone
from briefcase.bitemporal import (
BitemporalRecord,
InMemoryBitemporalStore,
)
store = InMemoryBitemporalStore()
# A feature-flag rollout percentage that was true in the real world at t0.
t0 = datetime(2026, 5, 1, tzinfo=timezone.utc)
learned_t0 = datetime(2026, 5, 1, 9, 0, tzinfo=timezone.utc)
record = BitemporalRecord.new(
key="flag:new_checkout",
valid_time=t0,
value={"rollout_percent": 25},
source="config_service",
transaction_time=learned_t0,
)
store.append(record)
latest = store.latest("flag:new_checkout")
print(latest.value) # {'rollout_percent': 25}
print(latest.content_hash()[:12]) # SHA-256 of the value payload

valid_time and transaction_time must be timezone-aware; BitemporalRecord.new raises ValueError otherwise. When transaction_time is omitted it defaults to now.

Correct a Value and Reconstruct the Past

A correction shares the original valid_time but gets a fresh transaction_time and a parent_record_id back to the original. The old belief stays in the store.

from datetime import datetime, timezone
from briefcase.bitemporal import (
BitemporalRecord,
InMemoryBitemporalStore,
AsOfView,
append_correction,
)
store = InMemoryBitemporalStore()
t0 = datetime(2026, 5, 1, tzinfo=timezone.utc)
learned_t0 = datetime(2026, 5, 1, 9, 0, tzinfo=timezone.utc)
original = BitemporalRecord.new(
key="config:max_upload_mb",
valid_time=t0,
value=50,
source="config_service",
transaction_time=learned_t0,
)
store.append(original)
# A decision was made at this instant, reading what the system knew then.
decision_ts = datetime(2026, 5, 2, 12, 0, tzinfo=timezone.utc)
# Later, the config service corrects the same valid_time: it was 100, not 50.
learned_correction = datetime(2026, 5, 3, 8, 0, tzinfo=timezone.utc)
append_correction(
store,
original,
corrected_value=100,
source="config_service",
transaction_time=learned_correction,
)
# Current truth reflects the correction.
print(store.latest("config:max_upload_mb").value) # 100
# As-of the decision, the system had not yet learned the correction.
with AsOfView(store, transaction_time=decision_ts) as view:
print(view.latest("config:max_upload_mb").value) # 50
print(len(store.history("config:max_upload_mb"))) # 2 — both beliefs kept

append_correction requires the correction’s transaction_time to be strictly after the original’s; it raises ValueError otherwise, so a correction can never silently fail to supersede.

Clamp Reads with AsOfView

AsOfView(store, transaction_time=...) wraps any store and clamps every read to a historical instant. Application code keeps calling latest(key) / as_of(key) unchanged — no post-instant information leaks in. The view is read-only; append raises.

with AsOfView(store, transaction_time=decision_ts) as view:
record = view.latest("config:max_upload_mb")
rows = view.history("config:max_upload_mb")
keys = view.keys()

Pass valid_time= as well to restrict to facts that were true at a past real-world moment, distinct from what the system had learned by then.

Batch vs. Stream Ingestion

Both produce identical bitemporal output; they differ in transaction_time semantics. batch_append settles a whole batch at one shared instant; stream_append learns each record independently.

from datetime import datetime, timezone
from briefcase.bitemporal import (
BitemporalRecord,
InMemoryBitemporalStore,
batch_append,
stream_append,
)
store = InMemoryBitemporalStore()
valid = datetime(2026, 5, 1, tzinfo=timezone.utc)
# batch_append: many records settle at one shared transaction_time.
settled_at = datetime(2026, 5, 1, 23, 0, tzinfo=timezone.utc)
batch = [
BitemporalRecord.new(key="rate:US", valid_time=valid, value=0.07, source="rates_feed"),
BitemporalRecord.new(key="rate:EU", valid_time=valid, value=0.19, source="rates_feed"),
]
batch_append(store, batch, transaction_time=settled_at)
# stream_append: each record is learned independently, at append time.
tick = BitemporalRecord.new(key="rate:US", valid_time=valid, value=0.075, source="rates_feed")
stream_append(store, tick)
print(store.latest("rate:US").value) # 0.075
print(sorted(store.keys())) # ['rate:EU', 'rate:US']
HelperWhen you’d reach for it
batch_appendA whole batch becomes known at once — settle it at one shared transaction_time.
stream_appendRecords arrive one at a time — each is learned independently at append time.

Durable Backends

InMemoryBitemporalStore is the reference implementation. For persistence across process restarts, use the SQLite backend. Append-only is enforced at the database layer via triggers.

from briefcase.bitemporal.backends import SqliteBitemporalBackend
backend = SqliteBitemporalBackend("evidence.db")

For multi-writer analytics, the Iceberg backend wraps pyiceberg:

Terminal window
pip install briefcase-ai[bitemporal-iceberg]
from briefcase.bitemporal.backends import IcebergBitemporalBackend

All backends implement the same BitemporalStore protocol, so AsOfView and application code are backend-agnostic.

Key Classes

SymbolWhy it matters
BitemporalRecordImmutable record with valid_time, transaction_time, value, source; new() constructor, content_hash().
InMemoryBitemporalStoreReference store; append / append_many / latest / history / as_of / keys.
AsOfViewRead-only view clamped to transaction_time and/or valid_time — reconstructs a past belief.
append_correctionAppends a superseding record so the original belief is preserved, not overwritten.
batch_append / stream_appendShared-instant vs. per-record ingestion.
SqliteBitemporalBackend / IcebergBitemporalBackendDurable backends sharing the BitemporalStore protocol.

Where this fits