A Clojure library that adapts Biff Web framework to use Datalevin as the database.
- System lifecycle management - Simple map-based component system inspired by Biff
- Database utilities - Connection management, transaction helpers, and query utilities
- Authentication - Password hashing with bcrypt, OAuth support (GitHub and generic providers)
- Session management - Datalevin-backed sessions with Ring session store
- Middleware - Authentication, CSRF protection, and request handling
Add to your deps.edn:
{:deps {io.github.datalevin/biff-datalevin {:git/tag "v0.1.0" :git/sha "..."}}}This library is designed to work as a drop-in Datalevin component for Biff applications:
(ns myapp.core
(:require [com.biffweb :as biff]
[biff.datalevin.core :as dl]
[biff.datalevin.db :as db]))
;; Use with Biff's start-system
(def initial-system
{:biff.datalevin/db-path "data/myapp"
:biff.datalevin/schema my-schema
;; ... other Biff config
})
(def components
[dl/use-datalevin ;; Adds :biff.datalevin/conn and :biff/db
;; ... other Biff components
])
;; use-datalevin sets both:
;; :biff.datalevin/conn - The Datalevin connection
;; :biff/db - Database snapshot (Biff compatibility)After transactions, refresh :biff/db to see new data:
(db/submit-tx ctx [{:user/id (UUID/randomUUID) :user/email "new@example.com"}])
(let [ctx (db/assoc-db ctx)] ;; Refresh :biff/db
(db/lookup ctx :user/email "new@example.com"))(ns myapp.core
(:require [biff.datalevin.core :as core]
[biff.datalevin.db :as db]
[biff.datalevin.auth :as auth]
[biff.datalevin.middleware :as mw]))
;; Define your schema
(def schema
{:user/id {:db/valueType :db.type/uuid :db/unique :db.unique/identity}
:user/email {:db/valueType :db.type/string :db/unique :db.unique/identity}
:user/password-hash {:db/valueType :db.type/string}})
;; Start the system
(def system
(core/start-system
{:biff.datalevin/db-path "data/myapp"
:biff.datalevin/schema schema}
[core/use-datalevin]))
;; Create a user
(let [user-tx (auth/create-user-tx {:user/email "user@example.com"
:password "secret123"})]
(db/submit-tx system [user-tx]))
;; Query users
(db/lookup system :user/email "user@example.com")
;; Stop the system
(core/stop-system system)System lifecycle management:
;; Start a system with components
(def system
(core/start-system
{:biff.datalevin/db-path "data/myapp"
:biff.datalevin/schema my-schema
:port 8080}
[core/use-datalevin
my-custom-component]))
;; Stop the system (calls cleanup functions in reverse order)
(core/stop-system system)
;; Add cleanup functions to a component
(defn my-component [system]
(let [resource (create-resource)]
(-> system
(assoc :my-resource resource)
(core/assoc-stop #(close-resource resource)))))Connection and query utilities:
;; Submit transactions with special values
(db/submit-tx system [{:user/id (java.util.UUID/randomUUID)
:user/email "new@example.com"
:user/created-at :db/now}]) ; :db/now -> current Date
;; Lookup single entity
(db/lookup system :user/email "user@example.com")
;; => {:user/id #uuid "...", :user/email "user@example.com", ...}
;; Lookup with custom pull expression
(db/lookup system :user/email "user@example.com" [:user/id :user/email])
;; Lookup all matching entities
(db/lookup-all system :user/role :admin)
;; Check existence
(db/entity-exists? system :user/email "user@example.com")
;; Run queries
(db/q '[:find ?e
:where [?e :user/role :admin]]
system)
;; Update entities
(db/submit-tx system [(db/merge-tx [:user/id user-id]
{:user/name "New Name"})])
;; Delete entities
(db/submit-tx system [(db/delete-tx [:user/id user-id])])Password and OAuth authentication:
;; Password hashing
(auth/hash-password "secret")
(auth/verify-password "secret" hash)
;; Create user with password
(let [user-tx (auth/create-user-tx {:user/email "user@example.com"
:user/username "myuser"
:password "secret123"})]
(db/submit-tx system [user-tx]))
;; Authenticate user
(auth/authenticate-user system "user@example.com" "secret123")
;; => {:user/id #uuid "...", :user/email "user@example.com", ...} or nil
;; GitHub OAuth
(auth/github-authorize-url
{:client-id "your-client-id"
:redirect-uri "http://localhost:8080/auth/github/callback"
:state "csrf-token"})
;; Exchange code for token
(let [token-response (auth/github-exchange-code
{:client-id "..."
:client-secret "..."
:code code
:redirect-uri "..."})]
(auth/github-get-user (:access_token token-response)))
;; Email verification tokens
(let [{:keys [token tx]} (auth/create-verification-token user-id)]
(db/submit-tx system [tx])
;; Send token to user via email...
)
;; Verify token
(auth/verify-token system token)
;; => user-id or nilDatalevin-backed session management:
;; Create a session
(let [{:keys [session-id tx]} (session/create-session user-id)]
(db/submit-tx system [tx])
session-id)
;; Get session with user data
(session/get-session system session-id)
;; => {:session/id ..., :session/user {:user/id ..., ...}, :session/expires-at ...}
;; Get just the user
(session/get-session-user system session-id)
;; Delete session
(when-let [delete-tx (session/delete-session-tx system session-id)]
(db/submit-tx system [delete-tx]))
;; JWT tokens for stateless auth
(def secret "your-32-byte-secret-key-here!!!")
(session/create-session-token session-id {:secret secret})
(session/verify-session-token token secret)
;; Ring session store
(require '[ring.middleware.session :refer [wrap-session]])
(-> handler
(wrap-session {:store (session/datalevin-session-store conn)}))Ring middleware stack:
;; Full site middleware (sessions, CSRF, auth)
(def handler
(-> my-routes
(mw/wrap-site-defaults
{:context {:biff.datalevin/conn conn}
:session-secret "your-32-byte-secret!!!!"
:csrf? true
:auth? true})))
;; API middleware (JWT auth, no CSRF)
(def api-handler
(-> my-api-routes
(mw/wrap-api-defaults
{:context {:biff.datalevin/conn conn}
:session-secret "your-32-byte-secret!!!!"})))
;; Require authentication
(-> handler
(mw/wrap-require-auth {:redirect "/login"}))
;; Require specific role
(-> handler
(mw/wrap-require-role {:role :admin :redirect "/forbidden"}))
;; CSRF token in forms
[:form {:method "post"}
(mw/csrf-input)
[:button "Submit"]]Recommended schema for common entities:
(def schema
{;; Users
:user/id {:db/valueType :db.type/uuid :db/unique :db.unique/identity}
:user/email {:db/valueType :db.type/string :db/unique :db.unique/identity}
:user/username {:db/valueType :db.type/string :db/unique :db.unique/identity}
:user/password-hash {:db/valueType :db.type/string}
:user/github-id {:db/valueType :db.type/long :db/unique :db.unique/identity}
:user/github-username {:db/valueType :db.type/string}
:user/avatar-url {:db/valueType :db.type/string}
:user/role {:db/valueType :db.type/keyword}
:user/created-at {:db/valueType :db.type/instant}
;; Sessions
:session/id {:db/valueType :db.type/uuid :db/unique :db.unique/identity}
:session/user {:db/valueType :db.type/ref}
:session/expires-at {:db/valueType :db.type/instant}
;; Verification tokens
:verification-token/token {:db/valueType :db.type/string :db/unique :db.unique/identity}
:verification-token/user {:db/valueType :db.type/ref}
:verification-token/expires-at {:db/valueType :db.type/instant}})This library is designed for Datalevin, which has some differences from Datomic/XTDB:
-
Entity creation: Don't use lookup refs as
:db/idfor new entities. Just include the unique attribute:;; Correct {:user/id (UUID/randomUUID) :user/email "user@example.com"} ;; Incorrect (won't work) {:db/id [:user/id some-uuid] :user/email "user@example.com"}
-
Entity updates: Use lookup refs as
:db/idfor updating existing entities:{:db/id [:user/id existing-uuid] :user/name "New Name"} -
References: Lookup refs like
[:user/id uuid]can be used for:db/valueType :db.type/refattributes, but the referenced entity must exist first. -
Retractions: Use entity IDs (numbers) for
:db/retractEntity, not lookup refs. The helper functions handle this automatically.
clj -M:testMIT License