901 karma · joined April 10, 2018
I just tried out the new version of ChatGPT to generate images for the main characters in a concept art style and for me it makes the story come to life a bit more.
I also run a Pathfinder Campaign every other week and have found great pleasure visualizing scenes in this way for myself. Sometimes I share them with the players and so far the feedback on that has been very positive.
I wish I could picture things in my mind but at least now I have something that can help me with my handicap!
I am less worried about the classics or the depth of wisdom that I miss. I am worried about the addiction that is dangled in front of my face and normalised completely now. As for society, I think this is an uphill battle that I don’t see a real and lasting solution for it - and by “it” I mean us fellow humans in aggregate escaping this vortex of what I feel is ultimately self-harming behaviour. I hope for better regulation. I hope for better education. I hope for people realizing how harming it all is and turning around but my sense is that the shape of the world right now will make that so difficult in practice that it won’t happen.
—- Disclosure: I run a small start up which offers teachers a platform to create and conduct digital exams and interfaces with the safe exam browser precisely because cheating is the number one complaint teachers I have spoken to have when it comes to digital exams.
In the local first world you might have navigated away already and created 3 more issues of which 2 more failed because of schema drift or other conflicts. And you might have edited one that was deleted. And now you need to figure out what exactly to tell the user - or what not to tell them.
One thing I would like to point out though is that building a performant sync engine that behaves the way you would like in most cases is a non-trivial thing.
If two users are offline and add, edit, remove issues and come online again, you need to reason about what happens. Sometimes you can get away with Last Writer Wins but what happens if an issue is deleted and then edited. What happens if an item in a list gets re-ordered differently on different clients, what's the final ordering? In which cases can you merge what state and in which do you need to discard something. Do you show a conflict resolution UI? How do you deal with rollbacks. How do you deal with schema drift and updates on items that would be affected by schema drift? Business logic might change between being reconnects. FK constraints can shift. Can you set up your data and the sync engine so that it only syncs the minimum amount of changes and batches them correctly during longer offline sessions so you don't fire 5000 change requests after reconnecting?
I recently had to implement local first + remote sync on some fairly complex dynamic forms and where luckily there is usually only one writer and I can get away with last writer wins and reject if things are too old or if there is schema drift and can just display an error message and roll back. But what I am trying to say is: Whatever can go wrong in an online-first world, can also go wrong in an offline first world but you might get informed of that all at once at a later time - or not at all and your data is not what you think it should be. Some sync engines like zero from rocicorp has opted out of supporting offline writes entirely because of all these problems.
And just to be clear: I love offline first approaches. I yearn for fast performance. And in a lot of cases slapping a sync engine on your app can really be helpful for that if your use case allows it. But it's absolutely crucial to be aware of the pitfalls that come with it.
I agree for certain fields such as your example but until recently software engineers were in disproportional demand and that has changed pretty dramatically in less time than one would need to get a degree in the field.
Edit: And the example of people working in construction: I know a few. Their companies are booked out for the next 2 years. They, however have shitty pay and disproportionately many work-caused health problems. The owners of those companies are having a good time though.
What I will say though is that if I could go back I would probably reconsider the "everything in typescript" and "lets use nextjs, it's what everybody uses" and reach for something more performant because the things that turned out to be difficult to build cleanly again existed on the edges of all of this.
Perhaps Go or Rust for the backend, generate a client or bindings and Solid or Svelte for the frontend.
The stack for non-specifics we landed on for now coincides with a lot of what is mentioned in the blog post with a few extras:
Nextjs, oRPC, Next-Intl, Zod, Tanstack Form, Tanstack Query, Drizzle, SQLite, Tiptap, Better-Auth, Jest, Tailwind, a heavily customized Shadcn.
However there are a few more things I would like to mention.
Formatting: Oxfmt
Linting: Oxlint
E-Mails: react-email
Icon Library: Lucide
User HTML in React: Interweave
Date Handling: date-fns
Drag and Drop Handling: DnD Kit
Payment (MoR): Creem (we are EU based, so a EU MoR matters to us)
The blog post creates a good overview of technologies that exist out and some pros and cons but it's not so clear to me why the things that are recommended are recommended except for: Companies use them.
We went through multiple rewrites of large parts of the codebase, originally we started out using supabase for the backend, then moving to drizzle with self hosted postgres, then switching to sqlite. We started with react-hook-form and switched to Tanstack Form. We had our own UI library, then switched to Shadcn and customized that heavily. We used next-auth (now auth.js) and switched to better-auth, used server actions, then switched to oRPC.
Most of the decisions we made were either grounded in a very specific technical aspect related to our product or just out of curiousity of whether the grass would be greener on the other side (it was sometimes, not always).
In my normal day job I have also mostly worked on Nextjs apps for the past few years, eCommerce, internal dashboards and tools etc and I guess my takeaway is that most of these tools can get you where you want but depending on the shape of your problem(s), some lend themselves better than others and you will probably only find out if you try it out. If you are building a highly complex app, you will inevitably run into situations where all those libraries and frameworks will not provide you with the things you want or will expose primitives that make it cumbersome to work with. I will give one recent example:
In our application we worry a lot about potential loss of form data of dynamically created forms, so we need some sync solution for said form data and have some form of persistence. The all-encompassing solution for us would involve: Local sync to indexed db, including tab sync, websocket sync of change increments (not the entire form, they are quite large), retry on error / connection loss, ideally some state merging logic for the form state in case of divergence with last writer wins fallback, granular form validation with different representations of data for draft and submit states, atomic re-renders (e.g. save button gets disabled when sync is in flight but no full re-renders when the form object changes) and different representations of the form data (image URLs from the server snapshot look different than base64 representations in the form for example). This is not trivial. We looked at different LoFi solutions (Tinybase, different CRDT approaches, Zero, SignalDB) with SignalDB being the closest one to what we would like. Eventually we had to ditch that too and implement a lesser version of all of this for our v1 that syncs the entire exam state periodically over oRPC (+ debounced local storage) and a pretty massive hook that abstracts the sync logic over various forms and form builders we have to handle all sorts of edge cases. Tanstack form is not meant for this (dirty tracking does not line up if the "default state" changes), there is no concept of drafts and final forms, there are no storage adapters that do conflict resolution the way you might want because that is so specific to your app.
Another point I would like to make is that, while modern react stacks do afford the developer a pretty nice UX 90% of the time, sometimes that comes with some pretty annoying tradeoffs. Nextjs is quite heavy and slow. No cron jobs when self-hosting. Having your db schema defined in TS (using drizzle), tacking some extra Zod on it at the client/server boundary and carry it through into the frontend and eventually in Tanstack form is very nice on paper but for dynamic forms with arrays and more complex data types the TypeScript LSP will absolutely shit the bed, even TSGO can't keep up or outright crashes regularly for me. Tanstack form type inference for form fields is also lacking for complex array types for us. Using SQLite is great because no separate process is needed to host the DB, but you don't get CDC or realtime updates easily (maybe turso will help here in the future). Using shadcn is really cool to get up and running and build things that look generally professional and nice, but once you modify the files (which you will need to in order to keep up with the demands of the app) the upgrade path becomes painful and the shadcn dependency list and amount of DOM it sometimes produces of it is also ridiculous.
All that is to say, I think the difficult part of engineering even in 2026 happens at the outer edges of what you build and no framework or stack is gonna save you from all of that (and probably shouldn't!).
Anyway another thing that kept bugging me when reading this is that web apps and development and performance constraints can be so vastly different that I am not sure this pattern always holds up. What if you have an app that depends on a ton of global state, what if you need web sockets, what if you need a highly dynamic form with partial validation derived from a schema that your drizzle definition spat out. And 2 junior devs that benefit from end 2 end type safety and working in Typescript alone to be productive. Add syncing across tabs, offline first requirements, multiplayer rich text editing, sophisticated auth, caching, accessibility constraints, end 2 end monitoring and logging with open telemetry, animated page transitions scroll timeline animations, pdf generation of certain views and a design team breathing down your neck that has a thousand different extra bells and whistles it wants that are impossible to represent and maintain with tailwind alone. And then the marketing department comes around and wants to track everything, inject their A/B testing framework, wants dynamically updated translations without forgoing SSR and SEO benefits and all of that has to run on a 8€/month Hetzner VPS.
I understand that if you work on software that only needs a handful of those things you can get away with a better stack than what the author seems to loathe. But it you work on a large enough application, then you can’t escape complexity, even if you find some of it remedied by a well suited tech stack.
If in the future we could trade a few hundred extra calories per day for a great immune system (without auto-immune side effects) we would have found a nice cheat code!