Skip to content

Bi-Temporal Model ​

contextdb tracks two independent time dimensions for every node and edge.

Two kinds of time ​

Bi-Temporal Explorer

Drag the slider to travel through time and see what the system knew vs. what was actually true.

Valid Time (world truth)Transaction Time (system learned)Retracted
JanFebMarAprMayJun
Valid Time
Tx Time
Jan 1
What was true at Jan 1
API limit: 100 req/s
valid Jan 1 → Mar 15
Office location: Building A
valid Jan 1 → May 1 · retracted: incorrect — building was B
What the system knew at Jan 1
Office location: Building A
system learned Jan 1
DimensionFieldMeaning
Valid timeValidFrom / ValidUntilWhen the fact was true in the world
Transaction timeTxTimeWhen the system learned the fact

These are independent. You can record a historical fact (valid_time in the past) that the system just learned (transaction_time = now).

Why it matters ​

Scenario: Your crawler discovers on March 20th that the API rate limit was changed to 500 req/s on March 15th.

go
ns.Write(ctx, client.WriteRequest{
    Content:    "API rate limit is 500 req/s",
    SourceID:   "config-crawler",
    Vector:     embed("API rate limit"),
    ValidFrom:  time.Date(2025, 3, 15, 0, 0, 0, 0, time.UTC),
    // TxTime is automatically set to now (March 20th)
})

Now queries understand that:

  • Before March 15th: the limit was 100 req/s
  • After March 15th: the limit is 500 req/s
  • Before March 20th: the system didn't know about the change

How this compares

Most vector databases have a single created_at timestamp or none at all. PostgreSQL supports bi-temporal queries but requires manual schema design and application-level enforcement. contextdb makes bi-temporal semantics automatic — every write gets both timestamps, every query respects them.

Temporal diff queries ​

Use the Diff API to see what changed between two points in time:

go
// What changed in the last 24 hours?
diffs, _ := graph.Diff(ctx, "my-app", yesterday, now)
for _, d := range diffs {
    fmt.Printf("[%s] %s (v%d)\n", d.Change, core.NodeText(d.Node), d.Node.Version)
}
// [added] New team member joined (v1)
// [modified] Sprint deadline updated (v3)
// [removed] Old standup cancelled (v2)

Time-travel queries ​

Use ValidAt to reconstruct the full state of the system as of any past moment:

go
// What did the system believe on June 1st?
snapshot, _ := graph.ValidAt(ctx, "my-app", june1st, nil)
fmt.Printf("System held %d beliefs on June 1st\n", len(snapshot))

Point-in-time queries ​

Use ValidAt and KnownAt to pin the two independent time axes:

go
// Which facts were valid on January 10th, using only evidence recorded by January 11th?
results, _ := ns.Retrieve(ctx, client.RetrieveRequest{
    Vector: embed("API rate limit"),
    TopK:   1,
    ValidAt: time.Date(2025, 1, 10, 0, 0, 0, 0, time.UTC),
    KnownAt: time.Date(2025, 1, 11, 0, 0, 0, 0, time.UTC),
})

ValidAt asks when a claim was true; KnownAt asks what had been recorded by then. A fact backfilled today with ValidFrom in January appears in a January ValidAt query with the default current KnownAt, but is excluded when KnownAt is also in January. AsOf remains a compatibility shorthand for ValidAt; an explicit ValidAt takes precedence. An omitted axis defaults to now. A correction or retraction recorded by KnownAt supersedes the earlier version even if its earlier version was valid at ValidAt.

Edge edits now retain versions as well. A historical graph walk selects the latest edge version known at KnownAt, including its endpoints, weight, and invalidation state. Postgres migration 003 seeds one version for each edge already present; it cannot reconstruct edge edits that happened before the migration. New edits and invalidations are retained from that point forward.

Node validity ​

Nodes expose IsValidAt(t) for temporal filtering:

go
node.ValidFrom   // when the fact became true
node.ValidUntil  // when the fact stopped being true (nil = still valid)
node.TxTime      // when the system recorded this

node.IsValidAt(time.Now()) // true if currently valid

Version history ​

Version History: API Rate Limit

How a single fact evolves through transaction time.

v1 / v5
v1tx: Jan 5
100 req/s
confidence
95%

Track how a fact evolved over time:

go
versions, _ := ns.History(ctx, nodeID)
for _, v := range versions {
    fmt.Printf("v%d  valid_from=%s  tx=%s  text=%s\n",
        v.Version, v.ValidFrom, v.TxTime, v.Properties["text"])
}

Temporal edges ​

Edges also carry temporal fields:

go
edge := core.Edge{
    Src:       nodeA,
    Dst:       nodeB,
    Type:      "supercedes",
    ValidFrom: time.Now(),
    // ValidUntil: nil means permanently valid
    // InvalidatedAt: for non-destructive logical deletion
}

Edges have IsActiveAt(t) which checks both validity window and logical deletion.

Released under the MIT License.