DEV Community

dumije
dumije

Posted on

I Got Tired of Losing Data History, So I Built a Python Library to Fix It. Meet Chrono Temporal (chrono-temporal)

What is Chrono Temporal (chrono-temporal)? A Practical Approach to Time-Travel Queries in PostgreSQL

Most applications treat data as a single, current state.
A user has one profile. A product has one price. A record gets updated… and the previous state is gone.
But in reality, data is not static, it evolves over time. And sometimes, the most important question is: What did this data look like before?

The Problem with Traditional Data Models

Let's say you update a product price:

pythonproduct.price = 599


Before the update:
Enter fullscreen mode Exit fullscreen mode

price = 999


After the update:
Enter fullscreen mode Exit fullscreen mode

price = 599


The previous value is gone. Now imagine trying to answer:

- What was the price on Black Friday?
- When exactly did this change happen?
- What else changed at the same time?

You'll typically need audit logs, history tables, triggers, or complex event sourcing setups. All of which add significant complexity to your codebase.

---

**Thinking in Timelines Instead of States**

Instead of storing only the *current state*, what if we store every state over time?

That means each change becomes part of a timeline:
Enter fullscreen mode Exit fullscreen mode

Jan 1 → price = 999

Feb 1 → price = 599

Mar 1 → price = 799
Now you can reconstruct past states, compare changes, and audit data naturally — without any extra infrastructure.

Introducing Chrono Temporal (chrono-temporal)

Chrono Temporal (chrono-temporal) is a Python library that adds time-travel queries to PostgreSQL. It allows you to query data at any point in history, track all historical changes, and compute diffs between any two states — all without rewriting your database architecture.
bashpip install chrono-temporal

Quick Start

from chrono_temporal import TemporalService, TemporalRecordCreate, get_engine, get_session
from datetime import datetime, timezone

engine = get_engine("postgresql+asyncpg://user:pass@localhost/mydb")
session_factory = get_session(engine)

async with session_factory() as session:
svc = TemporalService(session)

# Store a record
await svc.create(TemporalRecordCreate(
    entity_type="product",
    entity_id="prod_001",
    valid_from=datetime(2024, 1, 1, tzinfo=timezone.utc),
    data={"name": "iPhone 15 Pro", "price": 999}
))

# Query past state — what was the price on February 1st?
record = await svc.get_at_point_in_time(
    "product", "prod_001",
    datetime(2024, 2, 1, tzinfo=timezone.utc)
)
print(record[0].data)  # {"name": "iPhone 15 Pro", "price": 999}

# Get full price history
history = await svc.get_history("product", "prod_001")

# Compute diff — what changed between January and March?
diff = await svc.get_diff(
    "product", "prod_001",
    datetime(2024, 1, 1, tzinfo=timezone.utc),
    datetime(2024, 3, 1, tzinfo=timezone.utc),
)
print(diff)
Enter fullscreen mode Exit fullscreen mode

Example Diff Output
json{
"changed": {
"price": {
"from": 999,
"to": 599
}
},
"unchanged": ["name"],
"has_changes": true
}

This makes it easy to understand what changed, not just that something changed.

Why This Approach Matters

  1. Debugging Becomes Easier
    Instead of guessing what went wrong, you can inspect past states directly. No more "the data was correct before someone updated it and now we don't know what it was."

  2. Built-in Audit Trail
    Every change is naturally recorded as part of the timeline. SOX, GDPR, HIPAA — they all require audit trails. This gives you one for free.

  3. Better Data Integrity
    You never lose historical context when values change. Records are append-only — nothing ever gets deleted.

  4. Cleaner Than Manual Solutions
    No need to maintain separate audit tables, write trigger functions per table, or build event sourcing infrastructure from scratch. One pip install and you're done.

Real Use Case: Product Price History
I recently used chrono-temporal to build a product price tracking API, similar to how Amazon shows price history.

Instead of storing just the latest price, each update becomes part of a timeline. The API exposes endpoints like:

GET /products/{id}/as-of?as_of=2024-11-29 — price on Black Friday
GET /products/{id}/history — full price timeline
GET /products/{id}/lowest — cheapest price ever
GET /products/{id}/diff — what changed between two dates

Building this took about an hour using chrono-temporal as the data layer. Without it, I'd have needed custom audit tables, manual versioning logic, and bespoke query code for every endpoint.

When Should You Use This?
This approach is useful when:

Data changes frequently, and historical context matters
You need audit trails for compliance or debugging
You want to answer "what did this look like at date X?" from your application code

Examples include financial systems, SaaS subscription tracking, e-commerce pricing, contract versioning, HR and payroll systems, and feature flag history.

How It Works Under the Hood

Every entity is stored as a row in a temporal_records table with valid_from and valid_to timestamps. When a value changes, the old record is closed (valid_to is set) and a new record is created. Nothing is ever deleted. Time-travel queries filter by these timestamps to reconstruct state at any point in history.
The entire thing runs on standard PostgreSQL — no extensions, no special setup.

Final Thoughts
Most systems are designed around the idea of current state. But many real-world problems require understanding how data changes over time.
Thinking in timelines instead of states can simplify debugging, auditing, and reasoning about your data. chrono-temporal is an early attempt at making this pattern as easy as a pip install in Python.

Links

GitHub: https://github.com/Daniel7303/chrono-temporal
PyPI: https://pypi.org/project/chrono-temporal
Live API: https://chrono-temporal-ap.onrender.com/docs#/

Question for the community

How do you currently handle historical data in your systems? Audit tables, triggers, event sourcing or something else entirely? I'd love to hear what's working at scale.

Top comments (0)