Annually-Funded Developers' Update: March & April 2026

Hello Fellow Clojurists!

This is the second of six reports from the developers who are receiving annual funding for 2026. Thanks to everyone for supporting their work and important contributions to the Clojure community.

Bozhidar Batsov: nREPL, Clojure Mode, ts-mode, Orchard, CIDER, and more
Clojure Camp: Badges, nano-conj, excercises
Eric Dallo: eca, eca-desktop, clojure-lsp, clojure-lsp-intellij
Jeaye Wilkerson: Jank compiler architecture and optimization
Michiel Borkent: babashka, ClojureScript async/await, Rebel, squint, and much more

Bozhidar Batsov

2026 Annual Funding Report 2. Published May 1, 2026.

The last two months have been some of the busiest I’ve had in clojure-emacs land in a long while. No big “X.Y” CIDER release to point at yet, but there’s been a steady stream of important work across most of the sibling projects. Below are some of the highlights.

clojure-mode 5.22 and 5.23

Two clojure-mode releases back-to-back, after a long stretch of relative quiet:

  • 5.22 (March 3): a big bug-fix dump — clojure-sort-ns no longer mangles :gen-class, clojure-thread-last-all doesn’t eat closing parens into line comments, clojure-add-arity preserves arglist metadata, letfn bindings get a function face, and edn-mode indents data like data. On the feature side: a new clojure-preferred-build-tool for projects that have several build tool files lying around, a dedicated clojure-discard-face for #_ forms, and project root detection for ClojureCLR’s deps-clr.edn.
  • 5.23 (March 25): adopts the modern indent spec tuple format (((:block N)), ((:inner D)), …) that clojure-ts-mode and cljfmt already use. The legacy format (integers, :defn, positional lists) still works, but it’s slated for removal in clojure-mode 6. There’s a new public clojure-get-indent-spec API, and CI moved off CircleCI onto GitHub Actions.

Unifying the indent format across our Clojure tooling has been on my todo list for years - happy to see it finally happen!

clojure-ts-mode

No release in this window, but I poured a lot of effort into the test suite - proper indentation and font-lock test coverage, a new configuration option test suite, and integration tests against sample files. Boring infrastructure work, but it’s the kind of thing that pays compound interest as the mode keeps maturing.

Side note: In the past several months I’ve spent a bit of time hacking on neocaml and my work there provided quite a bit of inspiration for improvements to clojure-ts-mode. (and, of course - clojure-ts-mode inspired me to create neocaml in the first place)

Orchard 0.41

Orchard v0.41.0 shipped on April 13 with a rewrite of orchard.xref, a refactor of orchard.indent, and a test modernization pass. I chipped in some smaller fixes - a cycle guard in fn-transitive-deps, validating input to stacktrace/analyze, compiling a hot regex once instead of on every call, and standardizing the -test suffix across the test suite.

We’ve also migrated from Lein to tools.deps for Orchard and probably we’ll continue in this direction for the rest of our Clojure projects in the nREPL and Clojure Emacs GitHub orgs. No rush on that front, though.

cider-nrepl 0.59

