On January second, 2026, Session 012 added the EntityVersion struct to FLIN's virtual machine. Fifty minutes. Ten tests. The first tentative step toward a temporal data model.
By January seventh, Session 088, the temporal model stood at one hundred fifty-two out of one hundred sixty tasks complete -- ninety-five percent. Ten out of eleven categories at one hundred percent. Over one thousand tests passing. A complete, production-ready system for automatic versioning, time-travel queries, soft and hard deletion, restoration, temporal filtering, comparison analytics, version metadata, time arithmetic, and bitemporal storage.
This article is the retrospective. Not the code, not the bugs, not the implementation details -- those are covered in the previous nine articles. This is about what we built, why it matters, and what it means for the future of application development.
The Final Scorecard
| Category | Tasks | Status |
|---|---|---|
| TEMP-1: Core Soft Delete | 5/5 | Complete |
| TEMP-2: Temporal Access (@) | 18/18 | Complete |
| TEMP-3: Temporal Keywords | 14/14 | Complete |
| TEMP-4: History Queries | 22/22 | Complete |
| TEMP-5: Time Arithmetic | 12/12 | Complete |
| TEMP-6: Temporal Comparisons | 10/10 | Complete |
| TEMP-7: Time-Based Filters | 15/15 | Complete |
| TEMP-8: Hard Delete/Restore | 12/12 | Complete |
| TEMP-9: Retention Policies | 0/10 | Deferred |
| TEMP-10: Bitemporal Storage | 20/20 | Complete |
| TEMP-11: Integration Tests | 27/27 | Complete |
| Total | 152/160 | 95% |
The only incomplete category is TEMP-9 (Retention Policies) -- an optimization feature for automatic cleanup of old versions. The ten remaining tasks are "nice to have" for production deployments but do not affect the core temporal capabilities. Every functional feature of the temporal model is implemented, tested, and working.
What FLIN's Temporal Model Includes
A complete inventory of temporal capabilities, built across twenty-six sessions:
Automatic Versioning
Every entity tracks its complete history automatically. No configuration, no annotations, no opt-in. Each save creates a new immutable version with a sequential version number and timestamp.
entity Product { name: text, price: number }product = Product { name: "Widget", price: 10 } save product // Version 1 product.price = 15 save product // Version 2 product.price = 20 save product // Version 3 ```
Time-Travel Queries (@ Operator) Three forms of point-in-time access: relative version numbers, absolute date strings, and temporal keywords.
product @ -1 // Previous version
product @ "2024-06-15" // State on June 15, 2024
product @ yesterday // State as of yesterday
product @ last_month // State 30 days agoHistory Property Full version timeline for any entity, iterable in templates.
{for ver in product.history}
<p>v{ver.version_number}: ${ver.price} at {ver.created_at}</p>
{/for}Temporal Filtering and Ordering Query-style operations on history lists.
expensive = product.history.where_field("price", ">", 15)
chronological = product.history.order_by("created_at", "asc")
combined = product.history
.where_field("price", ">=", 10)
.order_by("price", "desc")Soft Delete, Hard Delete, and Restore Three-tier deletion model with GDPR compliance built in.
delete product // Soft: history preserved, restorable
restore(product) // Undo soft delete
destroy product // Hard: permanent erasure, GDPR compliantTemporal Comparison Helpers Six native functions for change detection and analytics.
field_changed(product, "price") // true/false
calculate_delta(old_price, new_price) // numeric difference
percent_change(old_price, new_price) // percentage
changed_from(product, "price", 15.00) // was it 15 before?
field_history(product, "price") // [10, 15, 20]Version Metadata Every entity exposes lifecycle metadata as first-class properties.
product.id // Entity identifier
product.version_number // Current version
product.created_at // Creation timestamp
product.updated_at // Last modification
product.deleted_at // Deletion timestamp (optional)Time Arithmetic Duration literals and type-safe time operations.
deadline = now + 7.days
reminder = deadline - 1.hours
time_left = deadline - now
cache_ttl = 5.minutesBitemporal Storage
Every version has valid_from and valid_to timestamps, enabling efficient range queries and point-in-time lookups.
History Persistence Version history persists to disk and survives application restarts. WAL-based storage ensures durability without sacrificing write performance.
What No Other Language Has
We surveyed every major programming language and framework before and during FLIN's development. None offer what FLIN's temporal model provides as a language-level feature.
SQL Databases: Temporal Tables PostgreSQL, MariaDB, and SQL Server support temporal tables (also called system-versioned tables). These track row history automatically, similar to FLIN's approach. But they are a database feature, not a language feature. The developer must:
1. Create the temporal table with specific syntax.
2. Write SQL queries with FOR SYSTEM_TIME AS OF clauses.
3. Handle the temporal columns in application code.
4. Build UI components to display history.
5. Implement soft delete, hard delete, and restore separately.
FLIN integrates temporal capabilities into the language itself. The @ operator, .history property, and comparison helpers are not SQL extensions -- they are native syntax.
Event Sourcing Frameworks Event sourcing (Axon, EventStore, Marten) stores events and reconstructs state by replaying them. This is powerful for certain domains but fundamentally different from FLIN's approach:
- Event sourcing requires defining events, handlers, projections, and sagas. FLIN requires nothing -- temporal tracking is automatic.
- Querying a past state in event sourcing means replaying events up to that point. In FLIN, it is a single indexed lookup.
- Event sourcing is a pattern, not a language feature. It requires framework adoption and architectural commitment. FLIN's temporal model works on every entity, always.
Immutable Database Systems Datomic and XTDB (formerly Crux) are immutable databases that store all historical states. They are the closest conceptual match to FLIN's temporal model. But they are database systems, not programming languages. The developer still writes application code in Clojure (for Datomic) or any JVM language (for XTDB), with temporal queries expressed as database queries rather than language expressions.
FLIN makes product @ yesterday a language expression, type-checked at compile time, with the result usable directly in templates and business logic. No query language boundary. No ORM translation layer.
Audit Trail Libraries Every framework has audit trail gems, packages, and plugins (PaperTrail for Rails, django-simple-history for Python, Javers for Java). These bolt on versioning at the application layer. They are useful but limited:
- They require per-model configuration.
- They store diffs or snapshots in separate tables.
- They provide read-only audit functionality, not general-purpose time-travel.
- They do not integrate with the type system, compiler, or view layer.
FLIN's temporal model is not an add-on. It is the data model.
The Journey in Numbers
| Metric | Value |
|---|---|
| Sessions | 26 (012 through 088) |
| Calendar days | 6 (January 2-7, 2026) |
| Tasks completed | 152 out of 160 |
| Categories at 100% | 10 out of 11 |
| Integration tests | 36 (all passing) |
| Library tests | 1,011 (all passing) |
| Lines of temporal code | ~2,500 |
| Bugs found and fixed | 8 major, dozens minor |
| New VM opcodes | 4 (AtVersion, AtTime, AtDate, History) + 2 (ListFilterField, ListOrderBy) |
| New native functions | 6 (comparison helpers) |
| New AST nodes | 2 (Expr::Temporal, Expr::Duration) |
| New types | 2 (FlinType::Duration, DurationUnit enum) |
Six days. Twenty-six sessions. A complete temporal data model woven into every layer of a programming language, from lexer to database.
The Architecture That Made It Possible
FLIN's layered architecture -- lexer, parser, type checker, code generator, VM, database -- was both the challenge and the enabler of the temporal model.
The challenge: temporal features required changes at every layer. The @ operator needed a token in the lexer, a parsing rule in the parser, type checking logic, bytecode emission, VM execution, and database queries. A bug in any layer produced confusing symptoms in another.
The enabler: the clean separation between layers meant that each piece could be built and tested independently. The lexer tokens were defined before the parser rules. The parser rules were defined before the type checker logic. The type checker logic was defined before the code generator. This layered approach allowed parallel development and incremental progress -- Session 012 built the VM layer while the parser and type checker were still catching up.
The architecture also meant that temporal features composed naturally with existing language features. The @ operator returns an entity, so it works with field access, conditionals, loops, and function calls. The .history property returns a list, so it works with .where_field(), .order_by(), .count, and {for} loops. Duration literals are integers at runtime, so they work with existing arithmetic opcodes.
No special cases. No "temporal mode." Just expressions that return values, composed through the same mechanisms as every other expression in the language.
What We Would Do Differently
Start With Integration Tests
The biggest time sink was the debugging marathon of Sessions 068-076. We built temporal features layer by layer, tested each layer in isolation, and assumed the end-to-end flow worked. It did not. The AtTime stub was the most embarrassing example: a feature that compiled, executed, and returned a value -- just the wrong value.
If we had written end-to-end integration tests from the start -- FLIN source code in, HTML output out -- we would have caught the stub immediately. The temporal model would have been complete in half the sessions.
Track at the Task Level, Not the Category Level
Our tracking document recorded category-level percentages but not individual task completion. This made it impossible to tell whether "TEMP-2 at 89%" meant "sixteen tasks done, two remaining" or "most things work but we are not sure which two do not." Task-level tracking would have prevented the six-task discovery in Session 079.
Define Semantics Before Implementation
The history duplication bug (Session 075) existed because we never explicitly defined who was responsible for including the current version in the .history result. Was it the database? The VM? Both? The ambiguity led to both doing it, causing duplication.
A one-paragraph semantic specification -- "the database stores past versions only; the VM appends the current version when constructing .history results" -- would have prevented the bug entirely.
The Remaining Five Percent
TEMP-9 (Retention Policies) accounts for the remaining ten tasks. These are optimization features for production deployments:
// Planned syntax
entity Metric {
value: number@retention(90.days) // Keep 90 days of history }
entity Metric { value: number
@compact(after: 30.days, to: "daily") // Compact old data } ```
Retention policies automatically prune old versions to manage storage growth. Compaction strategies merge fine-grained historical data into coarser summaries (daily, weekly, monthly). Both are important for long-running production systems with high-write entities.
We deferred these features deliberately. The temporal model at ninety-five percent is production-ready for the applications FLIN targets. Retention policies are an optimization for scale, not a prerequisite for functionality. When a FLIN application starts generating enough historical data to warrant automatic cleanup, the primitives for implementing retention are already in place (the prune_versions_before and prune_versions_keep_last methods from Session 012).
What This Means for Developers
FLIN's temporal model changes the economics of application development. Features that previously required dedicated infrastructure -- audit trails, undo functionality, compliance reporting, price history, activity logs, data recovery -- are now free. Not "free as in low cost." Free as in zero additional code.
A developer building an e-commerce platform in FLIN gets price history, order audit trails, and user activity logs without writing a single line of tracking code. A developer building a document management system gets version history, change detection, and rollback without an event sourcing framework. A developer building a regulated application gets compliance-grade audit trails without a separate audit database.
This is not a marginal improvement. This is a category shift. The temporal model eliminates an entire class of application infrastructure. It does for data history what garbage collection did for memory management: removes the burden from the developer and handles it automatically, correctly, and efficiently.
"E flin nu" -- It Remembers Things
The phrase "E flin nu" comes from Fon, a language spoken in Benin. It means "it remembers things." It became the motto of FLIN's temporal model during development, a reminder of what we were building: a language where data remembers.
Not data that can be configured to remember. Not data that remembers if you install the right plugin. Not data that remembers if you set up triggers and history tables and audit middleware.
Data that remembers because that is what data does in FLIN. Always. Automatically. From the first save to the last destroy.
One hundred fifty-two tasks. Ten categories. Six days. And a programming language where every entity remembers everything.
---
This is Part 10 of the "How We Built FLIN" temporal model series. The complete series documents the design, implementation, debugging, and completion of FLIN's temporal data model -- a feature that no other programming language offers as a language primitive.
Series Navigation: - [046] Every Entity Remembers Everything: The Temporal Model - [047] Version History and Time Travel Queries - [048] Temporal Integration: From Bugs to 100% Test Coverage - [049] Destroy and Restore: Soft Deletes Done Right - [050] Temporal Filtering and Ordering - [051] Temporal Comparison Helpers - [052] Version Metadata Access - [053] Time Arithmetic: Adding Days, Comparing Dates - [054] Tracking Accuracy and Validation - [055] The Temporal Model Complete: What No Other Language Has (you are here)