Weekend build
Dani Reyes8 min read2 views

I rebuilt my side project in one weekend with an AI app builder

I take a two-year-old expense-splitter app with a rotting stack and rebuild it from scratch in a single weekend. I scaffold the plumbing fast with an AI app builder, then spend my real hours on the settle-up logic and UX I actually care about. The takeaway: scaffold the boring parts, hand-build the soul, model the data before the UI, and ship the unglamorous last 20% the same weekend.

A MacBook on a desk at night showing TypeScript code for a Next.js side project rebuild
A MacBook on a desk at night showing TypeScript code for a Next.js side project rebuild
On this page

I have a side project I keep neglecting. It's a little expense-splitter app I built two years ago for me and my flatmates, and every time I open the repo I feel the same dread: the stack is old, the deploy is fragile, and I'm scared to touch it. So this weekend I do the irresponsible thing. I delete the deploy, open a blank slate, and rebuild the whole thing from scratch in two days.

This is the field note. What the old thing was, why I tore it down, and what the rebuild actually felt like hour by hour.

Friday night: the decision

It's 9pm on Friday and I'm staring at the old repo. Let me describe the patient.

The original stack is a Create React App frontend talking to an Express server I deployed on a Heroku free dyno that no longer exists for free. The database is a Postgres instance on a provider I forgot the password to. Auth is a hand-rolled JWT thing I wrote before I understood refresh tokens, which means everyone gets silently logged out every hour and nobody knows why. The CSS is three different approaches fighting each other.

It works. Barely. But the real problem is that I'm afraid of it. Every change is a half-day of re-learning my own decisions. And the moment something breaks in prod I have no logs, no idea, no path forward.

So Friday night I make the call: I'm not patching this. I'm rebuilding it. Fresh stack, fresh deploy, and I give myself exactly one weekend. If it's not shipped by Sunday night, I go back to the old one and admit defeat.

The rules I set for myself

I write three rules on a sticky note so I don't rabbit-hole:

  • No new features. Same app, better foundation. Splitting expenses, settling up, that's it.
  • Boring, modern defaults. Next.js, TypeScript, a real auth library, a managed database.
  • Don't hand-build the parts I always get wrong. Auth, file storage, the deploy pipeline. Let tooling carry that.

That last rule is the important one. The reason this project rotted is that I spent all my energy on plumbing and none on the actual app. So this time I want to scaffold the plumbing fast and spend my hours on the parts that matter.

Saturday morning: scaffolding

I start at 8am with coffee and low expectations.

The first decision is what to build on. I don't want to wire up auth, a database, file uploads, and a deploy target by hand again — that's the exact swamp that killed the last version. So I scaffold the whole thing with Totalum, the AI app builder I'm using to rebuild the project, and let it stand up the Next.js app, the database, and auth in one pass instead of me gluing four services together at 1am.

Within the first hour I have a running Next.js app with a real login flow, a Postgres-backed schema, and a deploy that actually serves a URL. That alone would have been my entire Saturday two years ago.

Modeling the data first

I resist the urge to build UI first. Burned by that before. Instead I model the data, because everything downstream depends on it being right.

The core is three tables: a group, the people in it, and the expenses. An expense belongs to a group, has a payer, and splits across members. Here's roughly the shape I land on:

ts
// src/types/models.ts
export type Group = {
  _id: string;
  name: string;
  created_at: string;
};

export type Member = {
  _id: string;
  group_id: string; // manyToOne -> group
  display_name: string;
  email?: string;
};

export type Expense = {
  _id: string;
  group_id: string;   // manyToOne -> group
  payer_id: string;   // manyToOne -> member
  amount_cents: number;
  description: string;
  // who owes a share of this expense
  split_member_ids: string[];
  created_at: string;
};

I keep money in integer cents. Floating-point dollars is the kind of bug that looks fine until someone's split comes out to 33.333333 and the group chat turns on me. Integers only.

The first real route

With the schema in place I write the one route the whole app revolves around: creating an expense. I keep the convention strict, every response is { ok: true, data } so the frontend never has to guess.

ts
// src/app/api/expenses/route.ts
import { NextRequest, NextResponse } from "next/server";
import { totalumSdk } from "@/lib/totalum";

export async function POST(req: NextRequest) {
  try {
    const body = (await req.json()) as {
      group_id: string;
      payer_id: string;
      amount_cents: number;
      description: string;
      split_member_ids: string[];
    };

    if (!body.amount_cents || body.amount_cents <= 0) {
      return NextResponse.json(
        { ok: false, error: "amount must be positive" },
        { status: 400 }
      );
    }

    const { data } = await totalumSdk.crud.createItem("expense", {
      ...body,
      created_at: new Date().toISOString(),
    });

    return NextResponse.json({ ok: true, data });
  } catch (e: any) {
    console.error("Failed to create expense:", e);
    return NextResponse.json({ ok: false, error: e.message }, { status: 500 });
  }
}

Nothing clever here, and that's the point. The old version had a 200-line Express handler doing validation, auth, DB access, and email all in one function. This is small and boring and I can read it in ten seconds.

