{:deps {us.whitford/fulcro-rad-git {:git/url "https://github.com/michaelwhitford/fulcro-rad-git"
:git/sha "LATEST_SHA"}}}
A database adapter plugin for Fulcro RAD that uses Git as the persistence layer. Entities are EDN files, transactions are git commits, and history is built in.
Entities stored as EDN files in typed subdirectories
Each save/delete is an isolated git commit (git commit --only)
Automatic resolver generation from RAD attributes — Pathom 2 by default, Pathom 3 optional
Save and delete middleware for RAD forms
Entity history via git log
Content search via git grep
Bare repository support with worktree operations
Conversation-style storage (each conversation = branch/worktree)
Worktree forking from arbitrary commits
Hybrid engine — JGit in-process for most read/write operations (fast, no
subprocess), with the git CLI as a fallback and for worktree operations
JGit 7.x does not support
Git 2.17+ installed and on PATH (for worktree support)
Clojure 1.12+
A Pathom parser — Pathom 2 (default) or Pathom 3. Pathom is not bundled by
this library; add com.wsscode/pathom (2) or com.wsscode/pathom3 to your
own project.
Add to your deps.edn:
{:deps {us.whitford/fulcro-rad-git {:git/url "https://github.com/michaelwhitford/fulcro-rad-git"
:git/sha "LATEST_SHA"}}}
Or use a local checkout:
{:deps {us.whitford/fulcro-rad-git {:local/root "/path/to/fulcro-rad-git"}}}
(ns com.example.model.memory
(:require
[com.fulcrologic.rad.attributes :as attr :refer [defattr]]
[us.whitford.fulcro.rad.database-adapters.git-options :as gto]))
(defattr id :memory/id :uuid
{::attr/schema :knowledge
::attr/identity? true})
(defattr title :memory/title :string
{::attr/schema :knowledge
::attr/identities #{:memory/id}
::attr/required? true})
(defattr content :memory/content :string
{::attr/schema :knowledge
::attr/identities #{:memory/id}})
(def attributes [id title content])
(ns com.example.components.database
(:require
[us.whitford.fulcro.rad.database-adapters.git :as git]
[us.whitford.fulcro.rad.database-adapters.git-options :as gto]
[com.example.model.memory :as memory]))
(defonce repositories (atom {}))
(defn start! []
(reset! repositories
(git/start-repositories
{::gto/repositories
{:knowledge {:dir "data/knowledge"
:auto-init? true}}}
{:attributes memory/attributes})))
(defn stop! []
(git/stop-repositories @repositories)
(reset! repositories {}))
The adapter is pathom-version-neutral. generate-resolvers produces
Pathom 2 resolvers by default (the Fulcro ecosystem default). Pathom 3 is
opt-in and loaded lazily — it is only touched when pathom3 is on the classpath.
generate-resolvers returns plain Pathom 2 resolver maps. Register them with
your Pathom 2 parser (for example, Fulcro RAD’s own parser) and add wrap-env
to your env-middleware chain.
(ns com.example.components.parser
(:require
[us.whitford.fulcro.rad.database-adapters.git :as git]
[com.example.model.memory :as memory]
[com.example.components.database :as db]))
(def all-attributes
(vec (concat memory/attributes)))
;; Plain Pathom 2 resolver maps
(def auto-resolvers
(git/generate-resolvers all-attributes :knowledge))
;; Env wrapper that injects repos (works with Pathom 2 and 3)
(def wrap-env
(git/wrap-env (fn [_env] @db/repositories)))
Use generate-resolvers-pathom3 to emit Pathom 3 Resolver objects, then
register them with pci/register. Requires com.wsscode/pathom3 on the
classpath.
(ns com.example.components.parser
(:require
[us.whitford.fulcro.rad.database-adapters.git :as git]
[com.wsscode.pathom3.connect.indexes :as pci]
[com.example.model.memory :as memory]
[com.example.components.database :as db]))
(def all-attributes
(vec (concat memory/attributes)))
(def auto-resolvers
(git/generate-resolvers-pathom3 all-attributes :knowledge))
(def indexes
(pci/register auto-resolvers))
;; Env wrapper that injects repos
(def wrap-env
(git/wrap-env (fn [_env] @db/repositories)))
(ns com.example.components.middleware
(:require
[us.whitford.fulcro.rad.database-adapters.git :as git]))
;; Terminal middleware (no handler to wrap)
(def save-middleware (git/wrap-git-save))
(def delete-middleware (git/wrap-git-delete))
;; Or wrapping another handler
(def save-middleware
(git/wrap-git-save base-handler))
For conversation-style storage where each conversation is an independent branch:
(git/start-repositories
{::gto/repositories
{:conversations {:dir "data/conversations.git"
:bare? true
:worktrees? true
:worktree-dir "worktrees"}}})
;; Create a new conversation (worktree + branch)
(git/add-worktree! repo-dir "/path/to/worktrees/conv-123" "conv-123")
;; => {:success true, :path "/abs/path/to/worktrees/conv-123", :branch "conv-123"}
;; Fork a conversation from a specific point
(git/fork-worktree! repo-dir "/path/to/worktrees/conv-456" "conv-456" "abc1234")
;; => {:success true, :path "...", :branch "conv-456"}
;; Write and commit within a worktree
(spit "/path/to/worktrees/conv-123/message.edn" (pr-str {:role :user :text "Hello"}))
(git/worktree-commit! "/path/to/worktrees/conv-123" "/path/to/worktrees/conv-123/message.edn" "user message")
;; => {:success true, :commit "[conv-123 abc1234] user message"}
;; List all worktrees
(git/list-worktrees repo-dir)
;; => [{:path "..." :head "abc1234" :branch "refs/heads/conv-123"} ...]
;; Remove a worktree (branch history preserved)
(git/remove-worktree! repo-dir "/path/to/worktrees/conv-123")
;; => {:success true}
;; Clean up stale references
(git/prune-worktrees! repo-dir)
;; Garbage collect to reclaim space
(git/gc! repo-dir)
(git/gc! repo-dir {:aggressive? true})
start-repositoriesInitialize all git repos from config map
stop-repositoriesShut down all repos (no-op for git)
start-repository!Start a single repository (normal or bare)
stop-repository!Stop a single repository
wrap-envPathom environment middleware to inject repository references (works with Pathom 2 and 3)
generate-resolversAuto-generate Pathom 2 resolvers from RAD attributes for a schema (default)
generate-resolvers-pathom3Auto-generate Pathom 3 resolvers (requires pathom3 on the classpath; loaded lazily)
wrap-git-saveSave middleware: delta → EDN write → git commit
wrap-git-deleteDelete middleware: file remove → git commit
entity-pathBuild file path for an entity: dir/entity-dir/id.edn
read-entityRead an EDN entity file, returns map or nil
write-entity!Write entity map to EDN file
list-entitiesList all entities of a type (reads directory)
entity-historyGet git log for an entity file
search-contentSearch entity files via git grep
git!Execute arbitrary git commands in a repo directory
init-bare!Initialize a bare git repository
bare-repo?Check if directory is a bare repo
bare-repo-statusReturn status with HEAD and worktree count
add-worktree!Add a new worktree with a new branch
fork-worktree!Add a worktree branching from a specific commit
remove-worktree!Remove a worktree (keeps branch history)
prune-worktrees!Clean up stale worktree references
list-worktreesList all worktrees with path/head/branch info
worktree-commit!Stage and commit a file within a worktree
gc!Run git garbage collection (optionally aggressive)
The git-options namespace provides configuration keys:
(require '[us.whitford.fulcro.rad.database-adapters.git-options :as gto])
;; Environment keys (in Pathom env)
::gto/repositories ; Map of schema -> repo map
;; Attribute-level options
::gto/entity-dir ; Override entity directory name
::gto/file-key ; Use this attribute's value as filename
::gto/generate-resolvers? ; Disable auto resolver generation
::gto/commit-message-fn ; Custom commit message function
::gto/read-only? ; Attribute is derived from git metadata
;; Bare repo / worktree options
::gto/bare? ; Initialize as bare repo
::gto/worktrees? ; Enable worktree support
::gto/worktree-dir ; Subdirectory for worktrees (default: "worktrees")
┌─────────────────────────────────────────────┐
│ Fulcro RAD Forms │
│ (save / delete / resolve) │
├─────────────────────────────────────────────┤
│ wrap-git-save │
│ wrap-git-delete │
│ delta → EDN → git commit │
├─────────────────────────────────────────────┤
│ generate-resolvers │
│ ID resolver (read EDN by ID) │
│ All-IDs resolver (list directory) │
├─────────────────────────────────────────────┤
│ utilities.clj │
│ git! read-entity write-entity! │
│ entity-history search-content │
│ init-bare! add-worktree! gc! │
│ dual-path dispatch: JGit ▸ CLI │
├──────────────────────┬──────────────────────┤
│ JGit (in-process) │ git CLI (shell) │
│ primary read+write │ fallback + ops │
│ init/commit/log │ JGit 7.x lacks │
│ grep/worktree add │ (e.g. worktree │
│ fast, no subprocess │ list/prune) │
└──────────────────────┴──────────────────────┘
Normal repos: Working directory with entity subdirectories
data/knowledge/memory/uuid-1.edn
data/knowledge/memory/uuid-2.edn
Bare repos + worktrees: Each worktree is an independent working directory on its own branch
data/conversations.git/ (bare repo, no working dir)
data/conversations.git/worktrees/conv-123/ (worktree on branch conv-123)
data/conversations.git/worktrees/conv-456/ (worktree on branch conv-456)
clojure -M:run-tests # all tests
clojure -M:run-tests --focus us.whitford.fulcro.rad.database-adapters.git-test # one ns
The :run-tests alias pins both Pathom 2 and Pathom 3 so the default (Pathom 2)
and optional (Pathom 3) resolver code paths are both exercised.
clojure -M:outdated
Builds are defined in build.clj (tools.build):
clojure -T:build jar # write pom + thin jar into target/
clojure -T:build clean # remove target/
clojure -T:build install # install to your local ~/.m2 repository
Deploy runs in a separate, isolated classpath (deps-deploy pulls legacy Maven
libraries that conflict with tools.build’s Maven resolver, so they cannot share
a process). Build the jar first, then deploy. A Clojars deploy token is
required — not your account password:
clojure -T:build jar
CLOJARS_USERNAME=<user> CLOJARS_PASSWORD=<deploy-token> clojure -X:deploy
Copyright (c) 2026 Michael Whitford
Distributed under the MIT License.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |