<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hacker News: jacobobryant</title><link>https://news.ycombinator.com/user?id=jacobobryant</link><description>Hacker News RSS</description><docs>https://hnrss.org/</docs><generator>hnrss v2.1.1</generator><lastBuildDate>Mon, 28 Sep 2026 03:12:57 +0000</lastBuildDate><atom:link href="https://hnrss.org/user?id=jacobobryant" rel="self" type="application/rss+xml"></atom:link><item><title><![CDATA[New comment by jacobobryant in "Plan mode is dead"]]></title><description><![CDATA[
<p>I like writing spec files exclusively by hand and just asking the agent to surface questions about it, which I then clarify by editing the spec file further by hand. It keeps the spec file more manageable than having the agent generate the spec file from your conversation.</p>
]]></description><pubDate>Sat, 26 Sep 2026 01:22:28 +0000</pubDate><link>https://news.ycombinator.com/item?id=49852208</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=49852208</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=49852208</guid></item><item><title><![CDATA[Show HN: Biff 2.0 (Clojure web framework)]]></title><description><![CDATA[
<p>Biff is a web framework that I originally released 6 years ago.[1] I've just finished more-or-less rewriting it from scratch. The main changes:<p>- SQLite is now the default database.<p>- Datastar is used for server-side rendering.<p>- Introduces biff.graph[2] and biff.fx, two libraries that both help structure your application logic.<p>- Biff is now "just" a collection of independent libraries rather than a single monolithic framework, which makes it much easier to swap out the various parts. e.g. using a non-default database is much less work now.<p>I'd been experimenting with a good chunk of this work (the database change and the biff.graph / biff.fx stuff) starting 2 years ago. The Datastar and independent libraries stuff goes back 6 months or so. Datastar's been on my radar for a while and has been picking up steam in the Clojure community, for good reason IMO. The independent library stuff was instigated by me trying out LLM-driven coding for the first time earlier this year: I wanted to try making a Clojure web app without using Biff to see if web frameworks still seemed helpful (my conclusion: absolutely) and how I might do things from first principles were I to start over. So I did that and then started packaging up/rewriting various Biff code as smaller individual libraries as I went.<p>Note that I consider Biff to be mostly "hand-coded"; I start with LLM-written drafts of code but often by the time I'm done editing it's been completely reworked. The initial drafts of docs are all hand-written. I'm interested to try using it for vibe-coding apps though. e.g. how much of the code that requires good judgment can I pull into the framework so that the apps are then easier for LLMs to not screw up.<p>[1] <a href="https://news.ycombinator.com/item?id=23921220">https://news.ycombinator.com/item?id=23921220</a><p>[2] <a href="https://news.ycombinator.com/item?id=48820361">https://news.ycombinator.com/item?id=48820361</a></p>
<hr>
<p>Comments URL: <a href="https://news.ycombinator.com/item?id=49647635">https://news.ycombinator.com/item?id=49647635</a></p>
<p>Points: 29</p>
<p># Comments: 0</p>
]]></description><pubDate>Thu, 10 Sep 2026 17:45:51 +0000</pubDate><link>https://biffweb.com</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=49647635</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=49647635</guid></item><item><title><![CDATA[Biff.datastar: lightweight web UIs with Clojure and Datastar]]></title><description><![CDATA[
<p>Article URL: <a href="https://github.com/jacobobryant/biff/tree/v2.x/libs/datastar">https://github.com/jacobobryant/biff/tree/v2.x/libs/datastar</a></p>
<p>Comments URL: <a href="https://news.ycombinator.com/item?id=49260323">https://news.ycombinator.com/item?id=49260323</a></p>
<p>Points: 2</p>
<p># Comments: 0</p>
]]></description><pubDate>Tue, 11 Aug 2026 15:59:53 +0000</pubDate><link>https://github.com/jacobobryant/biff/tree/v2.x/libs/datastar</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=49260323</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=49260323</guid></item><item><title><![CDATA[New comment by jacobobryant in "A Road to Lisp: Which Lisp"]]></title><description><![CDATA[
<p>as of now neovim works great with clojure, not sure about other lisps. vs code also.</p>
]]></description><pubDate>Fri, 17 Jul 2026 17:09:10 +0000</pubDate><link>https://news.ycombinator.com/item?id=48949705</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48949705</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48949705</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>yep, so if it's important for the application you're working on that you always run the minimum number of database queries possible, biff.graph isn't a good fit. Pathom's query planner might work as you've described; I'm not sure.</p>
]]></description><pubDate>Sun, 12 Jul 2026 23:01:21 +0000</pubDate><link>https://news.ycombinator.com/item?id=48885763</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48885763</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48885763</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>You could always introduce an explicit caching context by doing something like `(binding [<i>cache</i> (atom {})] ...)` whenever you start using some functions like this. If you were trying to use this approach inside a library then you could wrap the public functions with that. Not sure if that would work for the way you were trying to do it.</p>
]]></description><pubDate>Sun, 12 Jul 2026 16:43:30 +0000</pubDate><link>https://news.ycombinator.com/item?id=48882467</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48882467</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48882467</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>ah got it. yeah, in that case you can write a biff -> pathom resolver shim that works for everything. Again though the main thing is just the fact that they have two completely different query engines and aren't guaranteed to give the same results. e.g. off the top of my head I can think of a contrived scenario where biff.graph might not be able to resolve something but Pathom can since it can "look ahead" in the query planning step.<p>Maybe that kind of situation is fine and the question is really just if there are queries that biff.graph can handle which Pathom can't. If your resolvers are written correctly maybe not? But there have definitely been times with Pathom where I did something wrong that threw off the query planner in ways I didn't expect.<p>In any case, if I end up wanting to support migrating easily between the two as a core feature, I'd definitely want to e.g. do a bunch of generative tests to find out what kinds of queries end up with different results. Until then, a downside of supporting Pathom resolvers without a shim is that it might give people the false impression that biff.graph is a drop-in replacement for Pathom or vice-versa.<p>So far though the main target audience I have for biff.graph is people (biff users) who have never even heard of Pathom before, so interchangeability hasn't been a top concern. Though if many people start using biff 2 and then eventually some of the start wanting to migrate to pathom, I'd be down to explore that area.<p>And haha yeah nice to bump into you again--I think I remembered your username from reddit, assuming it's geokon there.</p>
]]></description><pubDate>Sun, 12 Jul 2026 16:29:54 +0000</pubDate><link>https://news.ycombinator.com/item?id=48882362</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48882362</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48882362</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>> If it weren't for those, would Pathom be a drop-in replacement? Or is there different logic?<p>I could've written biff.graph to work with actual Pathom resolvers. In fact it wouldn't be hard to write a shim that takes Pathom resolvers and returns biff.graph resolvers. Although not all resolvers would work since biff.graph doesn't support everything in EQL (e.g. union queries, attribute parameters).<p>The query results aren't strictly guaranteed to be the same, so even with a shim I wouldn't recommend dropping biff.graph into a large project that's already using Pathom. And then that's not even getting into all the Pathom features that biff.graph doesn't support at all (lenient mode, plugins, async mode, the graphql adapter...).<p>But as for the core concepts, yeah I'd say they're pretty close.<p>> I'm curious in what scenario PathomViz is not giving enough info. I had a lot of trouble getting it working tbh<p>I had that trouble too heh heh--I tried running it I know at least once but didn't succeed. I don't remember exactly what the issue was... but I probably should figure that out.<p>Even if I got better at debugging Pathom though, for Biff I would still prefer to have an implementation that's easier for users to understand so that ideally they don't even need extra tools to aid with debugging.<p>FWIW there is an example here[1] of what the biff.graph error looks like when a nested required attribute can't be resolved. That file also has examples of some additional validation logic I've thrown in, e.g. biff.graph will complain if one resolver declares an attribute as a join and another resolver declares it as a scalar. Sometime for our codebase at work I'll probably write some assertions to do those kinds of checks on our Pathom resolvers.<p>[1] <a href="https://github.com/jacobobryant/biff/blob/v2.x/libs/graph/docs/error-examples.md" rel="nofollow">https://github.com/jacobobryant/biff/blob/v2.x/libs/graph/do...</a></p>
]]></description><pubDate>Sun, 12 Jul 2026 08:11:50 +0000</pubDate><link>https://news.ycombinator.com/item?id=48879327</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48879327</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48879327</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>> From what I hear, the main draw is separating what you want from how you get it, so your calling code can just focus on what it needs. But you can use regular functions to do that. What libraries like Pathom do is leave it open to the caller what shape of data they need.<p>hmmm... it would be interesting to try an approach where you make heavy use of memoization and then write your functions to take the the minimal set of inputs (e.g. just the primary key for a record). I'm not sure if that's exactly what you had in mind, but here's a strawman example:<p><pre><code>  ;; instead of having a resolver with this input
  {:input [:person/age
           :person/name
           {:person/pet [:pet/species
                         :pet/n-legs]}]}
  
  ;; you could have this plain function which calls regular functions to get its
  ;; input, each of which only need a single entity ID for their input
  (defn get-person-stuff [db person-id]
    (let [age         (get-person-age db person-id)
          name        (get-person-name db person-id)
          pet-id      (get-person-pet db person-id)
          pet-species (get-pet-species db pet-id)
          pet-n-legs  (get-pet-n-legs db pet-id)]
      ...))
