Skip to main content

One year of Group Expense with Ease: migrating an SPA to SSR with LLM agents

· 12 min read
Ivan Barlog
AWS Solutions Architect @ BeeSolve

Last year when I left my job I wanted to see whether I could build something without the help of all the internal tools I'd been using for the last 5 years. I had a great chance to change anything I wanted within my own stack. That's when the Group Expense with Ease site was born. Today I am celebrating a year since its initial release which I have celebrated with what else than a rewrite.

Why this site exists​

First of all let me explain why this site even exists. Not only did I need to prove to myself that I can build a project from scratch I was also thinking about building my own clone of Splitwise for quite some time as I didn't like their paywalled approach.

I understand why someone wants to monetize such application but I used it twice a year on holidays with friends and I was just not going to pay for it. I am a developer - there was no other option but build my own version.

I wanted to build this for me and my friends but also eventually for other folks who don't want to pay for Splitwise. In order not to ruin myself financially I decided to build this in serverless fashion so if there are no users I don't need to pay for the infrastructure.

The application is and will be free forever so you can try it any time. If you really like it and/or it helped you solve your problems you can support me financially by buying me a coffee1.

In a gist, the site was born out of frustration plus I wanted to define my new own serverless stack.

The original stack​

As I sat down a year back and started to think what my new serverless stack might be I was really excited because the slate was clean. I knew exactly how the infrastructure should look but I was still undecided on the actual code and libraries. I knew I wanted to use TypeScript everywhere so CDK and Node.js Lambda was a no-brainer. In frontend I wanted to use React as it was (and still probably is) de-facto the standard. I also wanted to build a Single-Page Application (SPA) which can be easily deployed and optimized - remember I wanted this to be as cheap as possible so rendering anything server-side was not a great option for me.

I decided not to go with a full-stack framework2 as I wanted to avoid coupling. I ended up using the following stack:

  • React - de-facto standard
  • Material UI - I already had previous experience
  • TanStack router - at a time this was a leaner choice than React Router
  • TanStack Query - goes great with TanStack router and tRPC
  • tRPC - great alternative to GraphQL, simplified most of the stack
  • valibot - I use this for validations all over the place I love the API and the tree-shaking
  • various AWS SDK clients

Since I wanted the application to be as cheap as possible I couldn't afford to run a non-serverless database. I picked DynamoDB which I already had great experience with and this was a perfect fit for the project as I was the owner and I could decide on the design and behaviour of the site which was built around the underlying technology.

Infrastructure-wise I used all the AWS serverless services I evaluated to be useful:

  • AWS Lambda - fat function with tRPC API + various small functions with glue code
  • DynamoDB - single table design but for instance authorization system has its own table
  • S3 bucket - hosting all the static assets - essentially the whole frontend
  • HTTP API Gateway - with custom authorizer for HTTP-only cookies authentication and passwordless3 e-mail codes
  • Simple Email Service (SES) - for emailing (with react-email for templating)
  • CloudFront - all resources are behind a CDN which gives me easy HTTP-only cookies based authentication
  • Cognito - even if Cognito is serverless and powerful I wanted to create something simpler which I have full control over

This whole thing has been built before I started to use LLMs. I was very proud of myself that I was able to build such things and made these architectural (code but also infrastructure) decisions. These of course weren't done by mistake as I already had a lot of experience with web development and I tried a lot of things previously.

The evolution​

After I built the initial Group Expense with Ease site I've been building other systems over the year. I've extracted and simplified a lot of code out of this project which I've reused with other projects. I've also improved the code and I was able to push it back to this project.

For instance on my second project I've ditched the TanStack Form in favour of formisch which was a great choice and I couldn't explain better than the formisch team did. Simply put building a form library around a validation library from the start will always perform better and provides better DX than the other way around.

The most significant change in my own stack I've made was that I started to build things with SvelteKit. I liked the idea of SvelteKit for quite some time and I realized that server-side rendered (SSR) applications are not as problematic as I thought. Especially with the progressive enhancement. A similar thing has been introduced with Remix 1 and React Router 7 (which basically is Remix 2) and as I was frustrated with React anyway and I didn't have that big of a problem with the Svelte itself I tried it and it was a perfect match.

Even before LLM agents I've built (and re-built) a few small applications. I always want to get my hands dirty before I start using something (even today with LLM agents) because I just want to see if it makes sense and if it can be used easily for me. With the SvelteKit the premise for me was to ditch heavy React and get back to CSS libraries like Bulma. I wanted to write JSX but stop worrying about state, effects and re-renders. I wanted to simplify things. It's a shame that Remix 3 has been released only after I've already switched to SvelteKit because I believe that Remix has a bright future.

For the convenience I built kit-on-lambda which helps you to host SvelteKit on AWS Lambda. It provides CDK constructs which automatically put your SvelteKit behind CloudFront and S3 bucket with static assets and Lambda function with the server code. It's supposed to be convenient and I hope that if anyone uses it they agree.

Since there is no React I ditched the MUI as well. Bulma was reminding me of the good old times of Bootstrap so I switched to Graffiti UI from Scott Tolinski which was a great find.

On the backend I still use pure TypeScript wired through manual dependency injection pattern. I use AWS SDK clients for S3, SQS and DynamoDB plus I use a lot of my own libraries which are available to you for free as well.

Adopting the new stack​

I was happy with my new stack so I wanted to align even an old project like Group Expense with Ease with it. I've already built a few smaller applications like DMARC dashboard4 and Email service dashboard5 with and without LLM assistance so I already had a feel for how things work together. The kit-on-lambda has also been already in a good shape so I decided that migrating a React-based tRPC project might be easier than ever.

I knew that I wouldn't need to make many changes in my backend as it was almost 1:1 and I just needed to wire it to whatever SvelteKit provides - I've chosen experimental Remote functions which meant that I would be even able to reuse all my valibot schemas.

The most daunting thing would be to migrate the React MUI frontend to SvelteKit with graffiti but as I didn't need to do it myself I was confident it will be as easy as possible with LLM agents. Also the application is fairly easy and there are just a few screens so I thought to myself "Why not!".

The thing is I've been using LLM agents daily for over 6 months now but I still wasn't entirely convinced that this job could be done. I tried it anyway and it worked surprisingly well.

I think the reason for this being so easy is threefold:

  1. my backend has been written in a very pragmatic way - using dependency injection, validations already compliant with Standard Schema, framework agnostic
  2. SvelteKit is fairly simple and the team has done a great job optimizing for agents
  3. Graffiti UI is also very simple and Scott has also done a great job making sure agents understand it

The only thing I needed to do was to go a few rounds after migration making sure everything works as expected and everything looked alright. One more thing which helped with the migration was that the agent could "see" the original site which was deployed to staging through Chrome DevTools MCP.

Before vs after - SPA vs SSR​

As I migrated the application from stack to stack I took this as an opportunity to compare two different approaches. The first approach is a React-based SPA site with REST API. The second is an SSR application which ships as little JavaScript as possible.

The difference is significant:

  • JS shipped: ~800 KB → ~160 KB (~80% less)
  • LCP on cold load: ~2x faster
  • TTFB: near-instant empty shell (followed by hydration and data fetching) → ~100 ms (fully hydrated page)

This does not mean that SSRs are a silver bullet - there are always tradeoffs. For this type of project the SSR is great. Initial load introduced a bit of a latency but then each subsequent page load is very efficient thanks to the progressive enhancement which does not require full page reload meaning the full page is not rendered server side on each request.

These numbers are just illustrative and do not represent an exact benchmark. I measured both versions back to back in the same session against the same data which was enough for me. I got more data which means I might write another blog post dedicated to it in the future so if you are interested in reading that please let me know.

What's next​

After the migration was done I even ticked a few items from my roadmap making the project even more useful. From now on in addition to the original functionality of creating groups and inviting your friends you can create groups with "virtual" friends. Meaning you don't need to bother anyone if you just want to divide expenses with your friends.

More features like that may come in the future.

Infrastructure-wise I am playing around with the idea of replacing DynamoDB with DSQL - something I can imagine might be super easy to use on client projects and it also has a promise of being very cheap (cheaper than running an RDS instance at least). Plus I always wanted to use it but I didn't really have a project where DynamoDB wasn't the obvious choice. We'll see.

Conclusion​

The price of migrations is as low as ever today when you can use LLM agents. Does it make sense on a project which nobody uses? Maybe, maybe not. It makes sense to me because this project is my "test this new approach" project and since almost nobody uses it, the risk of changes is very low.

At the same time I encourage you to try the project. This does not mean I don't want any users. If you do, please leave me some feedback.

Try it today at group-expense-with-ease.com - it's free and forever will be.

Footnotes​

  1. I have only tested this after a year and I found out it is not working so I am in the middle of the discussion with the support. Hopefully it will work when someone decides to support me. ↩

  2. I've been experimenting with Remix 1 and React Router 7 (de-facto Remix 2) but I wanted something simpler. ↩

  3. I think using passwords in this day and age is a wrong choice. Implementing password authentication is too much of a responsibility. I shifted that responsibility to your e-mail provider. ↩

  4. Self hosted dashboard which helps you parse and display DMARC reports so you can easily identify if your DNS is properly set up. ↩

  5. Self hosted dashboard which shows emails sent through accompanying Email Service. ↩