cider-nrepl v0.59.0 landed on April 14. The headline change from my side is that the ops are now properly namespaced (#710) - something that’s been sitting in the issue tracker for a very long time. CIDER on master already speaks the new namespaced ops with a fallback to the legacy names for older cider-nrepl versions, so upgrades should be painless.

Other notable bits: Compliment and clj-reload are no longer shaded, the test middleware properly binds *report-counters*, and we extract inline maps from composite Lein profiles correctly.

refactor-nrepl

refactor-nrepl got a serious modernization push in April - no release yet, but a lot of cleanup landed:

  • Dropped http-kit in favor of the JDK’s built-in HTTP client (one fewer dependency, one less thing to break).
  • Restored hotload-dependency on top of tools.deps.
  • A new def-op macro to simplify how ops are defined and how errors are handled.
  • A shared test session helper, a make lint target, and modernized CircleCI executors.
  • A full dependency bump.

Expect a refactor-nrepl release once the dust settles.

clj-refactor.el

Just a small cleanup pass to align with the new Emacs 28+ baseline. clj-refactor is in maintenance mode these days, but I’m trying to keep it healthy.

Eventually I’ll be taking a closer look at moving some of its functionality to clojure-mode and CIDER, but there’s more ground-work that needs to be done first.

CIDER

No CIDER release this cycle, but master accumulated a lot of structural work that will land in CIDER 1.22:

  • nrepl-client.el decoupled from CIDER. It no longer depends on sesman or any of CIDER’s UI layer, which makes the nREPL client genuinely reusable outside CIDER. I also split cider-connection.el into cider-session.el + cider-connection.el and decoupled cider-eval.el from cider-repl.el.
  • cider-jack-in got a serious overhaul: a proper jack-in tool registry, unified entry points around shared helpers, TRAMP fixes, sane runtime defaults, and cider--resolve-command now actually works over TRAMP.
  • A pile of resilience fixes for the nREPL client: tear down the SSH tunnel on abnormal client disconnect, spawn the tunnel without a shell, plug request-id leaks in raw response handlers, stop response-handler errors from poisoning the response queue, and put a FIFO cap on nrepl-completed-requests so it can’t grow without bound.
  • Namespaced ops everywhere, matching cider-nrepl 0.59 - with a fallback to the legacy op names so older cider-nrepl keeps working.
  • A new nrepl-make-eval-handler with a keyword-arg API (and removal of the trivial wrappers that used to live around it).
  • Mode-line spinner while tests are running, which is a small thing but I missed it every time I ran a long test suite.
  • Lots of test coverage backfill on the client side.

This is shaping up to be one of those releases that doesn’t look flashy on the outside but cleans up a ton of long-standing internal mess. The decoupling work in particular has been on my mind for years.

I’ve also started working on experimental support for prepl in CIDER. I’m not sure if this will ever land or be properly supported, but it’s been a fun side quest to me to ensure that CIDER logic is decoupled properly from its bundled nREPL client that I plan to eventually spin out of CIDER.

MrAnderson

Slightly outside the clojure-emacs org, but related: I’ve been helping MrAnderson get back on its feet. Three PRs landed in March/April:

MrAnderson is what cider-nrepl (and others) use to shade dependencies, so a healthy MrAnderson is in everyone’s interest. There’s more to come here!

What’s next

CIDER 1.22 once the jack-in and decoupling work settles, a refactor-nrepl release to ship all the April cleanup, and hopefully a fresh MrAnderson cut soon after.

As always - thanks to everyone who pitched in, especially Sashko Yakushev who once again did much of the the heavy lifting on cider-nrepl and Orchard.

And a HUGE THANKS to the members of Clojurists Together for supporting my Clojure OSS work! You rock!


Clojure Camp

2026 Annual Funding Report 2. Published May 12, 2026.

  • Some of our CT funds are being set aside to support three efforts this year:
    • sponsoring conference attendance for new Clojurians,
    • supporting Clojure and non-Clojure meetups with a “pizza fund” (WIP),
    • hosting an experimental nano-conj (in-person multi-day open-ended hack-on-clojure “retreat”) (WIP)
  • On the “dev side”….
    • Worked through most of the backlog of exercises done in mobs and released on https://exercises.clojure.camp/
    • Continued on badge system, mostly wrestling with display of the “learning graph”
  • Other happenings
    • Hosted an in-person workshop at the Recurse Center (with another planned)
    • Started a new book club reading Knesl’s “Applied High Order Functions in Clojure”
    • Early planning for our presence at Conj this year (may be doing a “Learning Together” track on the workshop day, featuring Mobbing and Pairing sessions)
  • Raf has a major commitment finishing up in May, so June+July should see a big push on Camp projects (and resuming regular Mobs)

Eric Dallo

2026 Annual Funding Report 2. Published May 8, 2026.

Excited 2 months of lots of work and help from Clojurians! We had improvements in eca and clojure-lsp mainly, and new projects as well.

ECA

ECA keeps growing a lot, receiving lots of contributions, with more than 800 stars already I’m planning a stable release soon, in these 2 months we had lots of releases with ton of stuff, so I will focus on the main highlights:

0.109.1 - 0.131.1

  • Plugins: New plugin system to load external configuration from git repos or local paths, with an official marketplace at plugins.eca.dev. Plugins can provide skills, MCP servers, agents, commands, hooks, rules and arbitrary config overrides, managed via /plugins, /plugin-install and /plugin-uninstall.
  • Remote web control: New remote web control server for browser-based chat observation and control via web.eca.dev, allowing you to observe and drive ECA chats from any browser.
  • Trust mode: Clients can now auto-accept tool calls that would require manual approval, with regex patterns support for fine-grained shell_command approval.
  • Task tool: Built-in task tracking tool to let the agent plan and follow multi-step work more reliably.
  • Background shell commands: New background parameter on shell_command and a dedicated bg_job tool to manage long-running processes like dev servers and watchers.
  • Chat list, open, fork and /model: New chat/list, chat/open and /fork commands let clients render a chat sidebar and clone existing conversations, plus a new /model command to switch model mid-chat.
  • Message flags: Named checkpoints inside a chat for resuming and forking from a specific point, with full chat history preserved across compactions via tombstone markers.
  • ask_user tool: LLMs can now ask the user questions with optional selectable options, fully integrated with hooks and trust modes.
  • Image generation: Support for OpenAI’s built-in image_generation tool via the Responses API, including image edits across turns and MCP tools that return image content.
  • Prompt steering: New chat/promptSteer notification to inject user messages into a running prompt at the next LLM turn boundary, without stopping it.
  • MCP improvements: New mcp/addServer, mcp/removeServer, mcp/updateServer, mcp/enableServer and mcp/disableServer requests to manage MCP servers at runtime, plus much better OAuth spec compliance and a switch from the Java SDK to plumcp.
  • More providers and models: Added LiteLLM, LM Studio, Mistral and Moonshot as built-in providers with login support, Claude Opus 4.7, deepseek-v4-pro, gpt-5.4 and gpt-5.5 variants, and GitHub Enterprise Copilot.

ECA Desktop

eca-desktop eric mar apr

Since ECA has been pretty stable and built in top of a nice extensible protocol, it worked so well that we decide to offer the same server capabilities to a Desktop client, similar to Claude Desktop but reusing the same server, this makes possible to have the same experience without an Editor, especially useful for non techinical people in price for a thin layer connecting to the server.

That’s a lot for ECA and all part of the amazing community that’s been activelly helping with issues, feedback and contributions.

clojure-lsp

We had some important bumps with lots of fixes, new code actions and contributions! The main highlight here is the arrival of performance tests in the project, which is a long waited thing which would help unblock performance optmizations in clojure-lsp since now we have a way to reliable know a mean, p90, p80 of how much time clojure-lsp spend on its features like initialization.

2026.05.05-12.58.26

  • Fix cyclic-dependencies linter falsely reporting cycles for (require ...) calls inside (comment ...) forms. #2107
  • Support find-definition for fully qualified vars even when the namespace is not explicitly required. #2028
  • Fix create-test code action appending a duplicate deftest when one with the matching name already exists, now navigating to the existing deftest instead. #2274
  • Change the default of :clean :ns-inner-blocks-indentation from :next-line to :keep, so clean-ns (including the automatic run after add-missing-libspec, add-require-suggestion, add-missing-import, and move-form) no longer reflows the :require/:import block when the user has not configured an indentation style. Users who want the previous behavior can set :clean :ns-inner-blocks-indentation :next-line explicitly. #2261
  • Fix add-missing-require refer suggestions leaking across languages, so a .clj file no longer offers refers defined only in .cljs files (and vice versa). #2271
  • Add :private-by-default-on-extract? setting to control whether extracted functions and defs are private by default. #2258
  • Measure performance of code actions
  • Avoid incorrect circular dependency errors from :as-alias by working around clj-depend bug.
  • Fix inline-def to work with defs with metas.
  • Bump clj-kondo to 2026.04.16-20260503.191510-9.
  • bump up timeout for code action performance measurement, include p90 measurement #2236
  • implementation of inline function. #1827
  • Fix initialization crash when a source file has syntax errors (e.g. unbalanced parens) by using safe parser in unused-public-var linter’s :gen-class check. #2242
  • Bump rewrite-clj to 1.2.54.
  • implement move to :let refactoring #1732
  • Measure performance of didOpen and didChange
  • if code-action selection end-position args aren’t provided, don’t try to use them #2276
  • add unit tests for command actions location args #2279
  • New code actions: replace :refer with :as and replace :as with :refer, with support for merging into existing :refer vectors.

clojure-lsp-intellij

3.5.1 - 3.5.5

  • Implement createServerInstaller and createLanguageServerSettingsContributor on ClojureLanguageServerFactory. Newer LSP4IJ versions added these as default interface methods, but our def-extension/gen-class-backed factory always overrides interface methods, so explicit stubs are required.
  • Fix slurp-action-test against newer LSP4IJ versions. Bump clj4intellij to 0.9.0 and switch the test fixture to setup-heavy so the project base has a real filesystem path (LSP4IJ’s FileSystemWatcherManager calls VirtualFile.toNioPath(), which throws on the in-memory TempFileSystem used by light fixtures).
  • Fix new namespace creation incorrectly creating files under the absolute host filesystem path. #83
  • Fix QuoteHandler compile error by merging BAD_CHARACTER into the quote-handler TokenSet.
  • Fix auto closing single quotes.
  • Improve CI to have plugin zips in all releases, avoiding wait for Jetbrains approval.

Jeaye Wilkerson

2026 Annual Funding Report 2. Published May 8, 2026.

Howdy folks! Thank you so much for the sponsorship this year. For the last two months, I have been focused on compiler architecture and optimization for jank.

On the compiler architecture side, I have designed and implemented a custom intermediate representation (IR) for jank programs which sets the stage for writing Clojure-specific optimization passes. This IR operates at the level of Clojure’s semantics, which is much higher level than LLVM IR, and so we are able to perform optimizations which LLVM could never do for us.

On the optimization side, I have taken the first benchmark of many, recursive fibonacci, and I have optimized it to be nearly twice as fast as Clojure JVM, for the same exact code. This benchmark is the first of many and I will be following up with more benchmark optimization results in the coming two months, using our new IR as a platform for optimizing jank’s performance.

To read all about the details of jank’s IR and the benchmark optimization, take a look at this blog post:


Michiel Borkent

2026 Annual Funding Report 2. Published May 11, 2026.

In this post I’ll give updates about open source I worked on during March and April 2026.

To see previous OSS updates, go here.

Sponsors

I’d like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn’t exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!

Updates

Babashka conf and Dutch Clojure Days 2026

Babashka Conf 2026 was held on May 8th in the OBA Oosterdok library in Amsterdam! David Nolen, primary maintainer of ClojureScript, was our keynote speaker. We’re excited to have Nubank, Exoscale, Bob, Flexiana and Itonomi as sponsors. Nubank and Exoscale are hiring. Wendy Randolph was our event host. For the schedule and other info, see babashka.org/conf.
The day after babashka conf, Dutch Clojure Days 2026 was also held - so it was a great weekend in Amsterdam!
Hope to have seen many of you there!

Projects

In the last two months I spent significant time organizing babashka conf, but made progress in several projects as well.

My upstream work to enable async/await in ClojureScript was merged in the beginning of March. The implementation mirrors squint. Thanks David for reviewing and merging. Also deftest now supports an ^:async annotation so you can use async/await and don’t need to mess around with the cljs.test/async macro anymore:

I’ll be presenting this work at the Dutch Clojure Days.

Rebel-readline is now bb compatible. The work involved mainly exposing more JLine stuff and making sure rebel-readline didn’t hit any internal JLine APIs. One step to drive this to completion was to make a dependency, compliment, bb compatible. Thanks both to Bruce and Alexander for the cooperation.

Squint now supports cljs.test and multimethods! clojure-mode was ported to use the new cljs.test.

On the cream front, I put in effort to make the binary smaller and have been keeping up with the new GraalVM EA releases. I’ve been posting bug reports to the crema maintainer. Currently there’s still an unfixed bug around core.async that I have trouble reproducing in pure Java. I also added lots of library tests to CI so I can ensure stability in the long run. For now it remains experimental, but the direction is promising.

A performance PR to weavejester/dependency speeds up depend, depends? and topo-sort significantly, so clerk notebooks render faster.

The cljfmt library, also by @weavejester, now fully runs from source in babashka. The Java diff library that wasn’t bb-compatible was replaced with text-diff, but only for the babashka path. The JVM build of cljfmt still uses the original Java diff library, with a possible switch later once text-diff has matured.

Several SCI fixes were made to improve Clojure compatibility between babashka and Clojure. E.g. records can now support extending to IFn which was a blocker for some Clojure libs that tried to run in bb so far.

Clj-kondo 2026.04.15 got a few new linters thanks to @jramosg for stewarding most of these. It also has better out of the box potemkin support, and @alexander-yakushev contributed a wave of performance improvements.

Updates per project below. Bullets are highlights; see each project’s CHANGELOG.md for the full list.

  • babashka: native, fast starting Clojure interpreter for scripting.

    • Released 1.12.216, 1.12.217 and 1.12.218
    • Support rebel-readline as external REPL provider:
      • Add proxy support for Completer, Highlighter, ParsedLine, Writer, Reader
      • Add clojure.repl/special-doc and clojure.repl/set-break-handler!
      • Add clojure.main/repl-read
      • Add org.jline.reader.Buffer to class allowlist
    • Add clojure.java.javadoc namespace with javadoc available in REPL #1933
    • Fix (doc var), (doc set!) and other special forms #1932
    • Support (source inc) and (source babashka.fs/exists?) for built-in vars #1935
    • Support BABASHKA_REPL_HISTORY env var for configurable REPL history location #1930
    • Fix deftype and defrecord inside non-top-level forms (e.g. let, testing) #1936
    • #1948: add java.util.HexFormat interop support
    • #1403: fix uberscript warnings with :as-alias
    • #1955: support -version as an alias for --version
    • #1954: add clojure.lang.EdnReader$ReaderException
    • #1951: fix --prepare flag skipping next token
    • #1967: expose clojure.data.xml.tree/{flatten-elements,event-tree}, clojure.data.xml.event record constructors, and clojure.data.xml.jvm.parse/string-source
    • #1969: include java.net.Proxy and java.net.Proxy$Type Java classes (@jeeger)
    • #1939: disable JLine backslash escaping/shell history commands (@bobisageek)
    • Performance improvements for math operations and for calling functions on locals
    • Add many new classes to reflection config: java.lang.reflect.Constructor, java.lang.reflect.Executable, java.util.stream.Collectors, java.util.Comparator (for reify), and more
    • Bump JLine to 4.0.12, cheshire to 6.2.0, nextjournal.markdown to 0.7.255, edamame to 1.5.39, data.xml to 0.2.0-alpha11, jsoup to 1.22.2, rewrite-clj to 1.2.54, tools.cli to 1.4.256, transit-clj to 1.1.357, fs to 0.5.32
    • Full changelog
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • Fix recur with 20+ args in loop (#1035)
    • Check recur arity, throw when it doesn’t match (#1034)
    • Support IFn on defrecord, deftype and reify (#808, #1036)
    • Validate single binding pair in let-like conditional macros (#1037)
    • Normalize SCI types in hierarchy lookups (#1033)
    • Expose IPrintWithWriter as protocol (#1032)
    • Optimize tight loops: fused binding nodes + specialized inlined calls (#1031)
    • Support special form documentation in doc macro
    • Include SCI types in ns-map
    • Full changelog
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

    • Released 2026.04.15
    • #2788: NEW linter: :not-nil? which suggests (some? x) instead of (not (nil? x)), and similar patterns with when-not and if-not (default level: :off)
    • #2520: NEW linter: :protocol-method-arity-mismatch which warns when a protocol method is implemented with an arity that doesn’t match any arity declared in the protocol (@jramosg)
    • #2520: NEW linter: :missing-protocol-method-arity (off by default) which warns when a protocol method is implemented but not all declared arities are covered
    • #2768: NEW linter: :redundant-declare which warns when declare is used after a var is already defined (@jramosg)
    • #1878: support potemkin’s import-fn, import-macro, and import-def
    • #2498: support new potemkin import-vars :refer and :rename syntax
    • Performance optimizations across many linting paths (@alexander-yakushev) and hook-fn lookup caching to avoid repeated SCI evaluation
    • Add type support for pmap and future-related functions (future, future-call, future-done?, future-cancel, future-cancelled?) (@jramosg)
    • #2762: fix false positive: throw with string in CLJS no longer warns about type mismatch (@jramosg)
    • #2770: linter-specific ignores now correctly respect the specified linters
    • #2773: align executable path for images to be /bin/clj-kondo (@harryzcy)
    • #2621: load imports from symlinked config dir (@walterl)
    • #2798: report correct filename and error details when StackOverflowError occurs during analysis
    • Full changelog
  • cream: Clojure + GraalVM Crema native binary

    • Followed each GraalVM EA release: EA21 shrunk the binary to ~175MB, EA22 brought a virtual-thread fix, EA23 fixed the forkjoin segfault, EA24 finally allowed re-enabling clojure.core.async-test
    • Added smoke tests for httpkit, nextjournal/markdown, clj-yaml, core.async ioc-macros
    • Updated 10M loop benchmark numbers for EA21
    • Added Windows test status notes (still some failures on EA22)
  • squint: CLJS syntax to JS compiler

    • Released 0.10.186, 0.11.187, 0.11.188 and 0.11.189
    • Add multimethod support: defmulti, defmethod, get-method, methods, remove-method, remove-all-methods, prefer-method, prefers, plus hierarchy ops isa?, derive, underive, make-hierarchy, parents, ancestors, descendants (#806)
    • cljs.test/report is now a multimethod, extensible via defmethod. test-var now fires :begin-test-var / :end-test-var events.
    • Accept plain await in async functions, in anticipation of CLJS next. The legacy js-await and js/await forms continue to work as aliases for now.
    • Add built-in cljs.test / clojure.test support: deftest, is, testing, are, use-fixtures, async, run-tests
    • Fix with-meta now preserves callability when applied to a function
    • #783: auto-load macros from .cljc files via :require (no need for :require-macros); resolve qualified symbols from macro expansions
    • #784: resolve transitive macro deps and auto-import runtime deps from macro expansion
    • #809: add squint.compiler/compile* and squint.compiler/transpile* which accept either a string or a sequence of pre-parsed forms, skipping the forms -> string -> forms roundtrip for SSR use cases
    • #810: shorthand classes in #html / #jsx were erased when an attrs map was present without a :class key
    • Full changelog
  • cherry: Experimental ClojureScript to ES6 module compiler

    • Accept plain await as a special form, in anticipation of CLJS next
    • Multiple :require-macros clauses with :refer now properly accumulate instead of overwriting each other
    • Add cherry.test with clojure.test-compatible testing API: deftest, is, testing, are, use-fixtures, async, run-tests. Macros are compiler built-ins (shared with squint), so no :require-macros plumbing is needed in user code.
  • nbb: Scripting in Clojure on Node.js using SCI

    • Released 1.4.207
    • #408: support IFn on defrecord and reify
    • Fix REPL and nREPL not awaiting promesa thenables (e.g. p/then results)
  • fs: file system utility library for Clojure

    • Released 0.5.32 and 0.5.33
    • #232: add touch fn (@lread & @borkdude)
    • #197: docstring review: consistent arg naming, improved docstrings, added Coercions and Returns / Argument Naming Conventions sections to README (@lread)
    • #231: get/set attribute functions were never following links. They now respect the :nofollow-links option (@lread)
    • #254: fix split-ext and extension on dotfiles with parent dirs (e.g. foo/.gitignore)
    • #202: gzip & gunzip now honor dest dir when specified (@lread)
    • #215: document effect of umask on created files and directories (@lread)
    • #182: enable soft & hard link tests on Windows (@lread)
    • #242: test: add JDK 26 to CI test matrix (@lread)
  • clerk: Moldable Live Programming for Clojure

    • Improve analysis performance by bumping weavejester/dependency (#808)
    • Bump SCI to v0.12.51 (#793), enables async/await in viewer functions
    • Improve presentation performance (#803)
    • Remove bb-specific code for array-map data structure (#805)
    • Preserve TOC opts (#806)
    • Remove redundant declare of present+reset! (#809)
    • Fix build-graph crash on non-Clojure source files (#810)
  • edamame: configurable EDN and Clojure parser with location metadata and more

    • Released 1.5.38 and 1.5.39
    • parse-ns-form: include :use and :require-macros in :requires
    • Check if object is iobj before attaching metadata #141 #142
  • Nextjournal Markdown: A cross-platform Clojure/Script parser for Markdown

    • Released 0.7.225
    • Add option :disable-footnotes true to disable parsing footnotes #67
  • quickdoc: Quick and minimal API doc generation for Clojure

    • Released 0.2.6
    • #42: fix var name not recognized in docstring when preceded by multiline backtick expression
    • #52: fix formatting of function signature when :or destructuring uses namespaced keyword fallback value
    • Dedent indented docstrings before rendering #53
  • grasp: Grep Clojure code using clojure.spec regexes

    • Released 0.2.5
    • Bump SCI to 0.12.51, Clojure to 1.12.4
    • Upgrade CI to GraalVM 25; move Windows CI from Appveyor to GitHub Actions
    • Fix bug in native which dropped all match results (@bsless)
    • Fix circular reference in grasp.impl
  • babashka.nrepl: The nREPL server from babashka as a library

    • Lock output stream in send to prevent interleaved bencode frames from concurrent writes
    • info and lookup op refinements: lookup carries nested info map whereas info is a flatmap
  • pod-babashka-instaparse: instaparse from babashka

    • Expose add-line-and-column-info-to-metadata
    • Drop macOS Intel builds, now building for macOS aarch64 only
    • Migrate Windows CI from Appveyor to GitHub Actions
    • Upgrade CI to GraalVM 25
    • Add --features=clj_easy.graal_build_time.InitClojureClasses to native-image
  • instaparse-bb: Use instaparse from babashka

    • Released 0.0.7
    • Bump pod to 0.0.7
    • Add add-line-and-column-info-to-metadata and get-failure
    • Fix opts passing in parser (e.g. :output-format :enlive)
    • Support java.net.URL for grammars
  • babashka-sql-pods: babashka pods for SQL databases

    • Released 0.1.5 and 0.1.6
    • #74: add DB2 support (@janezj)
    • #72: handle concurrent requests (@katangafor)
    • Upgrade to Oracle GraalVM 25.0.2; bump next.jdbc, cheshire (Jackson 2.12 -> 2.20), PostgreSQL, MSSQL, HSQLDB, MySQL Connector/J drivers
    • Remove DuckDB support
    • #51: macOS binaries are now aarch64 only
  • http-client: HTTP client built on java.net.http

    • Replace defunct httpstat.us examples with httpbin.org in tests
  • neil: A CLI to add common aliases and features to deps.edn-based projects

    • Fix README instructions for dev installation (@teodorlu)
  • deps.clj: a faithful port of the clojure CLI bash script to Clojure

    • Released 1.12.4.1618
    • #145: support for installing in FreeBSD and Windows bash environments including MINGW64, MSYS_NT and Cygwin (@ikappaki)
    • Catch up with Clojure CLI 1.12.4.1618

Contributions to third party projects:

  • ClojureScript:
    • CLJS-3470: added async/await support (merged!)
    • CLJS-3476: added async deftest support (merged!)
  • weavejester/dependency: improve performance of depend, depends?, and topo-sort
  • weavejester/cljfmt: #404 babashka compatibility via new text-diff lib (merged!)
  • Engelberg/instaparse: submitted #242 for babashka compatibility. Required :bb reader conditionals to replace the AutoFlattenSeq deftype with plain vectors plus metadata markers, swap the Segment deftype for a reify-based CharSequence, and add a CI test runner. Open, awaiting review.

Other projects

These are (some of the) other projects I’m involved with but little to no activity happened in the past two months.

Click for more details

Permalink

Week Notes 2026.20

A Clojure Lambda function for monitoring AWS CloudWatch Logs subscriptions, using Gatus to monitor scheduled jobs, and configuring Amazon EventBridge Scheduler to run an ECS task twice a day.

Permalink

Expert Clojure Workflows for AI Agents: Four Skills from Production Experience

When you let an AI agent write Clojure code, you expect it to leverage the language's superpowers—the REPL's interactivity, structural editing, format-preserving code manipulation, and the rich ecosystem of wrapper libraries. Instead, what you typically see is mediocre code written slowly, as the agent makes the same mistakes every developer learns to avoid.

I discovered this the hard way.

The Setup: Vibe Coding with Observations

While building lite-crm with Claude Code, I deliberately avoided the –dangerously-skip-permissions flag. Instead, I sat beside the agent and watched it work—observing its patterns, frustrations, and failures. What I saw was an agent trained on millions of codebases but ignorant of how Clojure practitioners actually think.

Three concrete problems emerged:

Problem 1: The Wrapper Library Blind Spot

When encountering Java interop, the agent jumps straight into direct interoperability without ever asking: "Is there a Clojure wrapper library for this?"

The result: Uglier code, harder to maintain, and a missed opportunity for idiomatic Clojure.

Problem 2: Formatting Brittleness

Code formatters like cljfmt are essential—but they create a sneaky problem. When the agent modifies source and the formatter shifts indentation by a single space, the agent's subsequent str_replace operations fail due to whitespace mismatch.

The result: I watched it fail, retry, fail again, then give up and rewrite entire files. Enormous token waste.

Problem 3: Primitive Debugging

When a test failed, the agent fell back on the crudest debugging technique: add println statements, run the test, inspect output, delete the logs, restore the code. Repeat.

This is especially wasteful in a Clojure project where I've provided direct access to the REPL via the brepl CLI. The agent could inspect values interactively, test hypotheses instantly, and trace execution without touching source code. But it never did.

The Recognition

These weren't knowledge gaps. They were behavioral gaps—places where the agent's default approach conflicted with Clojure expertise.

In the context of Clojure Stack Lite (which includes proper testing harness and real database, not mocks), the agent wasn't just writing suboptimal code—it was making design decisions based on unfamiliar tools.

I decided to address this not by teaching the agent more facts, but by redirecting its behavior.

Four Skills to Close the Gap

The result is four skills, each targeting a specific behavioral pattern that distinguishes novice agents from expert Clojure practitioners:

1. clj-debug: From Logging to REPL Inspection

The Problem: Agents default to adding println, tap>, or logging statements, then running tests to inspect output.

The Pattern: In Clojure, this is backwards. The REPL lets you pin a value with def, explore its structure instantly, test hypotheses interactively—all without modifying code.

What the skill does: When you're about to debug, clj-debug redirects from logging patterns to REPL-based inline inspection. It teaches the agent to use def, keys, keyword access, and structural exploration—the actual workflow expert Clojure developers follow.

Behavioral change: From edit-test-inspect cycle to interactive REPL inspection. This is faster, non-invasive, and gives immediate feedback.

2. clj-discover: Systematic API Exploration

The Problem: When encountering unfamiliar Java classes or macros, agents jump to direct integration without exploring whether an idiomatic Clojure wrapper already exists.

The Pattern: Expert Clojure developers follow a deliberate workflow:

  1. Search for a Clojure wrapper library first (usually there is one)
  2. If not, inspect the Java class via reflection
  3. For macros, expand them to understand what code they generate

What the skill does: clj-discover codifies this workflow, ensuring the agent prioritizes idiomatic libraries and systematic exploration before writing integration code.

Behavioral change: From direct interop to research-first integration. The result is cleaner, more maintainable code.

3. clj-replace: Format-Aware Structural Replacement

The Problem: Code formatters shift indentation by spaces, breaking text-based str_replace. The agent then wastes tokens failing repeatedly or rewriting entire files.

The Pattern: Clojure is homoiconic—code is data. Two S-expressions are semantically equivalent even if formatted differently. Expert editors handle this automatically via structural editing.

What the skill does: clj-replace compares code by structure (S-expression equivalence) rather than text, ignoring whitespace while preserving the original file's formatting style. It uses the rewrite-clj library to parse, match, and replace nodes safely.

Behavioral change: From brittle text matching to robust structural matching. Formatting variations become irrelevant.

4. clj-refactor: Mechanism/Policy Separation

The Problem: Without guidance, agents write tangled code where reusable mechanisms are mixed with business policy, creating inflexible designs that accumulate technical debt.

The Pattern: Arne Brasseur's mechanism/policy separation principle is core to building maintainable Clojure systems. Mechanism is context-free, stable, and reusable. Policy is opinionated, domain-specific, and volatile. Expert developers keep these separate.

What the skill does: clj-refactor scans code for opportunities to extract mechanisms from policy—functions where hard-coded values or implicit context can be made explicit, dependencies can be pushed to parameters, and reusable logic can be isolated.

Behavioral change: From monolithic functions to extracted, composable mechanisms. Code becomes easier to test, reuse, and reason about.

Note: Unlike clj-debug, clj-discover, and clj-replace—which activate automatically when the agent encounters problems—clj-refactor is user-initiated. You invoke it when you want the agent to analyze code for refactoring opportunities, not in response to a failure.

Why This Matters

These aren't reference manuals or API documentation. They're workflow redirects—rules that teach AI agents to think like expert Clojure developers instead of generic code writers.

The underlying philosophy is simple: A skill's value is measured by behavioral change, not knowledge transfer.

When an agent uses clj-debug, it stops adding logging. When it uses clj-discover, it checks for idiomatic wrappers before raw interop. When it uses clj-replace, formatting becomes irrelevant. When you invoke clj-refactor, the agent identifies tangled mechanisms and suggests extraction. Each skill shifts the agent's default patterns closer to expert practice.

This matters because Clojure is a language of leverage. The REPL, immutability, homoiconicity, and the functional approach all reward practitioners who use them correctly. An agent that doesn't leverage these features isn't just writing slow code—it's missing the point of the language.

The goal is simple: your AI agent shouldn't just write Clojure code—it should think like a Clojure developer. These four skills make that possible.

Find them at: github.com/humorless/clj-native-agent

Permalink

Branches as Values, Merges as Queries

Branches as Values, Merges as Queries

May 2026

Snapshotting via copy-on-write is a well-trodden idea. ZFS and btrfs do it at the filesystem block layer; Neon and Aurora do it at the database page layer; Datomic and Datahike do it at the data-model layer. What differs is where the immutability lives, and that determines what you can do with the snapshots once you have them.

In Datahike, the database value itself is immutable. A datom never mutates; a query is always against a specific commit; a branch is a database value you can hand to a function. That last property changes the calculus in three ways.

First, branching is the same primitive as every other transaction. There’s no special bulk-load path, no restore mode, no control-plane operation — just a couple of small writes to storage.

Second, branches are database values you can pass to a query. The same query interface that reads the head of :db reads any historical commit on any branch. No special “as-of” mode, no separate replica.

Third, merging becomes a query. ZFS can clone a snapshot but can’t merge two of them — a filesystem doesn’t understand its own contents well enough to resolve a conflict. Datahike does: branches are database values, Datalog queries take multiple databases as inputs, so “what’s in :feature and not in :db is a query you write. Filtering, transformation, and conflict resolution are all the same language you query the database with.

The rest walks through datahike.versioning in order, with a brief note at the end on how the same surface shows up in the other bindings.

The storage model

A Datahike database is a persistent sorted set of datoms — five-tuples of [entity attribute value transaction op]. The storage layer is persistent-sorted-set, a B-tree-based immutable data structure designed for on-disk storage of sorted runs of datoms.

What matters for branching is the persistence property: every node is immutable. A transaction that adds, retracts, or modifies datoms walks from root to leaf, creates new nodes along the changed path, and leaves the unchanged subtrees pointing at exactly the same nodes as the prior snapshot. Both the old and new trees are valid; both are queryable; the new tree’s root is the only thing the system needs to know about to read it.

This is the same idea behind Clojure’s persistent vectors and Git’s object store. Datomic introduced it to databases in 2012; Datahike is the open-source descendant. Sharing is at the level of tree nodes: with a branching factor of 512, the tree stays shallow even for very large databases, and a transaction rewrites only the leaf and the few internal nodes on its path. Every other subtree is shared by pointer with the previous snapshot.

Two branch pointers, :db and :feature, into an immutable tree. The :feature branch has a new commit whose root points at one new internal node (B prime) and one new leaf (L prime); the rest of the tree — nodes A, C, and most leaves — is shared with :db.

Each node is content-addressable — its key in konserve (the storage abstraction) is derived from its contents. konserve maps the same protocol over filesystems, S3, JDBC databases, IndexedDB in browsers, and others. A node written once is never rewritten. The only thing that ever changes is a small map at a well-known key listing the root pointers for the indices in the current snapshot. That map is a commit. A branch is a named pointer at a commit, registered in a :branches set under a known key.

Creating a branch

What is this syntax?
require('[datahike.api :as d])

d/branch!(conn :db :feature)
(require '[datahike.api :as d])

(d/branch! conn :db :feature)

The system reads the commit-id currently at :db, verifies it points at a real commit, writes a new key mapping :feature → <commit-id>, and updates the :branches set to include :feature. Two key writes in the simple case — plus a CoW-branch operation for any attached secondary index (Lucene full-text, vector indices) that implements the branching protocol.

Wall-clock time depends almost entirely on the storage backend:

  • In-memory — sub-millisecond.
  • Local filesystem — a few milliseconds, dominated by fsync.
  • S3 — 10–100 ms, dominated by the network round-trip; the payloads are tiny.

No tree nodes are copied. :feature and :db reach through the same physical objects in storage. A million-datom branch costs nothing extra at fork time, and a hundred branches are still a hundred small writes — not a hundred database copies.

If the source doesn’t exist, branch! raises :from-branch-does-not-point-to-existing-branch-or-commit. If the target name is already taken, it raises :branch-already-exists. Both are explicit; you don’t get silent overwrites.

Reading from a branch

Branches are first-class. You read them by name (branch-as-db), by commit-id (commit-as-db), or by holding a connection that was opened with a :branch in its config.

What is this syntax?
def feature-db: d/branch-as-db(conn :feature)
def main-db: d/branch-as-db(conn :db)

d/q('[:find ?e :where [?e :widget/sku]] feature-db)

;; Or pin to a specific historical commit by UUID
def older-db: d/commit-as-db(conn #uuid "b4f2e1c0-2feb-5b61-be14-5590b9e01e48")
(def feature-db (d/branch-as-db conn :feature))
(def main-db    (d/branch-as-db conn :db))

(d/q '[:find ?e :where [?e :widget/sku]] feature-db)

;; Or pin to a specific historical commit by UUID
(def older-db (d/commit-as-db conn #uuid "b4f2e1c0-2feb-5b61-be14-5590b9e01e48"))

branch-as-db returns a database value — immutable, ready to query, safe to hold across calls. commit-as-db does the same for any historical commit, whether or not a branch still names it. Both work without an open connection on the target branch.

To write to a branch, connect with :branch in the config and transact normally:

What is this syntax?
def feature-conn: d/connect(assoc(cfg :branch :feature))
d/transact(feature-conn [{:widget/sku "Z", :widget/weight 99}])
(def feature-conn (d/connect (assoc cfg :branch :feature)))
(d/transact feature-conn [{:widget/sku "Z" :widget/weight 99}])

The write goes to :feature’s head; :db is undisturbed. Each branch has its own writer; transactions on different branches don’t serialize against each other.

The commit graph

Every transaction produces a commit whose :meta :datahike/parents set records its parents. branch! produces single-parent commits (the previous head of the branch). merge! produces commits with multiple parents. Walking back from any commit gives you the lineage.

What is this syntax?
require('[superv.async :refer [<?? S]]
  '[datahike.versioning :refer [branch-history]])

d/commit-id(@conn)
;; => #uuid "b4f2e1c0-…"

d/parent-commit-ids(@conn)
;; => #{#uuid "…"}        ; single parent on a normal commit
;; => #{#uuid "…" "…"}    ; two (or more) parents on a merge commit

<??(S branch-history(conn))
;; => sequence of stored DB values, in order from the current head back
;;    through every ancestor reachable via :datahike/parents
(require '[superv.async :refer [<?? S]]
         '[datahike.versioning :refer [branch-history]])

(d/commit-id @conn)
;; => #uuid "b4f2e1c0-…"

(d/parent-commit-ids @conn)
;; => #{#uuid "…"}        ; single parent on a normal commit
;; => #{#uuid "…" "…"}    ; two (or more) parents on a merge commit

(<?? S (branch-history conn))
;; => sequence of stored DB values, in order from the current head back
;;    through every ancestor reachable via :datahike/parents

branch-history is the workhorse for inspection: it walks the parent graph from the connection’s current branch backward and returns each commit as a DB value, with duplicates pruned. Useful for time-travel reports, audit trails, and assembling queries against arbitrary historical states.

Merging: merge-db plus Datalog

This is where the “branches as values” property earns its keep.

What is this syntax?
d/merge-db(conn #{:feature} tx-data)
(d/merge-db conn #{:feature} tx-data)

merge-db records a new commit on the current branch whose :datahike/parents includes both the previous head and :feature’s head. The tx-data is regular transaction data; Datahike applies it the same way it applies any transaction. The operation is routed through the writer so it serializes cleanly against concurrent transactions on the same branch. (Sync; there’s also d/merge-db! for the async path, intended for go blocks and listener callbacks.)

What merge-db does not do: figure out the tx-data for you.

That’s a feature, not a gap. Because branches are database values and Datalog queries take multiple databases as inputs, the diff between branches is a query:

What is this syntax?
d/q('[:find ?e ?a ?v
      :in $feature $main
      :where [$feature ?e ?a ?v _]
      [:db/txInstant not= ?a]
      not([$main ?e ?a ?v _])]
  feature-db main-db)
(d/q '[:find ?e ?a ?v
       :in $feature $main
       :where
       [$feature ?e ?a ?v _]
       [(not= :db/txInstant ?a)]
       (not [$main ?e ?a ?v _])]
     feature-db main-db)

:in $feature $main binds two databases; :where clauses pick which one each pattern matches against. The result is the set of datoms present in :feature but absent in :db — directly transformable to tx-data.

Real merges are more selective. A few patterns that fall out naturally:

Filter by attribute — merge only the schema changes, leave the data behind:

What is this syntax?
d/q('[:find ?e ?a ?v
      :in $feature $main
      :where [$feature ?e ?a ?v _]
      [contains?(#{:db/ident :db/valueType :db/cardinality} ?a)]
      not([$main ?e ?a ?v _])]
  feature-db main-db)
(d/q '[:find ?e ?a ?v
       :in $feature $main
       :where
       [$feature ?e ?a ?v _]
       [(contains? #{:db/ident :db/valueType :db/cardinality} ?a)]
       (not [$main ?e ?a ?v _])]
     feature-db main-db)

Last-write-wins on conflicting attributes — for each (e, a), pick the value with the latest transaction time across both branches:

What is this syntax?
d/q('[:find ?e ?a max(?t) ?v
      :in $feature $main
      :where or-join([?e ?a ?v ?t] [$feature ?e ?a ?v ?t] [$main ?e ?a ?v ?t])]
  feature-db main-db)
(d/q '[:find ?e ?a (max ?t) ?v
       :in $feature $main
       :where
       (or-join [?e ?a ?v ?t]
                [$feature ?e ?a ?v ?t]
                [$main    ?e ?a ?v ?t])]
     feature-db main-db)

Application-defined resolution — Datalog predicate clauses can call arbitrary functions, so routing each conflict through a domain resolver fits the same shape:

What is this syntax?
d/q('[:find ?e ?a ?v-resolved
      :in $feature $main ?resolve
      :where [$feature ?e ?a ?v-f _]
      [$main ?e ?a ?v-m _]
      [?v-f not= ?v-m]
      [?resolve(?e ?a ?v-f ?v-m) ?v-resolved]]
  feature-db main-db your-resolver-fn)
(d/q '[:find ?e ?a ?v-resolved
       :in $feature $main ?resolve
       :where
       [$feature ?e ?a ?v-f _]
       [$main    ?e ?a ?v-m _]
       [(not= ?v-f ?v-m)]
       [(?resolve ?e ?a ?v-f ?v-m) ?v-resolved]]
     feature-db main-db your-resolver-fn)

Once you have the tx-data — however you computed it — d/merge-db applies it and records the commit with both parents:

What is this syntax?
d/merge-db(conn
  #{:feature}
  mapv(fn [[e a v]]:
  [:db/add e a v]
end diff-tuples))
(d/merge-db conn #{:feature}
            (mapv (fn [[e a v]] [:db/add e a v]) diff-tuples))

branch-history then shows the merge commit; d/parent-commit-ids returns the full parent set.

The takeaway: Datahike doesn’t ship a built-in 3-way merge algorithm because it doesn’t need to. The merge algorithm is whatever Datalog query expresses your domain’s resolution rule. Three-way merge of textual files is hard because text has no semantics; merging datoms is a query because the data already carries its own structure.

This generalizes further than it looks. Martin Kleppmann has shown that CRDTs themselves can be expressed as pure Datalog queries over the operation log. Datahike’s merge model lets you adopt that approach incrementally: start with last-write-wins, add domain-specific resolvers where it matters, formalize as CRDT-shaped queries if you want full convergence guarantees.

Reset: force-branch!

force-branch! is the equivalent of git reset --hard. Pass a database value, a target branch, and the set of parent branches or commit-ids to attribute the new head to:

What is this syntax?
;; Rewind :feature to a known-good historical commit, treating it
;; as a fresh start from :db.
d/force-branch!(d/commit-as-db(conn #uuid "b4f2e1c0-…") :feature #{:db})
;; Rewind :feature to a known-good historical commit, treating it
;; as a fresh start from :db.
(d/force-branch! (d/commit-as-db conn #uuid "b4f2e1c0-…")
                 :feature
                 #{:db})

The branch head is overwritten unconditionally; the previous head becomes unreachable from the branch name. Existing connections to :feature are now stale and must be released and reconnected.

Useful for rolling back a bad branch after experimentation, pinning a branch to a known commit for audit, or rewriting a branch’s lineage when you need to. Use with care — the prior data isn’t deleted (GC controls that) but you’ve removed the named entry point, so if no other branch or commit-id references it, it goes away on the next sweep.

Cleanup: delete-branch! and gc-storage

What is this syntax?
d/delete-branch!(conn :feature)
(d/delete-branch! conn :feature)

Removes :feature from the :branches set. The branch’s data stays in konserve, reachable by commit-id, until garbage collection sweeps it — that’s intentional, so you can recover a deleted branch if you change your mind. Live connections to :feature will fail after this; remote readers should release.

You can’t delete :db. It’s the default main branch and removing it would orphan the database; if you want the database gone, delete the database. Other branches are fair game.

Storage reclamation is a separate, explicit step:

What is this syntax?
require('[superv.async :refer [<?? S]])

;; Default: only reclaim space from deleted branches.
<??(S d/gc-storage(conn))

;; With a cutoff date: keep snapshots newer than the date plus all
;; branch heads; delete intermediate snapshots older than the date.
let [thirty-days-ago new java.util.Date(System/currentTimeMillis() - 30 * 24 * 60 * 60 * 1000)]:
  <??(S d/gc-storage(conn thirty-days-ago))
end
(require '[superv.async :refer [<?? S]])

;; Default: only reclaim space from deleted branches.
(<?? S (d/gc-storage conn))

;; With a cutoff date: keep snapshots newer than the date plus all
;; branch heads; delete intermediate snapshots older than the date.
(let [thirty-days-ago (java.util.Date. (- (System/currentTimeMillis)
                                          (* 30 24 60 60 1000)))]
  (<?? S (d/gc-storage conn thirty-days-ago)))

Two things worth knowing about how gc-storage interacts with branch history:

Branch heads are always kept, regardless of cutoff. Every live branch’s current head survives every GC run; GC only removes the intermediate snapshots between commits — the dots between branch heads on the graph, not the latest dot on any branch.

Intermediate commits become unreachable below the cutoff. A 7-day cutoff means branch-history walks only return commits within that window plus the current heads, and d/commit-as-db lookups for older UUIDs fail because the snapshot is gone. The cutoff should also comfortably exceed your longest-running reader’s lifetime — Datahike’s distributed readers walk storage directly without coordinating with a writer, so a snapshot vanishing mid-query is a real failure mode. You’re trading old audit history (and reader safety) for disk space; pick the window to match your readers, compliance posture, and storage budget.

Without a date, d/gc-storage is always safe — it only reclaims storage from deleted branches. Datahike also ships an experimental online-GC mode that runs incrementally during transactions on single-branch databases; offline d/gc-storage is what you reach for in multi-branch setups.

For how gc-storage composes with purge (GDPR-driven datom deletion) and the broader governance story, see Data Governance in Versioned Systems.

What this unlocks

A handful of workflows that branching makes affordable:

  • AI agent sandboxes. Spin up fifty branches, each agent gets its own database to mutate. Merge what works, drop the rest.
  • Schema migration tests in CI. Branch from prod, apply the migration, run the regression suite, throw the branch away. The next CI run starts from the same prod commit.
  • Editorial workflows. Editors stage changes on a branch, reviewers query the staging branch, approve, merge.
  • Multi-tenant snapshots. Each tenant gets a branch of a shared base. Tenant-specific overrides live on their branch; base updates merge cleanly.
  • Time-travel debugging. When a bug shows up, branch from the current head, apply experimental fixes on the branch, and walk historical commits via commit-as-db to find when the offending state appeared.

None of these require special infrastructure. The same primitives that read the database also read every branch.

Across the other bindings

The versioning API is part of the Clojure API spec, and the Java, JavaScript / TypeScript, Python (pydatahike), C (libdatahike), and CLI (dthk) bindings are all auto-generated from it. Java surfaces it as Datahike.branchAsync / branchAsDb / mergeDb; JavaScript as d.branchBang / branchAsDb / mergeDb; equivalent forms in the others. The dthk CLI also supports the more general Datalog-driven merge workflow via dthk query with multi-source input and dthk transact — see the CLI doc for an example.

In SQL via pg-datahike, the read side is wired through session variables and a datahike.* function namespace: SET datahike.branch = 'feature', SET datahike.commit_id = '<uuid>', plus datahike.branches(), datahike.create_branch(), datahike.delete_branch(). Write-side merge-db over SQL is on the roadmap. See Datahike Speaks Postgres for the full pgwire surface.

For how the same branching model extends beyond Datahike — to Stratum (SQL / columnar), vector and full-text indices, and other systems via a shared protocol — see Yggdrasil: Branching Protocols.

Known limitations

  • Multi-branch purge is expensive. purge removes datoms from the current branch; if you need them gone from every branch that referenced them (for GDPR or similar), the operation walks each branch independently. See Data Governance in Versioned Systems.
  • No built-in 3-way merge. Datahike doesn’t ship one because the right resolution rule is domain-specific. The Datalog patterns above cover the common shapes.
  • pg-datahike write-side merge-db is not yet exposed over SQL. Reads against any branch work; writes always land on the connection’s default branch in 0.1.
  • Branch-diff is O(differing datoms). The query walks both trees. For a 100M-datom database with a small diff, this is fast; for a diff that spans most of the tree, plan accordingly.

Try it

The branching API is in datahike.api (with branch-history still in datahike.versioning). For SQL access, see pg-datahike and the wire-protocol writeup. Repo: github.com/replikativ/datahike.

Feedback to contact@datahike.io or open an issue.

Permalink

Clojure 1.12.5

Clojure 1.12.5 is now available! Find download and usage information on the Downloads page.

  • CLJ-2945 - reify - incorrectly transfers reader metadata to runtime object

  • CLJ-2228 - constantly - unroll to remove rest args allocation

Permalink

Agent-Ready Stack

I keep seeing people share vibe-coded apps built on TypeScript/React + Supabase — seemingly the default recommendation from Lovable or Cursor. As a Clojure programmer, I can't stay quiet about this. In an era where AI agents are deeply embedded in the development workflow, that choice carries structural hidden costs that almost nobody is talking about.

Context Window Is the Bottleneck, and Framework Design Determines Burn Rate

LongCodeBench research shows that Claude 3.5 Sonnet's accuracy on bug-fixing tasks drops from 29% to 3% as context grows from 32K to 256K tokens. Chroma tested 18 frontier models and found the same pattern across all of them.

Coding agents accelerate this degradation: every tool call, every file read, every error message accumulates in the context. A 30-step agent session can consume more than ten times the context of a single conversation turn.

Countless efforts are already underway to manage context from the harness-design side — but the tech stack itself has an enormous impact on context efficiency that rarely gets discussed.

Task-Relevant Subgraph

An AI agent completing a task doesn't need to read the entire codebase — only the files relevant to that task. Call this set the task-relevant subgraph. The size of the subgraph is determined by the architectural design of the framework, not by the model.

The problem with TypeScript + React + Supabase is that a single feature naturally spans multiple layers — component, hook, state, API client, type definition — each living in a different file. The subgraph starts large and only grows as shared dependencies accumulate.

AI tends to recommend the stack it was trained on the most, but "easy to generate" is not the same as "efficient for long-term AI-assisted development." These are two different things.

What Makes a Stack More Agent-Ready

My current go-to is Clojure Stack Lite, and several of its design choices structurally shrink the task-relevant subgraph.

HTMX eliminates implicit client state. React state is scattered across multiple interdependent files; to verify behavior, an agent has to simulate browser interactions. HTMX is driven by server responses, so an agent can verify with a plain curl — the response is an HTML fragment, right or wrong, no ambiguity.

HoneySQL eliminates implicit lazy loading. When an ORM produces an N+1 problem, the debug subgraph includes model definitions, association configs, and migration files, because the issue is buried in implicit behavior. HoneySQL expresses queries as SQL-as-data — no lazy loading, no association magic. N+1 can't happen silently, because the syntax simply doesn't allow it to sneak in. The debug subgraph shrinks from five files to one.

Blocking IO eliminates implicit error paths. The fundamental problem with async isn't the syntax — it's that error paths are implicit. Every async call site is a potential break point where an exception can detach from the main flow. To locate a root cause, an agent must trace the entire call chain, and context width grows linearly with chain length. Clojure's blocking IO has no async boundaries; exceptions follow a single path — propagate upward, handled uniformly in middleware. When debugging, an agent only needs two places: the middleware log and the call site the log points to. Context scope stays fixed regardless of system size.

Explicit Over Implicit Is Not Just a Clojure Virtue

All three points share a common structure: the less implicit behavior, the smaller the context an agent needs to bring in.

The point here isn't a framework or language comparison — it's an observation about design philosophy. Explicit over implicit is a virtue for human developers; for AI agents, it's a structural guarantee that they won't go dumb prematurely.

Design principles the Clojure community has championed for years happen to be a competitive advantage in the AI agent era. I've chosen to frame this in terms of context efficiency, hoping it helps more people appreciate what the Clojure community figured out a long time ago.

Permalink

ifgame - An Interactive Fiction game library for Clojure

On of the first programs I ever ran on a computer was a text adventure game, also known as Interactive Fiction. I think the first one I played was Adventureland by Scott Adams, which was based on the first ever text adventure called Adventure by Crowther and Woods. Adventureland was the first text adventure available for personal computers.

Not long after that I discovered Zork I, the first game by Infocom. I loved the Infocom games, I played most of them, spending many hours solving the games.

Permalink

Datahike Speaks Postgres

Datahike Speaks Postgres

May 2026

Open psql. Connect. Run a query. Switch branches. Run it again — same connection, same wire protocol, different version of the database.

$ psql postgresql://localhost:5432/inventory
inventory=> SELECT count(*) FROM widget;
 count
-------
  4218

inventory=> SET datahike.branch = 'pricing-experiment';
SET
inventory=> SELECT count(*) FROM widget;
 count
-------
  4221

inventory=> RESET datahike.branch;
SET

That’s not a feature toggle on a Postgres replica. It’s the same database — addressed through standard pgwire — viewed through two different commits. The implementation is pg-datahike, a beta we’re shipping today.

What it is

pg-datahike embeds a PostgreSQL-compatible adapter inside a Datahike process: wire protocol, SQL translator, virtual pg_* and information_schema catalogs, constraint enforcement, schema hints. Clients that speak Postgres talk to Datahike without a Postgres install — pgjdbc, Hibernate, SQLAlchemy, Odoo 19, and Metabase bootstrap unmodified against it. The migration path is round-trippable: pg_dump output replays into pg-datahike via psql, and the standalone jar dumps Datahike databases back out as portable PG SQL. Detailed test results at the end of this post.

A 60-second tour

The operator runs one jar. Everything else is psql.

$ java -jar pg-datahike-VERSION-standalone.jar
pg-datahike VERSION ready on 127.0.0.1:5432
  backend:  file (~/.local/share/pg-datahike)
  history:  off
  CREATE DATABASE:  enabled
  databases: ["datahike"]

Connect with: psql -h 127.0.0.1 -p 5432 -U datahike datahike
Press Ctrl+C to stop.

JDK 17+ is the only prerequisite; the jar is on GitHub releases. --memory for an ephemeral run; --help covers the rest.

The rest is psql — provision a fresh database, populate it, pin a session to a historical commit, drop it.

$ psql postgresql://localhost:5432/datahike

datahike=> CREATE DATABASE inventory;
CREATE DATABASE
datahike=> \c inventory
You are now connected to database "inventory".

inventory=> CREATE TABLE widget (sku TEXT PRIMARY KEY, weight INT);
CREATE TABLE
inventory=> INSERT INTO widget VALUES ('A', 10), ('B', 20);
INSERT 0 2
inventory=> SELECT datahike.commit_id();
                commit_id
---------------------------------------
 b4f2e1c0-2feb-5b61-be14-5590b9e01e48      ← copy this

inventory=> INSERT INTO widget VALUES ('C', 30);
INSERT 0 1
inventory=> SELECT count(*) FROM widget;
 count
-------
     3

inventory=> SET datahike.commit_id = 'b4f2e1c0-2feb-5b61-be14-5590b9e01e48';
SET
inventory=> SELECT count(*) FROM widget;     -- the database before the third insert
 count
-------
     2

inventory=> RESET datahike.commit_id;
SET
inventory=> \c datahike
datahike=> DROP DATABASE inventory;
DROP DATABASE

SET datahike.commit_id pins the session to a historical commit; everything else is plain Postgres. Sixty seconds, one jar, no Postgres install, no Clojure.

Architecture in one minute

What happens when you SET datahike.branch = 'feature'?

Datahike stores its database as a tree of immutable nodes in konserve, a key-value abstraction over filesystems, S3, JDBC, IndexedDB, and others. Every transaction writes new nodes for changed paths and shares unchanged subtrees with the previous version — the trick behind Clojure’s persistent vectors and Git’s object store. A commit is a small map listing the root pointers for each index; a branch is a named pointer at a commit.

So on SET datahike.branch = 'feature', the handler updates a session variable, and the next query loads that branch’s commit pointer from konserve, walks the tree, returns rows. No coordination with a transactor; storage is the source of truth. SET datahike.commit_id = '<uuid>' works the same way one level deeper — the session points at a specific commit instead of a branch head.

Two consequences worth flagging:

  • Branching is one konserve write. Creating a branch from any commit is constant time, regardless of database size, because structural sharing means the new branch points at existing nodes.
  • Reads don’t go through a transactor. Every node is content-addressable; any process that can read the storage can run queries against it. In principle, read fanout is bounded by storage bandwidth, not replica capacity — we’ll publish numbers in a follow-up. See Memory That Collaborates for more.

Integration patterns

1. Multi-database server

A single start-server call serves many Datahike connections. Clients route on the JDBC URL’s database name:

What is this syntax?
pg/start-server({"prod" prod-conn
                 "staging" staging-conn
                 "reports" reports-conn}
  {:port 5432})
(pg/start-server {"prod"    prod-conn
                  "staging" staging-conn
                  "reports" reports-conn}
                 {:port 5432})

Same shape on the standalone jar with repeatable --db flags: java -jar pg-datahike.jar --db prod --db staging --db reports.

jdbc:postgresql://localhost:5432/prod      → prod-conn
jdbc:postgresql://localhost:5432/staging   → staging-conn
jdbc:postgresql://localhost:5432/nonsuch   → 3D000 invalid_catalog_name

SELECT current_database() returns the connected name; pg_database enumerates the registry. Useful for multi-tenant deployments, or when ops wants one pgwire endpoint serving many independent stores.

2. Schema hints

Existing Datahike schemas don’t always look the way you’d want them to over SQL. :datahike.pg/* meta-attributes customize the SQL view without touching the underlying schema:

What is this syntax?
pg/set-hint!(conn :person/full_name {:column "name"})
pg/set-hint!(conn :person/ssn {:hidden true})
pg/set-hint!(conn :person/company {:references :company/id})
(pg/set-hint! conn :person/full_name {:column "name"})           ; rename the column
(pg/set-hint! conn :person/ssn       {:hidden true})             ; exclude from SQL
(pg/set-hint! conn :person/company   {:references :company/id})  ; FK target

After set-hint!, SELECT name FROM person works, ssn is invisible to SELECT * and information_schema.columns, and JOIN company c ON p.company = c.id resolves on Datahike’s native ref semantics.

3. Time-travel via SET

Datahike’s temporal primitives are exposed as session variables. The client doesn’t need to know what as-of means — it just sets a variable:

SET datahike.as_of   = '2024-01-15T00:00:00Z';  -- d/as-of
SET datahike.since   = '2024-01-01T00:00:00Z';  -- d/since
SET datahike.history = 'true';                  -- d/history
RESET datahike.as_of;

Every subsequent query in the session sees the chosen view. A reporting tool that doesn’t know about Datahike can produce point-in-time reports by setting one variable.

4. Git-like branching

Branching is cheap in Datahike: every transaction produces a new immutable commit, so a branch is just a named pointer at a commit UUID. Creation is O(1) — one konserve write, no data copy, no WAL replay. pgwire exposes the read side and the admin operations through standard PG mechanisms:

-- Introspect
SELECT datahike.branches();
SELECT datahike.current_branch();
SELECT datahike.commit_id();

-- Admin (konserve-level writes — they don't go through the tx writer)
SELECT datahike.create_branch('preview', 'db');     -- 'db' is Datahike's default branch name
SELECT datahike.create_branch('from-cid', '69ea6ee1-…');
SELECT datahike.delete_branch('preview');

-- Session view: three cuts on the same immutable log.
-- They compose — a feature branch's state as of yesterday is two SETs.
SET datahike.branch    = 'feature';
SET datahike.commit_id = '69ea6ee1-2feb-5b61-be14-5590b9e01e48';
SET datahike.as_of     = '2024-01-15T00:00:00Z';

Or pin a branch at connect time via the JDBC URL:

jdbc:postgresql://localhost:5432/prod:feature   → prod-conn, pinned to :feature
jdbc:postgresql://localhost:5432/prod           → prod-conn, default branch

SET datahike.commit_id = '<uuid>' is Datahike-unique: no other PG-compatible database lets a session pin to an exact commit identifier.

We’ll cover the structural-sharing model that makes branching this cheap in a follow-up post — including how it works across all the Datahike bindings, not just pgwire.

5. SQL-driven database provisioning

Set a :database-template on the server and pgwire clients self-provision and tear down databases over plain SQL. The template is a partial Datahike config; each CREATE DATABASE produces a fresh store with a generated UUID:

What is this syntax?
pg/start-server({"datahike" boot-conn}
  {:port 5432 :database-template {:store {:backend :memory} :schema-flexibility :write :keep-history? true}})
(pg/start-server {"datahike" boot-conn}
                 {:port 5432
                  :database-template {:store {:backend :memory}
                                      :schema-flexibility :write
                                      :keep-history? true}})

WITH clauses override the template per-database, and the SQL surface accepts both standard PG forms:

CREATE DATABASE myapp;                              -- inherits the template
CREATE DATABASE histdb WITH KEEP_HISTORY = true;    -- override per database
CREATE DATABASE memdb  WITH (BACKEND = 'memory',    -- Yugabyte-style paren form
                             INDEX   = 'persistent-set');
DROP DATABASE myapp;
DROP DATABASE IF EXISTS old_one;

Accepted WITH keys map case-insensitively to Datahike config:

WITH option Datahike config Notes
BACKEND [:store :backend] 'memory', 'file' built-in; 'jdbc', 's3', 'redis', 'lmdb', 'rocksdb', 'dynamodb' via external konserve libraries
STORE_ID [:store :id] Defaults to a fresh UUID per CREATE
PATH [:store :path] File backend; {{name}} interpolation supported
HOST / PORT / USER / PASSWORD / DBNAME [:store :*] jdbc / redis backends
SCHEMA_FLEXIBILITY :schema-flexibility 'read' or 'write'
KEEP_HISTORY :keep-history?
INDEX :index 'persistent-set':datahike.index/persistent-set
OWNER / TEMPLATE / ENCODING / LOCALE / TABLESPACE / … Postgres-only; silently accepted with a NOTICE so pg_dump round-trips work

The standalone jar enables this by default (use --no-create-database to disable). Embedded servers opt in via :database-template (or explicit :on-create-database / :on-delete-database hooks). Without one, CREATE / DROP DATABASE return SQLSTATE 0A000 feature_not_supported; mismatched preconditions return the standard PG SQLSTATEs.

Migrating from PostgreSQL

Wire compatibility extends to pg_dump SQL on both sides. Three workflows.

Real PostgreSQL → pg-datahike

pg_dump output replays straight into pg-datahike via psql or any JDBC client. Schema-side coverage: CREATE TABLE with FK constraints, CREATE SEQUENCE, DEFAULT nextval(…), CREATE TYPE … AS ENUM, CREATE DOMAIN, partitioned tables. Data-side: INSERT (single + multi-VALUES) and COPY … FROM stdin (text and CSV).

Run with the :pg-dump compat preset to silently accept constructs pg-datahike doesn’t model — triggers, functions, materialized views, ALTER OWNER:

java -jar pg-datahike.jar --compat pg-dump
psql -h localhost -p 5432 -U datahike -d datahike -f my_pg_dump.sql

Validated end-to-end against Chinook (15.6k rows, 11 tables, FKs, NUMERIC, TIMESTAMP) — full byte-identical bidirectional roundtrip — and Pagila (50k rows, 22 tables, ENUM, DOMAIN, partitioning, triggers, functions) — schema parses end-to-end, data loads.

pg-datahike → portable PG SQL

The standalone jar’s dump subcommand walks a Datahike database and emits pg_dump-shaped SQL. The output replays into either pg-datahike or real PostgreSQL via psql:

java -jar pg-datahike.jar dump --data-dir DIR --db NAME --out out.sql
java -jar pg-datahike.jar dump --config datahike-config.edn --copy

Flags cover INSERT-vs-COPY output, schema-only / data-only, and table exclusion. --config accepts a full Datahike config EDN, so any konserve backend works; store-id is auto-discovered.

What the resulting Datahike schema looks like

A native Datahike database — created with d/transact, never touched by SQL — also dumps as clean PG SQL. The inverse mapping is well-defined:

  • :db.unique/identityPRIMARY KEY NOT NULL
  • :db.unique/valueUNIQUE
  • :db.cardinality/many TT[] with PG array literals
  • :db.type/refbigint (the entity id; opt in to FK constraints with set-hint! :references)

So whether you start from a real PostgreSQL dump or from native Datahike, both sides translate cleanly through the same shape. The resulting schema is correct and queryable as both SQL relations and Datalog datoms. It isn’t always what you’d hand-design for entity-shaped Datalog queries — many apps stay with the relational shape, others evolve incrementally as they reach for Datalog’s strengths (pull patterns, rules, multi-source joins).

What it isn’t

This is a 0.1 beta and we want to be specific about the gaps:

  • PL/pgSQL, stored functions, triggers, rules, and materialized views are accepted under the :pg-dump compat preset (loaded but not executed); strict mode rejects them
  • No LISTEN / NOTIFY
  • No COPY … TO STDOUT (COPY … FROM stdin is supported in text and CSV formats)
  • FK ON DELETE enforced for NO ACTION / RESTRICT / CASCADE; SET NULL / SET DEFAULT and any ON UPDATE action are rejected at DDL
  • Single public schema — CREATE SCHEMA is silently accepted but a no-op
  • Cursor materialization is eager (entire result set held in memory)
  • No deferrable constraints
  • Generated columns parse but aren’t enforced
  • Writes always land on the connection’s default branch in 0.1, even when SET datahike.branch is active. Reads respect the pinned branch; writes don’t yet. Use datahike.versioning/branch! and merge! from Clojure for branch-targeted writes, or open a second connection on /<db>:<branch>.
  • Constraint enforcement is one-directional. SQL constraints declared via DDL (NOT NULL, CHECK, UNIQUE, FK RESTRICT) are enforced by the pgwire handler; direct (d/transact) writes from Clojure bypass them because Datahike’s schema doesn’t yet carry the constraint vocabulary. A future release will lift enforcement into the tx layer so both paths are gated.
  • Bulk-insert throughput is ~5,000 rows/sec on JDBC batch (Pagila replays in ~12s, Chinook in ~3s) — Datahike maintains EAVT/AEVT/AVET live, so a 10-column row costs ~10× a single index write. Tuned bulk paths in vanilla PG (COPY, pg_restore -j) are an order of magnitude faster, partly via deferred index construction; an analogous bulk-load fast path is a future item. Large migrations are overnight-cutover territory today.

The conformance posture is: pass for the workloads we’ve measured against, fail fast and loud everywhere else. We’d rather reject a stored procedure than execute it incorrectly.

Where this fits

If you’ve used Neon or Xata, the goal will look familiar — branchable Postgres. The mechanism is different. Their branches are control-plane operations: call the API, get a new compute instance over copy-on-write storage. pg-datahike’s branches are session-level — SET datahike.branch = 'feature' inside an open psql connection switches what you’re reading. No provisioning, no compute. An agent or a query planner can switch branches mid-session.

Commit pinning — SET datahike.commit_id = '<uuid>' — is the part where we don’t know of a peer. Neon’s time-travel is bounded by a 6h–1d restore window; pg-datahike pins to any historical commit, indefinitely. We have not seen another PG-compatible database expose this directly through the wire protocol.

Dolt is the closest in spirit — git-like semantics, commit pinning, time-travel — but Dolt is MySQL with a custom storage engine. pg-datahike rides on the standard Postgres wire protocol; every PG client works without modification.

The honest tradeoff: we are a compatibility layer over Datahike’s storage, not a fork of Postgres. Some features tied to the Postgres codebase — PL/pgSQL, the extension ecosystem, procedural languages — aren’t on our roadmap today. If you need those, use Postgres. If your bottleneck is versioning, branching, or reproducibility, this gets you there without leaving the wire protocol your tools already speak.

Datahike has been a Datalog database with a Clojure API and growing language bindings; pg-datahike isn’t a separate database, just another front end on the same store. There’s a sibling: Stratum, a SIMD-accelerated columnar engine that speaks the same wire protocol over an analytical column store with the same fork-as-pointer semantics. Both fit into a shared branching model — see Yggdrasil: Branching Protocols for how a Datahike database, a Stratum dataset, and a vector index can fork together at a single snapshot.

The rest of this post is for callers who do speak Clojure — the same data accessible as relations and as datoms, in-process queries that skip the wire, embedded mode without TCP, and configuration knobs that aren’t exposed over SQL.

Bidirectional view

The pgwire layer is a view onto Datahike’s datom store, not a separate representation. Tables you create over SQL show up as normal Datahike schemas, queryable from Clojure with (d/q …). Existing Datahike schemas show up as SQL tables with no setup.

What is this syntax?
;; Plain Datahike schema, transacted from Clojure
d/transact(conn
  [{:db/ident :person/id :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one :db/unique :db.unique/identity}
   {:db/ident :person/name :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}])

d/transact(conn [{:person/id 1, :person/name "Alice"}])
;; Plain Datahike schema, transacted from Clojure
(d/transact conn
  [{:db/ident :person/id   :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one :db/unique :db.unique/identity}
   {:db/ident :person/name :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}])

(d/transact conn [{:person/id 1 :person/name "Alice"}])
-- Same database, over psql:
SELECT * FROM person;
--   id |  name
--  ----+-------
--    1 | Alice

The reverse holds too — CREATE TABLE over pgwire transacts a normal Datahike schema, and the next (d/q …) from Clojure sees the rows you just inserted. There is no shadow representation, no separate metadata. One datom store, two query languages.

Using the library directly

Two ways to skip the standalone jar — start a server from your own JVM application, or bypass the wire layer entirely.

Start a server in-process

What is this syntax?
;; deps.edn
{:deps {org.replikativ/datahike {:mvn/version "LATEST"}
        org.replikativ/pg-datahike {:mvn/version "LATEST"}}}
;; deps.edn
{:deps {org.replikativ/datahike    {:mvn/version "LATEST"}
        org.replikativ/pg-datahike {:mvn/version "LATEST"}}}
What is this syntax?
require('[datahike.api :as d] '[datahike.pg :as pg])

let [boot {:store {:backend :memory, :id random-uuid()}, :schema-flexibility :write}]:
  d/create-database(boot)
  pg/start-server({"datahike" d/connect(boot)} {:port 5432, :database-template {:store {:backend :memory}, :schema-flexibility :write, :keep-history? true}})
end
;; => :running on :5432
(require '[datahike.api :as d]
         '[datahike.pg  :as pg])

(let [boot {:store {:backend :memory :id (random-uuid)}
            :schema-flexibility :write}]
  (d/create-database boot)
  (pg/start-server {"datahike" (d/connect boot)}
                   {:port 5432
                    :database-template {:store {:backend :memory}
                                        :schema-flexibility :write
                                        :keep-history? true}}))
;; => :running on :5432

Same pgwire surface, in-process. The integration patterns earlier in this post are the embedded-library API; the standalone jar wraps the same calls behind CLI flags.

Bypass the wire entirely

Tests and in-process applications don’t need the wire layer at all:

What is this syntax?
def h: pg/make-query-handler(conn)
h.execute("CREATE TABLE person (id INT PRIMARY KEY, name TEXT)")
h.execute("INSERT INTO person VALUES (1, 'Alice')")
h.execute("SELECT * FROM person")
(def h (pg/make-query-handler conn))
(.execute h "CREATE TABLE person (id INT PRIMARY KEY, name TEXT)")
(.execute h "INSERT INTO person VALUES (1, 'Alice')")
(.execute h "SELECT * FROM person")

Same SQL surface, no socket. Useful for property-based testing of SQL workloads, or for embedding the SQL interface inside a Clojure or ClojureScript application without exposing a port.

Permissive vs. strict compat

By default the handler rejects unsupported DDL — GRANT, REVOKE, CREATE POLICY, ROW LEVEL SECURITY, CREATE EXTENSION, COPY — with SQLSTATE 0A000 feature_not_supported. Most ORMs emit some of these unconditionally. Two ways to relax:

What is this syntax?
;; silently accept every auth/RLS/extension no-op (Hibernate, Odoo)
pg/make-query-handler(conn {:compat :permissive})

;; accept specific kinds only
pg/make-query-handler(conn {:silently-accept #{:grant :policy}})
;; silently accept every auth/RLS/extension no-op (Hibernate, Odoo)
(pg/make-query-handler conn {:compat :permissive})

;; accept specific kinds only
(pg/make-query-handler conn {:silently-accept #{:grant :policy}})

The named presets in datahike.pg.server/compat-presets cover the common ORM patterns.

SQL or Datalog?

Both interfaces see the same datoms, the same indexes, the same history. The choice is about how the query reaches the engine.

Reach for SQL when callers don’t share a runtime with the database — services over the wire, analysts in Metabase, tools that only speak the wire protocol — or when you want existing tooling: ORMs, migration runners, BI dashboards.

Reach for Datalog when the query runs in the same process as the database. Datahike’s Datalog API is a Clojure function: pass values in, get values out, no parsing, no serialization, no socket. Even pg-datahike’s embedded mode (the make-query-handler path shown above) still goes through the SQL parser and the translator; Datalog skips both. You can invoke arbitrary Clojure functions inside predicates, return live data structures without copying, and join across multiple databases on different storage backends in a single query.

The two paths compose. DDL via Flyway over SQL, then reads in Datalog from your Clojure backend. Or: Datahike schema in Clojure, ORM-driven CRUD over SQL. Both stay coherent because they’re views of the same datom store.

Compatibility evidence

We test pg-datahike against the same suites the Postgres ecosystem uses on itself. If a suite passes here, the apps that depend on it generally work here.

Layer Test suite Result What this proves
JDBC driver pgjdbc 42.7.5 — ResultSetTest 80 / 80 Cursors, type decoding, and metadata behave the way every JVM Postgres client expects.
Java ORM Hibernate 6 — DatahikeHibernateTest 13 / 13 JPA stacks — Spring, Quarkus, Jakarta — talk to pg-datahike the same way they talk to Postgres.
Python ORM SQLAlchemy 2.0 dialect 16 / 16 across 7 phases The Python data ecosystem — Django, Flask, FastAPI, Airflow, dbt — connects via the standard dialect path.
SQL semantics sqllogictest 779 assertions, 61 files Cases derived from PostgreSQL's regression suite, expressed in the sqllogictest format SQLite, CockroachDB, and DuckDB use for their own correctness work.
Real application Odoo 19 — --init=base --test-tags=:TestORM 11 / 11 cases, ~38k queries, zero translator errors A 200-table ERP with one of the most demanding open-source ORM layers boots and passes its own test suite.
BI tool Metabase native SQL 20-probe MBQL sweep Schema introspection, prepared statements, and result handling work for the paths real BI tools depend on.
Migration roundtrip Chinook + Pagila pg_dump fixtures Chinook: byte-equal roundtrip. Pagila: schema parses, data loads. A real Postgres database can be exported, replayed in pg-datahike, and dumped back — schema and data preserved through the round-trip.
Internal Unit suite 544 tests, 1603 assertions Standard regression coverage.

Per-commit suites run on CircleCI. Odoo, Metabase, and psql / libpq (\d, \dt, \df family) are run on a manual harness before each release. A dedicated compatibility page with linked test artifacts and a published gaps registry is in flight.

Try it

Download the jar from GitHub releases, java -jar pg-datahike-VERSION-standalone.jar, point psql at it. To embed in a JVM app, the coordinate is org.replikativ/pg-datahike on Clojars. Repo, docs, and issues at github.com/replikativ/pg-datahike; feedback to contact@datahike.io.

A follow-up post will cover the structural-sharing model that makes branching O(1), what merge! does, and the same workflow across every Datahike binding (Clojure, Java, JavaScript, Python, the C library, the CLI, and SQL). Subscribe to the RSS feed.

Permalink

1.12.145 Release

We’re happy to announce a new release of ClojureScript. If you’re an existing user of ClojureScript please read over the following release notes carefully.

Async Functions

Now that ClojureScript targets ECMAScript 2016 we can carefully choose new areas of enhanced interop. Starting with this release, hinting a function as ^:async will make the ClojureScript compiler emit an JavaScript async function:

(refer-global :only '[Promise])

(defn ^:async foo [n]
  (let [x (await (Promise/resolve 10))
        y (let [y (await (Promise/resolve 20))]
            (inc y))
        ;; not async
        f (fn [] 20)]
    (+ n x y (f))))

This also works for tests:

(deftest ^:async defn-test
  (try
    (let [v (await (foo 10))]
      (is (= 61 v)))
    (let [v (await (apply foo [10]))]
      (is (= 61 v)))
    (catch :default _ (is false))))

In the last Clojure survey support for async functions dominated the list of desired ClojureScript enhancements for JavaScript interop. This enhancement eliminates the need to take on additional dependencies for the common cases of interacting with modern Browser APIs and popular libraries.

For a complete list of fixes, changes, and enhancements to ClojureScript see here

Contributors

Thanks to all of the community members who contributed to ClojureScript 1.12.145

  • Michiel Borkent

Permalink

Building Data-Heavy Systems in Clojure Without Losing Simplicity

The Moment Everything Breaks (And Why It Always Happens)

It often begins the same way. The system performs well, traffic increases, data volumes grow, and new features accumulate. Then, gradually, performance degrades. Deployments slow down, bugs become harder to trace, and engineers spend more time debugging than building. What once felt scalable begins to feel fragile.

This is the underlying challenge of data-intensive systems: as data grows, complexity tends to grow with it.

Most teams respond predictably—by adding more tools, more layers, and more abstractions. But this often compounds the problem rather than solving it.

What if the solution to scale isn’t added complexity, but reduced complexity? This is the core philosophy behind Clojure, created by Rich Hickey.

This guide explores how to build scalable data architectures using simple, data-centric approaches—without compromising performance, reliability, or developer productivity.

The Problem: Why Data-Heavy Systems Become Unmanageable

🔹 The “Box Problem” in Traditional (Object-Oriented) Systems

In Java, data is wrapped inside objects. 

It works at the beginning of the application. But over time, complexity accumulates and becomes harder to manage. Why?

Because objects hide data:

  • Teams lack visibility into the system’s contents without performing analysis.
  • Logic and data are tightly coupled.
  • Changes ripple unpredictably.

This reflects a core limitation of Object-Oriented Programming: teams gradually shift from working with data to contending with the systems that encapsulate it.

🔹 Hidden Mutations = Invisible Bugs

In mutable systems:

  • One service updates data.
  • Another service reads an outdated state.
  • A third service overwrites everything.

Now imagine this happening across:

  • Microservices.
  • APIs.
  • Streaming pipelines.

Teams get:

  • Race conditions.
  • Data corruption.
  • Impossible-to-reproduce bugs.

📌This is why we at Flexiana believe in Functional Programming Fits Data-Heavy Systems.

🔹 Complexity Grows Faster Than Data

Complexity outpaces data growth, and that’s where things get messy.

As systems grow, teams often introduce:

  • Caching layers.
  • Queue systems.
  • Synchronization mechanisms.

Each “solution” adds more complexity.

📌Google’s SRE guidelines are pretty clear: if you make things complicated, you’re asking for trouble. Reliability drops, so keeping things simple really matters.

The Clojure Philosophy: Simple Data Over Complex Abstractions

Clojure takes a totally different approach. Forget all those complicated wrappers and abstractions. It just treats data as data — plain and straightforward. Do not stack items; no unnecessary layers.

🔹 Plain Maps and Vectors 

In Clojure:

In Clojure, data is represented as plain maps, vectors, and sets. No classes. No hidden behavior. Just data that is easy to inspect, easy to serialize (JSON, EDN), and easy to transform across services, pipelines, and systems without rewriting everything.

🔹 Why This Matters for Data-Heavy Systems

  • Easy to inspect.
  • Easy to serialize (JSON, EDN).
  • Easy to transform.

You can pass data across:

  • Services.
  • Pipelines.
  • Systems.

Without rewriting everything.

🔹 Structural Sharing (Scale Without Memory Explosion)

Clojure uses persistent data structures. There are no full dataset copies — it reuses what’s the same and stores only what’s new. Teams end up with millions of records but almost no additional memory overhead

Teams end up with millions of records, but almost no additional memory gets used.

🔹 Immutability: The Foundation of Simplicity

Immutability is the core idea. Once the team creates data, it stays exactly as it is — no messing around, no changes. That’s where the simplicity comes from. Instead:

  • New versions are created.
  • Old versions remain intact.

This eliminates:

  • Side effects.
  • Unexpected state changes.

And enables safe concurrency.

Keeping Data Correct with Malli (Schema Without Pain)

The bigger a system gets, the trickier it is to keep data in line. Everyone is worried about data going off track—so how does a team maintain strict control? That’s where Malli steps in.

🔹 So, What is Malli Clojure?

It’s a lightweight schema library that validates data and ensures teams aren’t sending anything unusual. Simple as that.

Example:

Clear Errors Instead of Chaos

Whenever the app breaks down and produces unclear errors, Malli tells teams straight-up what’s wrong, so they can fix errors fast:

Instant Output:

🔹 Why Malli Fits Data-Heavy Systems

  • Teams benefit from flexible schemas that adapt to changing data as conditions evolve, avoiding rigid constraints that disrupt the flow and enabling seamless, continuous adaptation.  
  • Malli integrates seamlessly into environments where teams are managing growing datasets and evolving requirements.
  •  It is designed to scale, maintaining stability even when data becomes unpredictable.

🔹 Better Errors = Faster Debugging

  • Validation messages are precise and actionable, enabling teams to quickly identify both the location and the cause of issues.
  • Identify issues early, avoid costly downtime, and maintain uninterrupted system operations.
  • Because errors are clearly surfaced, teams spend less time diagnosing issues and more time resolving them.

Concurrency Without Chaos

Concurrency is where most systems break.

Locks. Deadlocks. Race conditions. Clojure avoids all of this.

🔹 Why Immutability Removes the Need for Locks

  • Because data is immutable, multiple threads can read it safely without requiring synchronization.

This is a direct benefit of:

  • Functional Programming.

🔹 core.async and Event Streams

core.async makes handling streams simple.

Example:

🔹 Scaling with Simplicity

  • Fewer race conditions:
    • No more problems from the shared state. 
    • Data flows in a way that teams can actually follow.
    • Parallel code runs safely.
    • Teams sidestep those troublesome timing errors.
  • Debugging doesn’t have to be difficult:
    • Data is not buried within opaque structures—it remains explicit and directly accessible in maps.
    • The absence of side effects makes issues easier to trace and resolve.
    • Teams get the same results wherever they run their code.
    • Clear error signals (especially with Malli).

📌 See how this worked for a real-world high-traffic site in our Livesport Case Study.

The REPL Advantage: Building Systems Live

🔹 Instant Feedback Loop

Teams move beyond the traditional cycle of writing, building, deploying, and waiting. With a REPL, they can execute code immediately and receive instant feedback.

🔹 Test with Real Data

Need to understand how changes behave with real data? Simply load production data, experiment with live transformations, and debug in real time—while the system continues to run.

🔹 Continuous System Evolution

The overhead of long build cycles is eliminated. Teams can shape and refine their systems in real time, without delays or uncertainty.

🎬 Clojure exemplifies this approach—teams aren’t just writing code; they are interactively evolving their systems. You can see this in action in the video here:

Designing a Scalable Data Architecture in Clojure

As systems begin to handle larger volumes of data, complexity can escalate quickly. Some systems continue to perform reliably, while others struggle under the load. The difference is rarely accidental—it is largely determined by the underlying architecture.

Clojure takes a different path. It keeps things simple from the start—and that’s what makes it scale.

🔹 Core Principles of Simple Data Systems

Data-First Design

In many systems, logic comes first. Data is secondary. In Clojure, it’s the opposite. Data comes first.

  • Teams use maps, vectors, and sets.
  • Data is easy to read and inspect.
  • Nothing is obscured behind layers of objects; data remains transparent and directly accessible.

And that changes how you build systems. Instead of designing classes, teams work with data flows.

Why this helps:

  • Debugging is easier.
  • Data moves cleanly between services.
  • Teams don’t break things when requirements change.

Stateless Services

Each part of the system does one simple thing:

  • Takes data in.
  • Changes it.
  • Returns new data.

That’s it. No hidden state. No surprises. This works because of:

  • Immutability.
  • Functional Programming.

What teams get:

  • Teams can scale services easily.
  • Running things in parallel is safe.
  • Testing becomes straightforward.

➜ Clear Boundaries

As systems grow, boundaries tend to blur. One service starts doing too much. Data shapes drift.

Clojure pushes you to keep things clear.

  • Define what data should look like.
  • Validate it using tools like Malli.
  • Keep the functions concise and to the point.  

When teams do this, 

  • Each service runs on its own—so if something crashes, it doesn’t drag everything down with it.
  • Teams don’t end up with problems bouncing around the whole system. 
  • The system remains transparent and simple.

🔹 Recommended Stack

Clojure Backend

Clojure keeps backend logic simple. Teams use small functions to shape data, so everyone’s right next to the real information. It clears out the interference. 

Fewer lines of code mean teams reduce risks and clarify their goals.

Event-Driven Architecture

Instead of calling each other up, services just broadcast events to the world, allowing the appropriate recipients to receive them.

So, when something happens, teams create an event. The rest of the system listens and responds as needed. It’s a cleaner way to connect everything without binding them too firmly. Everything runs independently.

As Martin Fowler explains, event sourcing lets teams rebuild system state by replaying events. That makes systems easier to scale and debug.

What this gives teams:

  • Loose coupling.
  • A clear history of what happened.
  • The ability to replay and fix issues. 

🔹 Patterns to Follow

Data Pipelines

Think of the system as a pipeline. 

Each step is simple:

  • Take data.
  • Return new data.

Why this works:

  • Easy to follow.
  • Easy to test.
  • Easy to scale.

Event Sourcing

Save the full history, not just the current version. For example:

  • UserCreated
  • OrderPlaced
  • PaymentProcessed

The state results from all these events.

As Martin Fowler points out, this lets you:

  • Rebuild the state anytime.
  • Debug past issues.
  • Keep systems resilient.

Functional Transformations

In Clojure, most work is done through small functions.

Simple. Predictable. Testable.

 Why it matters:

  • No side effects.
  • Same input → same result.
  • Easy to test.

🔹 Example: A Transformation Pipeline

What does this indicate:

  1. Check if the input is good. 
  2. Add extra data.
  3. Apply business logic.
  4. Return a new version.

No mutation. No hidden steps.

📌 Martin Fowler puts it well: event sourcing lets a system rebuild everything it needs just by replaying a series of events. This keeps systems solid and ready to scale.
📌With Apache Kafka, data doesn’t sit stuck in batches—you receive it in real time.

Why Simplicity Wins (Business Perspective)

It’s not only about clean code. Simple systems save money, let teams move faster, and prevent failures. They’re easier to handle and easier to expand.

🔹 Lower Infrastructure Costs

Complex systems grow in layers.

More services, more duplication, more overhead. Simple systems stay lean.

  • Data is stored and passed efficiently.
  • Fewer components doing the same work.
  • Less need for constant scaling.

What this means in practice:

  • Lower cloud bills.
  • Better performance with the same hardware.
  • Fewer surprises as data grows.

Teams are not paying extra just to manage complexity.

🔹 Faster Developer Onboarding

When a system is hard to read, new developers slow down. They depend on others to understand how things work. Simple systems remove that friction.

  • Code is clear and direct.
  • Data flows are easy to follow.
  • Logic is not hidden behind layers.

The impact:

  • New developers get productive fast.
  • Less reliance on “tribal knowledge”.
  • Teams spend more time building, less time explaining.

And when someone leaves, the system doesn’t become a mystery.

🔹 Reduced Failure Rates

When things get complicated, failures follow. Hidden states, complex dependencies, and surprise side effects make bugs a pain to find. But simple systems just work better—they’re easier to predict. 

  • Give them the same input, and they’ll spit out the same output every time. 
  • Teams don’t see weird interactions between parts, so problems don’t hide as easily. 
  • Tracking down issues gets a whole lot simpler.

What does this mean?

  • Teams deal with reduced production problems. 
  • If something does go wrong, teams fix it faster. 
  • There’s less downtime, and any issues that pop up don’t cause as much damage.

Real-World Use Cases

Simple data systems aren’t just a buzzword—it’s what keeps high-volume, real-time systems running smoothly, even as things shift and scale up.

🔹 High-Throughput Systems

Let’s begin with systems that receive events continuously, leaving little opportunity for delays or unforeseen errors.

Financial systems

Payment processing or trading platforms are pretty unforgiving. They process thousands of transactions each second, and teams can’t have mistakes or inconsistent data. 

Simple, data-focused architecture really shines here: each transaction is just data, tracked as an event that teams can review or replay. When things go wrong, it’s a lot easier to pinpoint exactly what failed and roll everything back to a safe place.

What does this fix?

  • Teams get clear audit trails.
  • It’s safer when many transactions occur at once.
  • Tracking down the root of any problem is much faster.

➜ Sports/live data platforms

These systems face heavy loads with real-time updates—millions of people are refreshing nonstop. No matter how wild the traffic gets, the data everywhere needs to stay perfectly in sync. 

That’s where tools like Apache Kafka help out. Score changes and other updates stream through constantly, and every part of the platform reacts right away, without missing a beat.

Why keep it simple?

  • Real‑time means updates don’t fail.
  • The system stays steady, even during huge spikes.
  • Finding and fixing live bugs isn’t a big deal.

🔹 AI/ML Data Pipelines

AI systems are addicted to clean, reliable data. That’s usually where messy architectures fail.

➜ Feature pipelines

Before models do anything, teams need to turn raw data into usable features. Data arrives continuously from everywhere, and each transformation has to stay consistent. 

With a basic pipeline, teams always know what’s happening at each step. It’s way easier to test things, catch mistakes, and keep everything running as expected.

What teams get:

  • Fewer strange data mix-ups.
  • Faster fixes when the models lose alignment.
  • Better performance as the models learn.

➜ Data preprocessing

Data preprocessing is just as important. This is all the data cleaning, normalization, and blank-filling before training or inference. 

If teams build things in a clear, functional way—no hidden side effects—each step is independent, and they can replay everything if needed.

That makes a difference:

  • The results can be reproduced, not simply assumed.
  • Testing and experimenting are smoother.
  • It’s less likely that teams will miss hidden data errors.
📌  If you’re fed up with struggling against chaotic, hard-to-understand systems and want to build something solid, we can help. Check out our AI and Machine Learning services.

🔹 Rich Hickey explains more about building data-heavy systems in his talks — explore below:

The ideas explored in this post are deeply rooted in the work of Rich Hickey. His talks have shaped how the Clojure community thinks about simplicity and data

The following talks by Rich Hickey form the intellectual foundation of the blog post “Building Data-Heavy Systems in Clojure Without Losing Simplicity.” These resources are recommended for further reading and should be linked from the blog where relevant.

1. Simple Made Easy (2011)

Link: https://www.youtube.com/watch?v=SxdOUGdseq4

Summary: Hickey’s most influential talk. Reframes how developers think about complexity and simplicity — the philosophical backbone of the blog’s entire approach

2. The Value of Values

Link: https://www.youtube.com/watch?v=-6BsiVyC1kM

Summary: A deep dive into why values win over variables. Hickey demonstrates that immutability eliminates entire categories of bugs around shared state, making it the natural foundation for concurrent, data-heavy systems.

3. Are We There Yet?

Link: https://www.youtube.com/watch?v=ScEPu1cs4l0

Summary: Explores how software should explicitly model time. Argues that values should be immutable by default and that mutable state is a source of accidental complexity. This talk is the intellectual foundation for Clojure’s design around immutable data structures.

4. Clojure Made Simple

Link: https://www.youtube.com/watch?v=028LZLUB24s 

Summary: Focuses specifically on Clojure’s two defining traits: data orientation and simplicity. Covers how these characteristics lead to faster time to market, smaller codebases, and better quality — exactly what the blog promises for data-heavy systems.

5. Deconstructing the Database

Link: https://www.youtube.com/watch?v=Cym4TZwTCNU

Summary: Hickey argues that traditional OOP and relational databases entangle value, identity, and state in ways that make reasoning about data evolution difficult. Directly relevant to the blog’s argument about avoiding hidden state in data-heavy systems.

6. The Language of the System

Link: https://www.youtube.com/watch?v=ROor6_NGIWU

Summary: Examines how the architecture of distributed systems (multiple communicating programs) compares to single-program architecture. Explores tradeoffs in data formats and what characteristics well-designed system components should have.

❓ FAQs 

Q1:  Why is Clojure good for data-heavy systems?

Because it keeps data simple. You work with maps and vectors. No hidden state. No complex object layers. So it’s easier to track data, change it, and debug issues—even when the system grows.

Q2: What makes Clojure simpler than Java?

It avoids a lot of moving parts.

  • No heavy object-oriented structure.
  • No mutable state by default.
  • Fewer abstractions.

You write less code. And it’s easier to see what’s going on.

Q3: Is functional programming better for big data?

Often, yes. Functional Programming removes side effects. That makes systems more predictable. When things are predictable, parallel execution is simple and stable.

Q4: What is Malli in Clojure used for?

It checks your data. You define what valid data looks like. Malli checks the data and makes it reliable across services.

Q5: How does immutability improve scalability?

If data doesn’t change, no race conditions or state‑sharing bugs.

If you want to update something, just make a new version instead of changing the old one. That means different parts of the system run in parallel without conflict.

 Scaling up gets a lot easier and less risky.

Q6: Can Clojure handle real-time data streams?

Absolutely. Clojure comes with tools like core.async, so you can process streams of data in real time. It lets you build systems that 

  • Keep up with data as it comes in.
  • Handle events right away.
  • Scale out without getting blocked. 

That’s why it’s such a good fit for streaming or event-driven applications.

Conclusion: Clojure Simplicity as a Competitive Advantage

Most teams believe: “Complex systems require complex solutions.”

Clojure proves the opposite.

By embracing:

  • Simple data.
  • Immutability.
  • Functional design.

You get:

  • Faster systems.
  • Lower costs.
  • Happier developers.

And most importantly: Teams build systems that don’t collapse under their own weight.

📞 Book a Scalable System Audit

Connect with Flexiana’s experts to get a clear view of your system. 

We’ll dig into your architecture, pinpoint what’s slowing you down, and work with you to map out a plan that simplifies your setup and makes it ready to grow.

The post Building Data-Heavy Systems in Clojure Without Losing Simplicity appeared first on Flexiana.

Permalink

Learn Ring - Session Vs Cookie

Cookies and sessions are both ways to persist data across HTTP requests, but they work differently:

  • Stored on the client (browser)
  • Data is sent with every HTTP request in the request header
  • Can persist for a set expiration time (even after the browser closes)
  • Limited to about 4KB of data
  • Can be read by JavaScript (unless HttpOnly flag is set)
  • Examples: remembering a user’s theme preference, tracking analytics

Session

  • Stored on the server
  • The browser only holds a small session ID (usually in a cookie)
  • Typically expires when the browser is closed (or after a server-defined timeout)
  • Can hold much larger amounts of data
  • More secure since the actual data never leaves the server
  • Examples: storing login state, shopping cart contents

How they work together

Browser                          Server
  |                                |
  |--- Request (with session ID) ->|
  |                                |--- Looks up session ID in store
  |                                |--- Retrieves session data
  |<-- Response -------------------|

A common pattern is: the server creates a session, stores the session ID in a cookie, and on each request the browser sends that cookie so the server can find the right session data.

Quick comparison

Feature Cookie Session
Storage location Client (browser) Server
Size limit ~4KB No strict limit
Lifetime Configurable Usually browser session
Security Less secure More secure
Performance Faster (no server lookup) Slightly slower
Use case Preferences, tracking Auth, sensitive data

Rule of thumb: Use cookies for small, non-sensitive data you’re okay storing client-side. Use sessions for sensitive data or anything that shouldn’t be exposed to the client.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.