somewhat serious

Why Datomic: A Small Example

I've been casually working on a niche job board and leveraging the experience to learn primarily about Datomic and building a web application without JavaScript.

This short post focuses on the former, Datomic.

Background

I'm implementing sessions. The OWASP Cheat Sheet on sessions gives a good overview, so I will not.

My initial model of sessions only specified a single fact for time bound for inactivity. However, I had a hunch this was not sufficient, but I pressed on with building out session logic. After a bout of sickness and coming back to the project, I decided to take a step back and read about NIST's prescriptions for sessions. NIST prescribes two time-based facts associated with a session identifier: inactivity and reauthenticate. So I had to rework my data model... or so I though.

Datomic, A Beacon Of Light In a Dark World

For sessions I'm leveraging DataScript, which implements Datomic's API's, with some slightly different idioms. However, the concepts between the two port well.

My initial schema for anything ephemeral (sessions, passcodes, etc.) contains a few facts:

{:purpose {:db/cardinality :db.cardinality/one
           :db/index true}
 :identity {:db/cardinality :db.cardinality/one
            :db/unique :db.unique/identity
            :db/index true}
 :expiration {:db/cardinality :db.cardinality/one
              :db/index true}
 :associations {:db/cardinality :db.cardinality/many
                :db/valueType :db.type/ref}}

The above specifies: * purpose - what the fact is intended for * identity - a unique string which is used as means of identity for an entity * expiration - time value * associations - associated entities for a given entity

The intention with the above was to assert (i.e., create) session entities that have a single expiration associated with them, like so:

(transact connection
  [{:db/id -100
    :identity "unique string"
    :purpose :session
    :expiration #inst "2026..."
    :associations [{:db/id -101
                    :identity "some@email.com"
                    :purpose :email}]}])

Upon learning of NIST's recommendation, I initially thought "oh dear... I need to rework and now manage separate time facts". However, I was struck with the reality: purpose and expiration can be composed as an associated entity rather than needing to rework the schema, which would be necessary for a SQL database. Changing to the following only required an update to a couple queries:

(transact connection
  [{:db/id -100
    :identity "unique string"
    :purpose :session
    :associations
    [{:db/id -101
      :identity "some@email.com"
      :purpose :email}
     {:db/id -102
      :expiration #inst "2026..."
      :purpose :inactive}
     {:db/id -103
      :expiration #inst "2026..."
      :purpose :reauthenticate}]}])

Now the session model supports the kinds of time prescribed by NIST and most importantly -- at least in this small scenario -- the change was legible and cheap.

Implications "In The Large"

If this was a system that was a component in a larger system the previous model can still be supported while also supporting the new model. Teams can rely on the past and the present. Which leads to a nice property: datalog becomes the interface, not some other intermediary like a JSON API (or similar).

There was no need for a migration: defining new tables, new relations, potentially dropping old columns which some folks still rely on... essentially, in a production system this change is low risk, relative to the same information being conveyed by a SQL based system, which therefore is high risk.

In Sum

The world Datomic asserts is an objectively better one. It is not a panacea, there are sharp edges, but they are legible. Datomic, and its sister project like DataScript, have such well-defined semantics the sharp edges are reasonable. They're tersely documented because the semantics and model established do not require a combinatorial explosion of preconditions which must be evaluated to have an idea on how the system may be behaving.