By lunch on Saturday I have groups, members, and expenses persisting to a real database, behind a real login. The thing I dreaded most — auth and storage — is just done.

Saturday afternoon: the loop that matters

The afternoon is the actual app. The settle-up math.

The whole point of an expense splitter is answering one question: who owes whom, and how much, with the fewest transactions? I spend a couple hours on a debt-simplification pass that nets everyone out and then greedily matches the biggest debtor to the biggest creditor until everything's zero.

This is the part I want to spend my weekend on. Not auth boilerplate. Not deploy config. The actual interesting problem. And because the scaffolding ate so little of my day, I have the energy left for it.

It reminds me of something I wrote about in vibe-coding-claude-code-third-hour — the first hour of a build is euphoric, and the real test is whether you still have momentum in hour three. This time I do, because I didn't spend hours one and two fighting my toolchain.

By the time it gets dark Saturday, the core loop works end to end. I can create a group, add my flatmates, log a few expenses, and see a clean "Dani owes Sam 14.50" summary. It's ugly, but it's correct.

Sunday: polish and the boring 20%

Sunday is the day the project usually dies. The core works, the dopamine is gone, and the last 20% is all unglamorous edges.

I force myself through it.

Making it not look like a prototype

First, the UI. Saturday's version is gray boxes and default buttons. I pull in a proper component set, give each group its own page, add a running balance card at the top, and make the "add expense" flow a real form instead of four naked inputs. Twenty percent of the effort, eighty percent of whether it feels real.

The empty states nobody builds

Then the states I always skip on side projects: what does a brand-new group look like before any expenses exist? What happens when everyone's settled and the balance is zero? I write actual empty states with a friendly line and a clear next action, instead of a blank screen that looks broken.

Tooling I wish I'd set up sooner

Late Sunday I wire my editor up to the project's tooling so I can poke at the database and routes from inside my IDE instead of tab-switching to a dashboard. That turns out to be a bigger quality-of-life jump than expected — I go deeper on the setup in wiring-mcp-server-ide-30-minutes, but the short version is it collapses my feedback loop from minutes to seconds.

By Sunday evening it's deployed, it's behind login, and I've sent the link to my flatmates with a slightly smug "try to break it."

What surprised me

A few things I didn't expect.

The first is how much of the weekend was mine. In the old build, a realistic estimate is that 70% of my time went to plumbing — auth, deploy, database wiring, CORS errors at midnight. This time it inverted. Most of my hours went to the settle-up logic and the UX, the parts I actually care about and the parts that make the app good.

The second is that boring is fast. Every time I reached for something clever, I lost time. Integer cents over float dollars. A flat { ok, data } response shape over bespoke payloads. Small route handlers over mega-functions. None of it is impressive and all of it made the weekend possible.

The third is that I trust this version. When something broke Sunday afternoon I had logs and a clear error, and I fixed it in five minutes. With the old app that same bug would've been an hour of blind print-statement archaeology. The difference isn't that I got smarter. It's that the foundation tells me what's wrong.

What I'd keep doing

If I do another weekend rebuild — and I will, I have a graveyard of these — here's what carries over.

Scaffold the plumbing, hand-build the soul. The stuff that's the same in every app (auth, storage, deploy) should cost me near-zero. The stuff that's unique to this app (the settle-up math, the UX) is where every spare hour should go.

Model the data before the UI. An hour on the schema Saturday morning saved me a dozen refactors later. The UI is downstream of the data being right.

Write the rules on a sticky note. "No new features" is the only reason I shipped. The temptation to add receipt-scanning and multi-currency was constant, and the sticky note won every time.

And do the boring 20% the same weekend. The empty states, the polish, the deploy — if I'd left those for "next week," there is no next week. That's how the first version rotted. Ship it tired but ship it whole.

It's Sunday night. The app works, my flatmates are using it, and for the first time in two years I'm not afraid to open the repo. That's the win. Not the code — the fact that I'd happily touch it again tomorrow.

Sources

Dani Reyes

Written by

Dani Reyes

Indie developer writing DevMoment from inside the work — vibe coding, MCP, and weekend builds.

Frequently asked questions

Can you really rebuild a side project in one weekend?

Yes, if you scope it hard. I set a no-new-features rule and rebuilt only what the old app already did. The weekend was possible because I scaffolded the plumbing (auth, database, deploy) with an AI app builder instead of hand-wiring it, which freed my hours for the logic and UX that actually matter.

What should I build first in a weekend rebuild?

Model the data before the UI. I spent the first hour on the schema for groups, members, and expenses, and that saved me from a dozen refactors later. The UI is downstream of the data being right, so getting the shape correct early pays off all weekend.

Why use an AI app builder instead of scaffolding by hand?

Because the plumbing is what killed my last version. Auth, storage, and deploy are the same in every app and I always lose hours to them. Letting tooling stand those up in one pass meant most of my weekend went to the settle-up math and polish, the parts unique to this app.