{"id":42811083,"url":"https://github.com/datalevin/biff-datalevin","last_synced_at":"2026-01-30T05:01:07.169Z","repository":{"id":333956015,"uuid":"1139340962","full_name":"datalevin/biff-datalevin","owner":"datalevin","description":"Use Datalevin in Biff Web framework","archived":false,"fork":false,"pushed_at":"2026-01-22T00:44:20.000Z","size":29,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-22T13:53:35.820Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/datalevin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-21T20:47:25.000Z","updated_at":"2026-01-22T00:44:23.000Z","dependencies_parsed_at":"2026-01-22T14:04:02.573Z","dependency_job_id":null,"html_url":"https://github.com/datalevin/biff-datalevin","commit_stats":null,"previous_names":["datalevin/biff-datalevin"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/datalevin/biff-datalevin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datalevin%2Fbiff-datalevin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datalevin%2Fbiff-datalevin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datalevin%2Fbiff-datalevin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datalevin%2Fbiff-datalevin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/datalevin","download_url":"https://codeload.github.com/datalevin/biff-datalevin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datalevin%2Fbiff-datalevin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28904583,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-30T04:02:34.702Z","status":"ssl_error","status_checked_at":"2026-01-30T04:02:33.562Z","response_time":66,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-30T05:00:42.670Z","updated_at":"2026-01-30T05:01:07.160Z","avatar_url":"https://github.com/datalevin.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# biff-datalevin\n\nA Clojure library that adapts [Biff](https://biffweb.com/) Web framework to use [Datalevin](https://github.com/juji-io/datalevin) as the database.\n\n## Features\n\n- **System lifecycle management** - Simple map-based component system inspired by Biff\n- **Database utilities** - Connection management, transaction helpers, and query utilities\n- **Authentication** - Password hashing with bcrypt, OAuth support (GitHub and generic providers)\n- **Session management** - Datalevin-backed sessions with Ring session store\n- **Middleware** - Authentication, CSRF protection, and request handling\n\n## Installation\n\nAdd to your `deps.edn`:\n\n```clojure\n{:deps {io.github.datalevin/biff-datalevin {:git/tag \"v0.1.0\" :git/sha \"...\"}}}\n```\n\n## Biff Integration\n\nThis library is designed to work as a drop-in Datalevin component for Biff applications:\n\n```clojure\n(ns myapp.core\n  (:require [com.biffweb :as biff]\n            [biff.datalevin.core :as dl]\n            [biff.datalevin.db :as db]))\n\n;; Use with Biff's start-system\n(def initial-system\n  {:biff.datalevin/db-path \"data/myapp\"\n   :biff.datalevin/schema my-schema\n   ;; ... other Biff config\n   })\n\n(def components\n  [dl/use-datalevin  ;; Adds :biff.datalevin/conn and :biff/db\n   ;; ... other Biff components\n   ])\n\n;; use-datalevin sets both:\n;;   :biff.datalevin/conn - The Datalevin connection\n;;   :biff/db             - Database snapshot (Biff compatibility)\n```\n\nAfter transactions, refresh `:biff/db` to see new data:\n\n```clojure\n(db/submit-tx ctx [{:user/id (UUID/randomUUID) :user/email \"new@example.com\"}])\n(let [ctx (db/assoc-db ctx)]  ;; Refresh :biff/db\n  (db/lookup ctx :user/email \"new@example.com\"))\n```\n\n## Quick Start\n\n```clojure\n(ns myapp.core\n  (:require [biff.datalevin.core :as core]\n            [biff.datalevin.db :as db]\n            [biff.datalevin.auth :as auth]\n            [biff.datalevin.middleware :as mw]))\n\n;; Define your schema\n(def schema\n  {:user/id {:db/valueType :db.type/uuid :db/unique :db.unique/identity}\n   :user/email {:db/valueType :db.type/string :db/unique :db.unique/identity}\n   :user/password-hash {:db/valueType :db.type/string}})\n\n;; Start the system\n(def system\n  (core/start-system\n    {:biff.datalevin/db-path \"data/myapp\"\n     :biff.datalevin/schema schema}\n    [core/use-datalevin]))\n\n;; Create a user\n(let [user-tx (auth/create-user-tx {:user/email \"user@example.com\"\n                                     :password \"secret123\"})]\n  (db/submit-tx system [user-tx]))\n\n;; Query users\n(db/lookup system :user/email \"user@example.com\")\n\n;; Stop the system\n(core/stop-system system)\n```\n\n## Modules\n\n### Core (`biff.datalevin.core`)\n\nSystem lifecycle management:\n\n```clojure\n;; Start a system with components\n(def system\n  (core/start-system\n    {:biff.datalevin/db-path \"data/myapp\"\n     :biff.datalevin/schema my-schema\n     :port 8080}\n    [core/use-datalevin\n     my-custom-component]))\n\n;; Stop the system (calls cleanup functions in reverse order)\n(core/stop-system system)\n\n;; Add cleanup functions to a component\n(defn my-component [system]\n  (let [resource (create-resource)]\n    (-\u003e system\n        (assoc :my-resource resource)\n        (core/assoc-stop #(close-resource resource)))))\n```\n\n### Database (`biff.datalevin.db`)\n\nConnection and query utilities:\n\n```clojure\n;; Submit transactions with special values\n(db/submit-tx system [{:user/id (java.util.UUID/randomUUID)\n                       :user/email \"new@example.com\"\n                       :user/created-at :db/now}])  ; :db/now -\u003e current Date\n\n;; Lookup single entity\n(db/lookup system :user/email \"user@example.com\")\n;; =\u003e {:user/id #uuid \"...\", :user/email \"user@example.com\", ...}\n\n;; Lookup with custom pull expression\n(db/lookup system :user/email \"user@example.com\" [:user/id :user/email])\n\n;; Lookup all matching entities\n(db/lookup-all system :user/role :admin)\n\n;; Check existence\n(db/entity-exists? system :user/email \"user@example.com\")\n\n;; Run queries\n(db/q '[:find ?e\n        :where [?e :user/role :admin]]\n      system)\n\n;; Update entities\n(db/submit-tx system [(db/merge-tx [:user/id user-id]\n                                    {:user/name \"New Name\"})])\n\n;; Delete entities\n(db/submit-tx system [(db/delete-tx [:user/id user-id])])\n```\n\n### Authentication (`biff.datalevin.auth`)\n\nPassword and OAuth authentication:\n\n```clojure\n;; Password hashing\n(auth/hash-password \"secret\")\n(auth/verify-password \"secret\" hash)\n\n;; Create user with password\n(let [user-tx (auth/create-user-tx {:user/email \"user@example.com\"\n                                     :user/username \"myuser\"\n                                     :password \"secret123\"})]\n  (db/submit-tx system [user-tx]))\n\n;; Authenticate user\n(auth/authenticate-user system \"user@example.com\" \"secret123\")\n;; =\u003e {:user/id #uuid \"...\", :user/email \"user@example.com\", ...} or nil\n\n;; GitHub OAuth\n(auth/github-authorize-url\n  {:client-id \"your-client-id\"\n   :redirect-uri \"http://localhost:8080/auth/github/callback\"\n   :state \"csrf-token\"})\n\n;; Exchange code for token\n(let [token-response (auth/github-exchange-code\n                       {:client-id \"...\"\n                        :client-secret \"...\"\n                        :code code\n                        :redirect-uri \"...\"})]\n  (auth/github-get-user (:access_token token-response)))\n\n;; Email verification tokens\n(let [{:keys [token tx]} (auth/create-verification-token user-id)]\n  (db/submit-tx system [tx])\n  ;; Send token to user via email...\n  )\n\n;; Verify token\n(auth/verify-token system token)\n;; =\u003e user-id or nil\n```\n\n### Sessions (`biff.datalevin.session`)\n\nDatalevin-backed session management:\n\n```clojure\n;; Create a session\n(let [{:keys [session-id tx]} (session/create-session user-id)]\n  (db/submit-tx system [tx])\n  session-id)\n\n;; Get session with user data\n(session/get-session system session-id)\n;; =\u003e {:session/id ..., :session/user {:user/id ..., ...}, :session/expires-at ...}\n\n;; Get just the user\n(session/get-session-user system session-id)\n\n;; Delete session\n(when-let [delete-tx (session/delete-session-tx system session-id)]\n  (db/submit-tx system [delete-tx]))\n\n;; JWT tokens for stateless auth\n(def secret \"your-32-byte-secret-key-here!!!\")\n(session/create-session-token session-id {:secret secret})\n(session/verify-session-token token secret)\n\n;; Ring session store\n(require '[ring.middleware.session :refer [wrap-session]])\n\n(-\u003e handler\n    (wrap-session {:store (session/datalevin-session-store conn)}))\n```\n\n### Middleware (`biff.datalevin.middleware`)\n\nRing middleware stack:\n\n```clojure\n;; Full site middleware (sessions, CSRF, auth)\n(def handler\n  (-\u003e my-routes\n      (mw/wrap-site-defaults\n        {:context {:biff.datalevin/conn conn}\n         :session-secret \"your-32-byte-secret!!!!\"\n         :csrf? true\n         :auth? true})))\n\n;; API middleware (JWT auth, no CSRF)\n(def api-handler\n  (-\u003e my-api-routes\n      (mw/wrap-api-defaults\n        {:context {:biff.datalevin/conn conn}\n         :session-secret \"your-32-byte-secret!!!!\"})))\n\n;; Require authentication\n(-\u003e handler\n    (mw/wrap-require-auth {:redirect \"/login\"}))\n\n;; Require specific role\n(-\u003e handler\n    (mw/wrap-require-role {:role :admin :redirect \"/forbidden\"}))\n\n;; CSRF token in forms\n[:form {:method \"post\"}\n (mw/csrf-input)\n [:button \"Submit\"]]\n```\n\n## Schema Reference\n\nRecommended schema for common entities:\n\n```clojure\n(def schema\n  {;; Users\n   :user/id           {:db/valueType :db.type/uuid :db/unique :db.unique/identity}\n   :user/email        {:db/valueType :db.type/string :db/unique :db.unique/identity}\n   :user/username     {:db/valueType :db.type/string :db/unique :db.unique/identity}\n   :user/password-hash {:db/valueType :db.type/string}\n   :user/github-id    {:db/valueType :db.type/long :db/unique :db.unique/identity}\n   :user/github-username {:db/valueType :db.type/string}\n   :user/avatar-url   {:db/valueType :db.type/string}\n   :user/role         {:db/valueType :db.type/keyword}\n   :user/created-at   {:db/valueType :db.type/instant}\n\n   ;; Sessions\n   :session/id        {:db/valueType :db.type/uuid :db/unique :db.unique/identity}\n   :session/user      {:db/valueType :db.type/ref}\n   :session/expires-at {:db/valueType :db.type/instant}\n\n   ;; Verification tokens\n   :verification-token/token {:db/valueType :db.type/string :db/unique :db.unique/identity}\n   :verification-token/user {:db/valueType :db.type/ref}\n   :verification-token/expires-at {:db/valueType :db.type/instant}})\n```\n\n## Important Notes\n\n### Datalevin vs. Datomic/XTDB\n\nThis library is designed for Datalevin, which has some differences from Datomic/XTDB:\n\n1. **Entity creation**: Don't use lookup refs as `:db/id` for new entities. Just include the unique attribute:\n   ```clojure\n   ;; Correct\n   {:user/id (UUID/randomUUID) :user/email \"user@example.com\"}\n\n   ;; Incorrect (won't work)\n   {:db/id [:user/id some-uuid] :user/email \"user@example.com\"}\n   ```\n\n2. **Entity updates**: Use lookup refs as `:db/id` for updating existing entities:\n   ```clojure\n   {:db/id [:user/id existing-uuid] :user/name \"New Name\"}\n   ```\n\n3. **References**: Lookup refs like `[:user/id uuid]` can be used for `:db/valueType :db.type/ref` attributes, but the referenced entity must exist first.\n\n4. **Retractions**: Use entity IDs (numbers) for `:db/retractEntity`, not lookup refs. The helper functions handle this automatically.\n\n## Testing\n\n```bash\nclj -M:test\n```\n\n## License\n\nMIT License\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatalevin%2Fbiff-datalevin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatalevin%2Fbiff-datalevin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatalevin%2Fbiff-datalevin/lists"}