What it does
Sign in, add expenses, organise them into your own categories, and open the analytics page to see charts of where your money goes. Each user only ever sees their own data.
Keeping users in sync
Authentication is handled by Clerk, but expenses live in my own Postgres database, so the two have to agree on who each user is. I solved this with a webhook: when a user signs up or changes their details in Clerk, Clerk calls an API route in the app, which verifies the request's signature (with Svix) before creating or updating the user in the database.
Server Actions instead of an API
All reads and writes for expenses, categories and analytics are Next.js Server Actions. That kept the app small: no separate REST layer, and type safety from the database all the way to the form.
What I'd improve
Reviewing my own code afterwards, I noticed the same error-handling block repeated across many server actions. The next step is to pull it into a single wrapper, so every action handles auth and errors the same way.