Why Elixir Is the Best Language for Building a Bootstrapped, B2B SaaS in 2024
The common wisdom that I seem to read everywhere is that the best language/tooling to start a SaaS with is the one you already know. eg don’t switch to Elixir if you can smash out a great webapp with React and NextJS because you’re super familiar with that stack.
While I agree with this common wise advice (some would choose Django/htmx, others would choose Rails/Hotwired, this guy chose Phoenix/LiveView), some technologies just can’t be compared, as their scopes are too different. AFAIK you cannot build a whole B2B SaaS with NextJS and nothing else. And I think the whole point of this post is to talk about “One-Person Stack”, which means:
A stack that can cover the entire spectrum of what needs to be done to build a B2B SaaS
A stack that has so few different technologies that a single person can handle all of it
A stack where each technology allows you to be productive with very few lines of code
When we talk about B2B SaaS, we talk about business logic, heavy domain rules, user management with permissions provisioned by an external system like a CRM, relational database with complex entities… Probably you can achieve all that with NextJS alone, but it would be a huge amount of work, while frameworks like Django/Rails/Phoenix/Laravel/Symfony/SpringBoot are tailored just for that.
Honest question: what does the stacks you provided (or just one of them) give you in terms of tackling the problems you stated?
E.g. what does Rails (or whichever’s your favorite) give you wrt tackling business logic or database with complex entities that Nextjs doesn’t (and thus the NodeJs / JavaScript & TypeScript ecosystem)?
The ecosystem is huge, but if you have experience in that ecosystem you are just as likely (or more) to find good solutions for these problems as you would have as if you were starting out in a whole new stack right?
Not saying Nextjs is perfect, but it is a pretty versatile tool with tutorials and sdks and bindings for pretty much any large vendor or common problem. If you find you need to do something where Nextjs, since we are really talking about the JS ecosystem, you can self host or reuse code with express or whatever webserver. Also most devs have enough experience with JS to be able to quickly jump in and contribute if your one man show becomes a two men show.
What do frameworks provide? Many things already integrated: an ORM (to represent, manipulate and migrate complex data model), a complete user management system (from simple login to complex ACL), a consistent way to write things that are similar (web endpoints, background jobs, etc.), standard security stuff (CSRF etc.), reusable features like emailing, sometimes an admin CRUD, sometimes a way to split your app into isolated-but-integrated modules to ensure separation of concerns when scaling (yes, I ❤️ Django)… The fact that all this is integrated under a single dependency is very valuable because you don’t need to write a single line of glue code and because everything just keeps working together when you upgrade that single dependency. So yes, probably you can build anything you want using the “assemble many small libs because you know the ecosystem” approach, but it will be a lot more work than just using a framework, a lot more work than a single dev can carry.
Why not JS? The problem I have with JS right now is that it’s becoming religious. You see how invective @LenFalken’s intervention was? They even twisted my sayings (I said “you cannot build a B2B SaaS with NextJS and nothing else”). When I build a B2B SaaS as a single dev, I don’t want my main stack language to be dictated by the web view layer. And I think this is the main point of the original post: don’t let JS dictate your choices! If you’re comfortable with Phoenix, build your SaaS using Phoenix, and maybe you won’t need any web view complicated lib after all…
“most devs have enough experience with JS”: I was not aware of that, most devs I know, while being decent JS devs, have at least another preferred language and don’t want to make JS their main language…
I agree, but I also think for a small team, a SPA-ish or SSR-ish solution is going to end up adding a lot of overhead because you have to write the backend API and then unpack it on the frontend. For a large team, this is great because you can work on the backend and frontend in parallel. For a small team though this just adds work and latency. So I think all things equal, a small team of experienced LiveView / LiveWire / HTMX devs will be more productive than a small team of NextJS (+ ??? backend) devs.
Then you use what you’re familiar with, like I said?
If you don’t know any of them at all, and can’t even start building a webapp with any technology, then start with a no-code solution or find a technical cofounder.
I like Elixir, but lots of these points aren’t unique at all. Easy integrated client+server hybrid stuff is all the rage with Hotwire, Fresh, Next.js/RSC, etc. – LiveView is not the only one. Postgres-backed job queue libraries exist in lots of languages too (why haven’t they agreed on some interoperability standard?? ><) and so on.
With the whole BEAM cool benefits thing… the big problem is you’re not Ericsson. It’s cool to have Mnesia but it’s not built for complex business data so you end up just using Postgres. It’s cool to have native connections across nodes and complex supervision and hot reload in production but you end up with typical shared-nothing request handlers, talking only HTTP/Postgres/etc. to the outside world, and deployed normally like any other app in any other language, restarting the whole VM on upgrades. It’s cool to “let it crash” but you need good error reporting and don’t need fancy error recovery because again, well-defined shared-nothing scopes like “an incoming HTTP request” and “a background job”.
I want to be convinced and find the Graal, but I’m still torn between Python, and now Common Lisp. When I take a look to maybe rewrite my Python/Django app to Elixir, I miss stuff: no admin dashboard (unless you pay 300USD), no automatic DB migrations unlike Django and CL’s Mito (yet I don’t have Ecto experience so maybe it’s a non issue), a lot of code generation (they rot, don’t they?)… LiveView? I can use HTMX, its websockets extension, Unpoly, and they are cross-stack. No compile-time type checks, like Python, unlike CL; a deployment story in-between Python (dangerous) and CL (build and send a binary) and, because I am now spoiled with CL’s richness of image-based interactive features, I see much less of them in Elixir (no “compile this function ad get warnings”). And oh, there’s some syntax to pay attention to again, those { % => etc. Elixir’s Emacs modes don’t look good when Slime or Sly offer a ton of features. With Elixir I’m back at the terminal which feels like a regression. I’m spoiled.
So, is Elixir really the best language for building a bootstrapped SaaS? Python/Django, I admit, despite their flaw, have arguments. For a solo developer, Common Lisp is hyper productive. Its web offering is minimal but if you know the web, plug in a DB, HTMX, a login system and you’re on tracks. CL won’t have shiny dashboards (wait does it? Grafana dashboard for SBCL and Hunchentoot: memory, threads, requests per second, GC state…) and a supervision tree (only ruricolist/moira to monitor and restart background threads), but you can get a GenServer-inspired actor library (mdbergmann/sento), and I believe they share runtime features: efficiency, live reload is doable. Last but not last, CL is maybe easier to use for other tasks: ingest data efficiently (SBCL is fast), small-ish binaries, scripts (now easier with my CIEL helper).
Elixir is absolutely more shiny and enterprise ready and I don’t know what I miss, but there’s nothing ideal for a rewrite…
Phoenix can come with an admin dashboard. I like to have control over what happens in migrations, the plumbing can be generated.
Liveview is a bit more than htmx. Having a stateful process behind a browser tab gives you a lot of options with realtime web apps. For example, Phoenix PubSub will allow you to send messages to other Liveview processes and update the state acordingly.
If you have time, there is the “make a twitter clone in 15 minutes video” for a short intro into LiveView.
As a one man SaaS developer, I never really had much need for an Ecto Admin dashboard because I use a remote console session to interact with the system if necessary.
If I want to change data in the production database, I would opt for a migration because then that change is stored in my vcs and not something I have to remember.
The phoenix generators can be very granular, providing only models up to the point of liveviews. With the first migrations and tests to get you started. If you don’t like the architecture then you will have to type it yourself or rearrange it afterwards.
thank you, Torch looks really useful. I don’t need such a dashboard for me, I confess I still expose the Django admin to clients, for some parts of the DB, when I didn’t expose a better interface yet and they want to edit a DB entry.
Fair enough, that is a good use case. Does the Django admin interface allow you to edit the associated records as well? And does it handle JSONB objects?
With the generators I got that for free, sort of. Embedded schemas and lists of them were a bit more difficult, but that is easier now with the latest Phoenix.
I still can’t pull away from python/django when I need to get something done. On the side I like to play around with other things, but my go-to for web is python/django+htmx
so yes, you can build a binary for CL apps. Coming from Python, that’s a joy. rsync the binary to the server, run it, done! You can do it with various implementations, like SBCL or CCL. With SBCL the binary relies on glibc, someone has a patch ongoing to have truly static binaries. A hello world binary typically weights ±25MB and starts in 0.4s on my machine (or choose uncompressed, 100MB and starts up in 0.01s). That’s bigger than Rust, smaller than Deno, maybe in par with Go when the application grows (my app with dozens of dependencies is still ±32MB). You can include any file in the release, so we can ship static assets. I was able to build a binary for my web apps.
When the binary is running, you can start a Swank server and connect to it from your editor at home, get a REPL and do anything (inspect state, change parameters, fix and recompile a function, live update…).
From what I understand of the BEAM, it’s a pretty impressive runtime that has lots of properties you normally need something as complex as Kubernetes to rival. Don’t make the mistake of thinking this is about language choice; it’s actually more about the runtime.
Yes and no. BEAM provides immutable data and actors. You can send messages between actors that logically copy all objects that you sent (though, in implementation, actually just reference count). The only mutable data structure is the process dictionary, which cannot be referred to by reference and is local to each actor.
So you get these benefits with any language running on BEAM, but any language running on BEAM will look like Erlang.
So you get these benefits with any language running on BEAM, but any language running on BEAM will look like Erlang.
Is this principle (not the specifics of the implementation) different in e.g. JVM-land? Intuitively, it seems to me like that you always run under the constraints of the target platform, but I’m not sure if other platforms provide more flexibility than the BEAM.
It’s a different set of things. The JVM support mutable and immutable objects and arrays / buffers, but it enforces a single-inheritance object model and a particular dispatch / overriding model, though you can work around that somewhat in other languages with some name mangling). You can support quite a lot of OO languages (Redline even implements Smalltalk) directly on it. The CLR is more flexible and can even support a dialect of C++ and ML. If you wanted to run, say, Haskell, then the JVM or CLR would not enable a particularly efficient implementation.
BEAM is much more tightly coupled with the language. Anything other than a mostly pure functional language, optionally with actor-model extensions, is very hard to support (and if you don’t have the actor-model bits then it’s not a good substrate for the functional language). You couldn’t even moderately efficiently target Python, Java, or C++ to run in BEAM, for example. Elixir and Erlang are different syntax over the same set of core abstractions and these abstractions are very different from most other languages.
was hoping to find some comparison with existing alternatives to perform the same task; this is a good post overall but “the best” is being assumed: just because the author found a cool way of achieving it does not make it the best, at least a quick state of the art overview would be nice to have
The common wisdom that I seem to read everywhere is that the best language/tooling to start a SaaS with is the one you already know. eg don’t switch to Elixir if you can smash out a great webapp with React and NextJS because you’re super familiar with that stack.
While I agree with this common wise advice (some would choose Django/htmx, others would choose Rails/Hotwired, this guy chose Phoenix/LiveView), some technologies just can’t be compared, as their scopes are too different. AFAIK you cannot build a whole B2B SaaS with NextJS and nothing else. And I think the whole point of this post is to talk about “One-Person Stack”, which means:
Wtf, why can’t you with NextJS? You absolutely can.
When we talk about B2B SaaS, we talk about business logic, heavy domain rules, user management with permissions provisioned by an external system like a CRM, relational database with complex entities… Probably you can achieve all that with NextJS alone, but it would be a huge amount of work, while frameworks like Django/Rails/Phoenix/Laravel/Symfony/SpringBoot are tailored just for that.
Honest question: what does the stacks you provided (or just one of them) give you in terms of tackling the problems you stated?
E.g. what does Rails (or whichever’s your favorite) give you wrt tackling business logic or database with complex entities that Nextjs doesn’t (and thus the NodeJs / JavaScript & TypeScript ecosystem)?
The ecosystem is huge, but if you have experience in that ecosystem you are just as likely (or more) to find good solutions for these problems as you would have as if you were starting out in a whole new stack right? Not saying Nextjs is perfect, but it is a pretty versatile tool with tutorials and sdks and bindings for pretty much any large vendor or common problem. If you find you need to do something where Nextjs, since we are really talking about the JS ecosystem, you can self host or reuse code with express or whatever webserver. Also most devs have enough experience with JS to be able to quickly jump in and contribute if your one man show becomes a two men show.
There are multiple questions here:
Thank you for the thorough reply. I also understood you as Nextjs and nothing else.
I agree, but I also think for a small team, a SPA-ish or SSR-ish solution is going to end up adding a lot of overhead because you have to write the backend API and then unpack it on the frontend. For a large team, this is great because you can work on the backend and frontend in parallel. For a small team though this just adds work and latency. So I think all things equal, a small team of experienced LiveView / LiveWire / HTMX devs will be more productive than a small team of NextJS (+ ??? backend) devs.
What if you’re not familiar with React and friends?
Then you use what you’re familiar with, like I said?
If you don’t know any of them at all, and can’t even start building a webapp with any technology, then start with a no-code solution or find a technical cofounder.
I like Elixir, but lots of these points aren’t unique at all. Easy integrated client+server hybrid stuff is all the rage with Hotwire, Fresh, Next.js/RSC, etc. – LiveView is not the only one. Postgres-backed job queue libraries exist in lots of languages too (why haven’t they agreed on some interoperability standard?? ><) and so on.
With the whole BEAM cool benefits thing… the big problem is you’re not Ericsson. It’s cool to have Mnesia but it’s not built for complex business data so you end up just using Postgres. It’s cool to have native connections across nodes and complex supervision and hot reload in production but you end up with typical shared-nothing request handlers, talking only HTTP/Postgres/etc. to the outside world, and deployed normally like any other app in any other language, restarting the whole VM on upgrades. It’s cool to “let it crash” but you need good error reporting and don’t need fancy error recovery because again, well-defined shared-nothing scopes like “an incoming HTTP request” and “a background job”.
I want to be convinced and find the Graal, but I’m still torn between Python, and now Common Lisp. When I take a look to maybe rewrite my Python/Django app to Elixir, I miss stuff: no admin dashboard (unless you pay 300USD), no automatic DB migrations unlike Django and CL’s Mito (yet I don’t have Ecto experience so maybe it’s a non issue), a lot of code generation (they rot, don’t they?)… LiveView? I can use HTMX, its websockets extension, Unpoly, and they are cross-stack. No compile-time type checks, like Python, unlike CL; a deployment story in-between Python (dangerous) and CL (build and send a binary) and, because I am now spoiled with CL’s richness of image-based interactive features, I see much less of them in Elixir (no “compile this function ad get warnings”). And oh, there’s some syntax to pay attention to again, those
{ % =>etc. Elixir’s Emacs modes don’t look good when Slime or Sly offer a ton of features. With Elixir I’m back at the terminal which feels like a regression. I’m spoiled.So, is Elixir really the best language for building a bootstrapped SaaS? Python/Django, I admit, despite their flaw, have arguments. For a solo developer, Common Lisp is hyper productive. Its web offering is minimal but if you know the web, plug in a DB, HTMX, a login system and you’re on tracks. CL won’t have shiny dashboards (wait does it? Grafana dashboard for SBCL and Hunchentoot: memory, threads, requests per second, GC state…) and a supervision tree (only ruricolist/moira to monitor and restart background threads), but you can get a GenServer-inspired actor library (mdbergmann/sento), and I believe they share runtime features: efficiency, live reload is doable. Last but not last, CL is maybe easier to use for other tasks: ingest data efficiently (SBCL is fast), small-ish binaries, scripts (now easier with my CIEL helper).
Elixir is absolutely more shiny and enterprise ready and I don’t know what I miss, but there’s nothing ideal for a rewrite…
Phoenix can come with an admin dashboard. I like to have control over what happens in migrations, the plumbing can be generated.
Liveview is a bit more than htmx. Having a stateful process behind a browser tab gives you a lot of options with realtime web apps. For example, Phoenix PubSub will allow you to send messages to other Liveview processes and update the state acordingly.
If you have time, there is the “make a twitter clone in 15 minutes video” for a short intro into LiveView.
What do you recommend/mean by this?
Well, it depends on what you want for an admin dashboard, but https://github.com/phoenixframework/phoenix_live_dashboard/ gives you telemetry information and statistics about the running system.
ah, so it isn’t a Django-like database admin dashboard (CRUD views).
Could you also point on how to generate the plumbing of Ecto migrations?
This could be something that resembles what you mention: https://github.com/mojotech/torch
As a one man SaaS developer, I never really had much need for an Ecto Admin dashboard because I use a remote console session to interact with the system if necessary.
If I want to change data in the production database, I would opt for a migration because then that change is stored in my vcs and not something I have to remember.
For migrations I use: https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Gen.Migration.html
The phoenix generators can be very granular, providing only models up to the point of liveviews. With the first migrations and tests to get you started. If you don’t like the architecture then you will have to type it yourself or rearrange it afterwards.
thank you, Torch looks really useful. I don’t need such a dashboard for me, I confess I still expose the Django admin to clients, for some parts of the DB, when I didn’t expose a better interface yet and they want to edit a DB entry.
Fair enough, that is a good use case. Does the Django admin interface allow you to edit the associated records as well? And does it handle JSONB objects?
With the generators I got that for free, sort of. Embedded schemas and lists of them were a bit more difficult, but that is easier now with the latest Phoenix.
looks like we can: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#inlinemodeladmin-objects
didn’t find this. Editing JSON seems possible with a plugin. https://github.com/abogushov/django-admin-json-editor (or others)
Tell me more!
I still can’t pull away from python/django when I need to get something done. On the side I like to play around with other things, but my go-to for web is python/django+htmx
so yes, you can build a binary for CL apps. Coming from Python, that’s a joy. rsync the binary to the server, run it, done! You can do it with various implementations, like SBCL or CCL. With SBCL the binary relies on glibc, someone has a patch ongoing to have truly static binaries. A hello world binary typically weights ±25MB and starts in 0.4s on my machine (or choose uncompressed, 100MB and starts up in 0.01s). That’s bigger than Rust, smaller than Deno, maybe in par with Go when the application grows (my app with dozens of dependencies is still ±32MB). You can include any file in the release, so we can ship static assets. I was able to build a binary for my web apps.
When the binary is running, you can start a Swank server and connect to it from your editor at home, get a REPL and do anything (inspect state, change parameters, fix and recompile a function, live update…).
We can also run an app from sources of course.
https://lispcookbook.github.io/cl-cookbook/scripting.html
Wow, that’s pretty cool! I may give it a shot in a little while. Thanks!
so, I’m building a Django-like admin dashboard for Common Lisp… I got the first views working, let’s see how that goes.
From what I understand of the BEAM, it’s a pretty impressive runtime that has lots of properties you normally need something as complex as Kubernetes to rival. Don’t make the mistake of thinking this is about language choice; it’s actually more about the runtime.
Yes and no. BEAM provides immutable data and actors. You can send messages between actors that logically copy all objects that you sent (though, in implementation, actually just reference count). The only mutable data structure is the process dictionary, which cannot be referred to by reference and is local to each actor.
So you get these benefits with any language running on BEAM, but any language running on BEAM will look like Erlang.
Is this principle (not the specifics of the implementation) different in e.g. JVM-land? Intuitively, it seems to me like that you always run under the constraints of the target platform, but I’m not sure if other platforms provide more flexibility than the BEAM.
It’s a different set of things. The JVM support mutable and immutable objects and arrays / buffers, but it enforces a single-inheritance object model and a particular dispatch / overriding model, though you can work around that somewhat in other languages with some name mangling). You can support quite a lot of OO languages (Redline even implements Smalltalk) directly on it. The CLR is more flexible and can even support a dialect of C++ and ML. If you wanted to run, say, Haskell, then the JVM or CLR would not enable a particularly efficient implementation.
BEAM is much more tightly coupled with the language. Anything other than a mostly pure functional language, optionally with actor-model extensions, is very hard to support (and if you don’t have the actor-model bits then it’s not a good substrate for the functional language). You couldn’t even moderately efficiently target Python, Java, or C++ to run in BEAM, for example. Elixir and Erlang are different syntax over the same set of core abstractions and these abstractions are very different from most other languages.
was hoping to find some comparison with existing alternatives to perform the same task; this is a good post overall but “the best” is being assumed: just because the author found a cool way of achieving it does not make it the best, at least a quick state of the art overview would be nice to have