</code></pre>
And you know, I think that would be workable, even though it feels more boilerplatey to me. It would still get you the main benefit of not having to keep track of all the data shapes that are needed by the functions you're calling etc. Some off-the-cuff thoughts:<p>- with this approach you have a single function for each attribute, so you don't have the situation with pathom/biff.graph where there are multiple resolvers that could be called to get a particular attribute. However note that you could always put an assertion in your codebase that ensures no two resolvers share the same output key, which would then also give you the ability to know exactly what resolvers are being called.<p>- my example above doesn't include optional inputs, so that's logic you'd also need to write into all your functions: don't fetch the pet data if the pet ID is nil, don't return anything if the person name is nil, etc.<p>- if you do all that with regular code instead of dependency injection, that does mean you have more code to test, and you have to either supply a test DB (and populate it with everything the functions you're calling need) or mock out the functions. With the dependency injection approach you get plain-old-pure-functions which helps keep your unit tests nice and dumb.<p>- I like the readability of being able to look at the input / output queries and know exactly what shape of data I'm dealing with.<p>- There might be performance issues with the memoized functions approach. Pathom and biff.graph both support batch resolvers for example, and I'm not sure if you could do the equivalent as cleanly with the functions approach. And Pathom of course has its additional query planning step which does... stuff.<p>Going back to your comment, some thoughts:<p>> But I think letting the caller do subtle query changes that can completely change which resolvers are triggered and how something is fetched is kinda leaky.<p>This is an area where you might like biff.graph more than Pathom. Since there's no query planning step, the way that biff.graph executes your queries should be fairly predictable. It's basically just doing a depth-first traversal of your query.<p>(My first bullet point above is relevant too--you can always restrict yourself to having only one resolver per attribute so there's no question of what resolver is getting used.)<p>> How do you write the perfect resolver for all situations? How do you keep them from accidentally exploding their fetches?<p>Typically you write resolvers with only one level of joins/nesting and then let the query engine do the rest. so e.g. instead of writing a resolver that returns something like `{:person/pet {:pet/id 1, :pet/toys [{:toy/id 2, ...}, ...]}}`, you would have one resolver that returns `{:person/pet {:pet/id 1}}` and then another resolver that takes a pet ID and returns `{:pet/toys [{:toy/id 2}, ...]}` etc.<p>So there is a trade-off here in that e.g. you may end up running multiple database queries even though you could've stuffed everything you need into a single database query. That is mitigated by batch resolvers at least so you don't get N+1 query problems.<p>I've never needed to do this myself yet, but if you do run into any places where the performance isn't good enough, you can always write those bits the regular way (e.g. have a resolver that does a more complex query and returns nested data and/or don't even use pathom/biff.graph for this one bit). i.e. optimize where needed but stick with the default in most places.<p>> Is it not better to have things be explicit through function calls instead of chasing down disjointed call graphs?<p>There are pros and cons I think. Sometimes you want to know how an input is being computed and sometimes you want to be able to understand some logic in isolation. In practice I've acclimated quite a bit to the graph structure; I feel like it does a nice job of helping you split your code into the right "chunks".</p>
]]></description><pubDate>Sun, 12 Jul 2026 07:50:25 +0000</pubDate><link>https://news.ycombinator.com/item?id=48879219</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48879219</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48879219</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>Thanks for mentioning datajet, I'll be taking a look at that for sure...</p>
]]></description><pubDate>Sun, 12 Jul 2026 06:31:21 +0000</pubDate><link>https://news.ycombinator.com/item?id=48878863</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48878863</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48878863</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>> It's very cool you managed to make a mini Pathom - esp in so few lines of code :))<p>Thanks! The possibility of doing this had been on my mind for a while... and then I finally got around to trying it since all I had to do to get started was say "try making something like pathom but without [...]". I actually have all the prompts and feedback for the initial POC over here[1] since at the time I was using github issues/comments for my LLM-driven-development workflow.<p>Over the past few weeks as prep for release I went over all the code manually (especially since the whole point of this thing is for the implementation to be easy to understand) and basically rewrote the whole thing, or at least that's what it felt like.<p>> But the end result looks almost identical? Resolver declarations are a bit reorganized and look a bit cleaner - though you could do that with a wrapper around Pathom. Why not fork Pathom and just make some QOL adjustments?<p>The main thing I was going for was just to reduce the implementation size; the tweaks I made to e.g. `defresolver` were really just a side thing. To give some more background on the motivations, an issue I've had sometimes with Pathom is figuring out what's going wrong when my queries don't give me the results I'd expect. A few times as part of that I've gone spelunking through the Pathom codebase but still had never built up a complete understanding of how the query planning and execution works, which has meant that my debugging has always been more trial-and-error / black-box than I'd prefer. So I wanted to see "what is the least complex way that I could take an EQL query and figure out the results, even if the way I do it is dumber than the way Pathom does it?"<p>i.e. I'm trying to minimize the amount of time it takes for someone to read the code and understand exactly what's going on under the hood. Hence layering more code on top of Pathom would only hinder that goal.<p>[1] <a href="https://github.com/jacobobryant/biff.graph/issues?q=is%3Aissue%20state%3Aclosed" rel="nofollow">https://github.com/jacobobryant/biff.graph/issues?q=is%3Aiss...</a></p>
]]></description><pubDate>Sun, 12 Jul 2026 06:29:24 +0000</pubDate><link>https://news.ycombinator.com/item?id=48878853</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48878853</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48878853</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.graph: structure your Clojure codebase as a queryable graph"]]></title><description><![CDATA[
<p>Yeah, it's a big eye-opener. I'd like to see if I can figure out an ergonomic way to do it in Python since I do a fair amount of work in that, and passing ORM objects around isn't great.</p>
]]></description><pubDate>Sun, 12 Jul 2026 02:37:28 +0000</pubDate><link>https://news.ycombinator.com/item?id=48877781</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48877781</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48877781</guid></item><item><title><![CDATA[Biff.graph: structure your Clojure codebase as a queryable graph]]></title><description><![CDATA[
<p>Article URL: <a href="https://github.com/jacobobryant/biff/tree/v2.x/libs/graph">https://github.com/jacobobryant/biff/tree/v2.x/libs/graph</a></p>
<p>Comments URL: <a href="https://news.ycombinator.com/item?id=48820361">https://news.ycombinator.com/item?id=48820361</a></p>
<p>Points: 147</p>
<p># Comments: 32</p>
]]></description><pubDate>Tue, 07 Jul 2026 16:48:48 +0000</pubDate><link>https://github.com/jacobobryant/biff/tree/v2.x/libs/graph</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48820361</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48820361</guid></item><item><title><![CDATA[Biff.fx: lightweight effects system for Clojure]]></title><description><![CDATA[
<p>Article URL: <a href="https://biffweb.com/p/fx/">https://biffweb.com/p/fx/</a></p>
<p>Comments URL: <a href="https://news.ycombinator.com/item?id=48557848">https://news.ycombinator.com/item?id=48557848</a></p>
<p>Points: 31</p>
<p># Comments: 0</p>
]]></description><pubDate>Tue, 16 Jun 2026 16:30:46 +0000</pubDate><link>https://biffweb.com/p/fx/</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48557848</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48557848</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.core: system composition for Clojure web apps"]]></title><description><![CDATA[
<p>hehe yes. There are plenty of other languages with dominant frameworks etc; I like being in a community of experimenters.</p>
]]></description><pubDate>Wed, 10 Jun 2026 02:29:03 +0000</pubDate><link>https://news.ycombinator.com/item?id=48470603</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48470603</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48470603</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.core: system composition for Clojure web apps"]]></title><description><![CDATA[
<p>If everyone wants to move to biff.core that's fine with me!</p>
]]></description><pubDate>Tue, 09 Jun 2026 21:35:00 +0000</pubDate><link>https://news.ycombinator.com/item?id=48468082</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48468082</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48468082</guid></item><item><title><![CDATA[New comment by jacobobryant in "Biff.core: system composition for Clojure web apps"]]></title><description><![CDATA[
<p>AI has been working out well for me writing Clojure, both in personal projects and at work. Documentation, not so much... I write all that by hand.<p>For Biff I've been using AI to generate a rough draft of all the code and then I take a manual pass over things before releasing. Seems to be a good middle ground.</p>
]]></description><pubDate>Tue, 09 Jun 2026 16:22:24 +0000</pubDate><link>https://news.ycombinator.com/item?id=48463180</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48463180</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48463180</guid></item><item><title><![CDATA[Biff.core: system composition for Clojure web apps]]></title><description><![CDATA[
<p>Article URL: <a href="https://biffweb.com/p/core/">https://biffweb.com/p/core/</a></p>
<p>Comments URL: <a href="https://news.ycombinator.com/item?id=48463018">https://news.ycombinator.com/item?id=48463018</a></p>
<p>Points: 138</p>
<p># Comments: 33</p>
]]></description><pubDate>Tue, 09 Jun 2026 16:12:52 +0000</pubDate><link>https://biffweb.com/p/core/</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48463018</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48463018</guid></item><item><title><![CDATA[New comment by jacobobryant in "Bttf is a command line datetime Swiss army knife"]]></title><description><![CDATA[
<p>As the author of a different project also named Biff, I do have to warn you that half the comments on your HN posts will be people quoting back to the future--though I haven't decided yet if that's annoying or an engagement hack!<p>[1] <a href="https://github.com/jacobobryant/biff" rel="nofollow">https://github.com/jacobobryant/biff</a></p>
]]></description><pubDate>Thu, 28 May 2026 14:48:16 +0000</pubDate><link>https://news.ycombinator.com/item?id=48309746</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=48309746</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48309746</guid></item><item><title><![CDATA[Biff 2.0 sneak peak: Clojure web framework]]></title><description><![CDATA[
<p>Article URL: <a href="https://biffweb.com/p/biff2/">https://biffweb.com/p/biff2/</a></p>
<p>Comments URL: <a href="https://news.ycombinator.com/item?id=47837882">https://news.ycombinator.com/item?id=47837882</a></p>
<p>Points: 3</p>
<p># Comments: 0</p>
]]></description><pubDate>Mon, 20 Apr 2026 17:41:30 +0000</pubDate><link>https://biffweb.com/p/biff2/</link><dc:creator>jacobobryant</dc:creator><comments>https://news.ycombinator.com/item?id=47837882</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47837882</guid></item></channel></rss>