biff 2026-08-02

@foo I'm doing a spike project to see if I can keep biff's (v2) dev ergonomics (I LOVE biff as solo developer btw ♥️ ) and use grain's event modeling primitives (command, event etc) While doing a devops test - claude found issue while running uberjar task biff needed + :biff.tasks/generate-assets-fn nik.grain-biff-spike/generate-assets key in config.edn and in v2 git branch's demo project config edn doesn't mention it so I wanted to check if this was a miss or there is a reason for this. Claude's report: *`uberjar`: found and fixed a real bug*, then hit a sandbox performance ceiling on full verification. First run failed immediately: IllegalArgumentException: Not a qualified symbol: at com.biffweb.tasks.uberjar/uberjar (uberjar.clj:19)uberjar requires :biff.tasks/generate-assets-fn (a qualified symbol resolved via requiring-resolve and called with the config map, meant for project-specific asset generation beyond Tailwind, which the task already handles itself via its own (biff.run/run-task "css" "--minify") call). Confirmed by reading com.biffweb.tasks.util.clj's config registration: the key is declared (`'qualified-symbol?`) but has no library-shipped default — and checking biff/demo's own resources/config.edn shows it doesn't set this key either, so this is either an undocumented requirement or biff/demo itself has never run uberjar for real. Fixed by adding a no-op nik.grain-biff-spike/generate-assets fn and pointing :biff.tasks/generate-assets-fn at it in config.edn.

I haven't gotten to cleaning up the v2 biff.tasks code yet, so I may still need to make some decisions around what exactly v2 will do. but in the v1 starter app, the generate-assets-fn does this:

(defn generate-assets! [ctx]
  (biff/export-rum static-pages "target/resources/public")
  (biff/delete-old-files {:dir "target/resources/public"
                          :exts [".html"]}))
i.e. it generates static html files. I did decide to not bring the static files feature into biff 2 since I ended up not really using it myself at all. So I potentially might just remove generate-assets-fn from biff 2 altogether. But there's also questions around e.g. if you have other kinds of generated assets, or if you use something other than tailwind to generate CSS, should the uberjar task be configurable to handle that, or should people just copy and paste the uberjar task since it isn't that big anyway. in any case for now it's totally fine to just set that key to a no-op function.

got it, and yeah I had a hunch so just wanted to confirm. I personally haven't used any static files yet (and don't see myself using it production in near future) Another thing I found was > biff.datastar/module's bare :biff.core/on-tx key silently does nothing unless (com.biffweb.core/module) is also in the modules vector. I don't think this is big deal. maybe a doc/comment for module and datastar components should be good. I'm assuming most people care about business than setup, at least initially.

hmm... since it is biff.core, it probably would make sense to have com.biffweb.core/start include com.biffweb.core/module in the modules list by default.

That sounds like sensible default. Another thing i bumped into. We noticed that right now biff is using patch-signals for response for POST, and patch-elements are being used only for the initial GET request I'm interested to know what made you go with this pattern or is it something you are currently ruminating on? please feel free to correct my understanding, I'm new to datastar, and my current mental model was we can just send html fragments as response (same semantics as HTMX but now using SSE) is any particular benefit of using signal vs elements?

that's related to the CQRS thing--the general idea is that updates to the page come primarily (if not only) from the long-lived SSE connection, and then the actions/POSTs return empty responses. having the actions return new signals does technically break that model a little bit. I haven't gone over the demo app(s) code too closely yet, but they're probably using patch signals more than necessary.

from the little I've seen so far, a lot of stuff that LLMs try to use signals for is probably better done with server-side tab state, which the actions write to and the SSE thread reads from

👍🏼 1

hmm I'm little confused. My mental model for CQRS (influenced by event modeling) was that while implementing you can keep you domain logic pure (only data) for command processing and query pull does the UI wiring update decisions are also influenced by it (as long as your domain logic is CQRS style) for eg in my spike, I started with simple html string with form for submit then added datastar (almost plug and play style, wiring effort was only at boundary. Req params -> command invocation and query response -> datastar signal response Does it matter (only for CQRS model) how we are updating the UI itself and are free to make decisions based on other factors (easy maintenance, debugging, performance etc)

edit - UI update decisions, not wiring decisions.

I'm probably using the term "CQRS" imprecisely; I haven't really studied the pattern much. That does make sense though that CQRS shouldn't necessarily be about if the UI updates come via request-response or from the SSE thread. Maybe "hyperlith-style CQRS" would be a better term 🙂 or "fat morph" / "the datastar tao" are terms the datastar folks use... Anyway, regardless of what the term is for this approach, I am writing biff.datastar etc with the idea that you'll let the single long-lived SSE thread push all the UI updates to the frontend and have actions primarily just mutate backend state. IMO it simplifies things even further from htmx; in effect it's kinda like you're just saying "whenever something changes, have the user refresh the page" rather than even having to care about where exactly in the dom a chunk of html should be inserted etc

got it and yeah, I agree. it sorta bit me while I was new to HTMX as well. I was doing lower widget level swap everywhere and it got complicated as I made UX richer while learning more about the domain. To start with just sending entire main component, was much easier to reason about and refactor

➕ 1