# How to Build a Budgeting Tool with Lovable

- Tool: How to Build with Lovable
- Difficulty: Beginner
- Compatibility: Lovable Free or higher
- Last updated: April 2026

## TL;DR

Build an envelope budgeting app in Lovable where every dollar gets assigned to a named envelope before you spend it. Features a zero-sum constraint that warns when allocations exceed income, auto-updating spent amounts via a Supabase trigger, and visual progress bars that turn red as envelopes fill up.

## Before you start

- Lovable account (Free tier is sufficient for this build)
- Supabase project with URL and anon key saved to Cloud tab → Secrets
- Your monthly income amount to set up the first budget period

## Step-by-step guide

### 1. Create the budgeting schema with trigger for spent_amount

Prompt Lovable to set up the database tables and the trigger that auto-updates spent_amount. The trigger is the key piece that keeps envelope balances accurate without frontend logic.

```
Build an envelope budgeting app. Create these Supabase tables:

- budget_periods: id, user_id, name (e.g. 'June 2024'), start_date (date), end_date (date), income_amount (numeric), is_active (bool default false), created_at
- envelopes: id, user_id, period_id (FK budget_periods), name, allocated_amount (numeric), spent_amount (numeric default 0), color (hex), icon (emoji), sort_order (int), created_at
- transactions: id, user_id, envelope_id (FK envelopes), amount (numeric, always positive), description, transaction_date (date), created_at

Create a PostgreSQL trigger function update_envelope_spent() that:
- On transactions INSERT: UPDATE envelopes SET spent_amount = spent_amount + NEW.amount WHERE id = NEW.envelope_id
- On transactions DELETE: UPDATE envelopes SET spent_amount = spent_amount - OLD.amount WHERE id = OLD.envelope_id
- Attach as AFTER INSERT OR DELETE trigger on transactions table

RLS: all tables require user_id = auth.uid().

Create a computed column view envelope_summary: all envelope columns plus remaining_amount (allocated_amount - spent_amount) and percent_used ((spent_amount / NULLIF(allocated_amount, 0)) * 100).
```

> Pro tip: Ask Lovable to add a 'Create Period from Template' feature: copying envelopes from the previous period with the same names, colors, and allocated amounts. This saves users from re-creating envelopes every month.

**Expected result:** All three tables are created with the trigger function attached. Inserting a transaction in the Supabase SQL editor updates the envelope's spent_amount automatically.

### 2. Build the envelope Cards with progress bars

Create the main budget view showing envelope Cards with color-coded progress bars. The Cards grid is the primary interface users will use daily.

```
Build the main budget view at src/pages/Budget.tsx:

1. Period selector at top: Select dropdown showing all budget_periods for the user, 'New Period' Button
2. Zero-sum summary Banner below period selector:
   - Show: Period Income: $X | Allocated: $Y | Unallocated: $Z
   - If sum of allocated_amount > income_amount: show red Banner 'Over-allocated by $Z. Reduce envelope amounts to match your income.'
   - If unallocated > 0: show blue Banner 'You have $Z to allocate.'
   - If exactly zero: show green Banner 'Your budget is balanced.'
3. Envelope Cards grid (2-3 columns):
   - Each Card: envelope icon + name, allocated amount, spent amount, remaining amount
   - shadcn/ui Progress component: value = percent_used from envelope_summary view
   - Progress color: green if percent_used < 75, yellow if 75-99, red if >= 100
   - 'Add Transaction' Button on each Card
   - Overflow indicator: 'Over by $X' text in red if spent > allocated
   - Card drag handle for reordering (updates sort_order)
4. 'Add Envelope' Button (floating, bottom right) opens a Dialog with: name Input, icon Emoji picker (simple Select of common emojis), color Select, allocated amount Input
```

**Expected result:** Envelope Cards show with progress bars. Adding a test transaction via the Supabase dashboard updates the progress bar instantly on the next page load.

### 3. Build the transaction entry and history

Create the transaction entry dialog for each envelope and a transaction history page. Transactions are the daily input that drives the progress bars.

```
// src/components/budgeting/AddTransactionDialog.tsx
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { supabase } from '@/integrations/supabase/client'
import { useQueryClient } from '@tanstack/react-query'

const schema = z.object({
  amount: z.coerce.number().positive('Amount must be greater than 0'),
  description: z.string().min(1, 'Description is required'),
  transaction_date: z.string().min(1, 'Date is required'),
})

type FormValues = z.infer<typeof schema>

interface Props {
  envelopeId: string
  envelopeName: string
  userId: string
  open: boolean
  onOpenChange: (open: boolean) => void
}

export function AddTransactionDialog({ envelopeId, envelopeName, userId, open, onOpenChange }: Props) {
  const queryClient = useQueryClient()
  const [isSubmitting, setIsSubmitting] = useState(false)

  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: {
      amount: undefined,
      description: '',
      transaction_date: new Date().toISOString().split('T')[0],
    },
  })

  async function onSubmit(values: FormValues) {
    setIsSubmitting(true)
    const { error } = await supabase.from('transactions').insert({
      user_id: userId,
      envelope_id: envelopeId,
      amount: values.amount,
      description: values.description,
      transaction_date: values.transaction_date,
    })
    setIsSubmitting(false)
    if (!error) {
      queryClient.invalidateQueries({ queryKey: ['envelopes'] })
      form.reset()
      onOpenChange(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>Add to {envelopeName}</DialogTitle>
        </DialogHeader>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
            <FormField control={form.control} name="amount" render={({ field }) => (
              <FormItem>
                <FormLabel>Amount</FormLabel>
                <FormControl><Input type="number" step="0.01" placeholder="0.00" {...field} /></FormControl>
                <FormMessage />
              </FormItem>
            )} />
            <FormField control={form.control} name="description" render={({ field }) => (
              <FormItem>
                <FormLabel>Description</FormLabel>
                <FormControl><Input placeholder="Coffee, groceries..." {...field} /></FormControl>
                <FormMessage />
              </FormItem>
            )} />
            <FormField control={form.control} name="transaction_date" render={({ field }) => (
              <FormItem>
                <FormLabel>Date</FormLabel>
                <FormControl><Input type="date" {...field} /></FormControl>
                <FormMessage />
              </FormItem>
            )} />
            <Button type="submit" className="w-full" disabled={isSubmitting}>
              {isSubmitting ? 'Adding...' : 'Add Transaction'}
            </Button>
          </form>
        </Form>
      </DialogContent>
    </Dialog>
  )
}
```

**Expected result:** The Add Transaction dialog opens from each envelope Card. Submitting the form inserts a transaction and the envelope's progress bar updates on the next render.

### 4. Add budget period management

Build the period creation flow and the ability to copy envelopes from a previous period. This is the key workflow users follow at the start of each month.

```
Build budget period management at src/pages/Periods.tsx:

1. Periods list: show all budget_periods for the user as Cards with: period name, date range, income amount, total allocated, total spent, is_active Badge
2. 'New Period' Dialog:
   - Period name Input (default: 'Month Year' from current date)
   - Start date and end date DatePickers (default: first and last day of next month)
   - Income amount Input
   - 'Copy envelopes from' Select: show previous periods. If selected, will copy envelope names, colors, icons, allocated amounts (not spent amounts) from that period
   - On submit: create budget_period, optionally copy envelopes with spent_amount = 0
3. 'Set Active' Button on each period Card: sets is_active = true for this period and false for all others. The main Budget page shows the active period by default.
4. Period detail view: clicking a period Card shows a read-only summary of that period's envelopes and total spending (for reviewing past periods)
5. Delete period Button: only allowed if period has no transactions. Show a Tooltip explaining this restriction.
```

> Pro tip: Ask Lovable to auto-create the next month's period at the end of each month using a Supabase Edge Function scheduled with pg_cron. Copy envelopes from the current active period automatically so the new period is ready on the first of the month.

**Expected result:** The Periods page lists all budget periods. Creating a new period with 'Copy from previous' duplicates envelope names and allocations with zero spending. Setting a period active switches the main budget view.

## Complete code example

File: `src/components/budgeting/ZeroSumBanner.tsx`

```typescript
import { Alert, AlertDescription } from '@/components/ui/alert'
import { CheckCircle, AlertCircle, Info } from 'lucide-react'

interface Props {
  incomeAmount: number
  totalAllocated: number
}

export function ZeroSumBanner({ incomeAmount, totalAllocated }: Props) {
  const diff = incomeAmount - totalAllocated
  const absFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Math.abs(diff))

  if (diff < 0) {
    return (
      <Alert variant="destructive" className="mb-4">
        <AlertCircle className="h-4 w-4" />
        <AlertDescription>
          Over-allocated by {absFormatted}. Reduce envelope amounts so they total your period income of{' '}
          {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(incomeAmount)}.
        </AlertDescription>
      </Alert>
    )
  }

  if (diff > 0.01) {
    return (
      <Alert className="mb-4 border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950">
        <Info className="h-4 w-4 text-blue-600" />
        <AlertDescription className="text-blue-700 dark:text-blue-300">
          {absFormatted} unallocated. Assign it to an envelope to complete your zero-sum budget.
        </AlertDescription>
      </Alert>
    )
  }

  return (
    <Alert className="mb-4 border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
      <CheckCircle className="h-4 w-4 text-green-600" />
      <AlertDescription className="text-green-700 dark:text-green-300">
        Budget balanced. Every dollar is assigned.
      </AlertDescription>
    </Alert>
  )
}
```

## Common mistakes

- **Storing spent_amount without a trigger** — If you calculate spent_amount in the frontend and store it in the database, it gets out of sync if a user adds transactions from two devices simultaneously or if a transaction is deleted. Fix: Use the AFTER INSERT OR DELETE trigger on the transactions table. The trigger runs on the database server side and is always accurate regardless of how transactions are created or deleted.
- **Allowing allocated_amount to be zero or negative** — A zero-allocation envelope serves no purpose and a negative allocation is nonsensical. These values break the zero-sum calculation. Fix: Add a check constraint: CHECK (allocated_amount > 0). Also add validation in the Zod schema for the envelope form: z.number().positive('Allocation must be greater than 0'). Prevent saving an envelope form if the value is zero.
- **Not handling the period end date when filtering transactions** — If a user adds a transaction dated after the period end (e.g., entering a future-dated expense), it updates spent_amount but doesn't belong to the period. The progress bar shows incorrect values. Fix: Add a check constraint on transactions: validate that transaction_date is between the linked envelope's period start_date and end_date. Do this with a trigger that fetches the period dates via the envelope FK chain before allowing the insert.
- **Loading all transactions into the budget view** — The main budget view only needs envelope-level summaries (allocated, spent, remaining). Loading all individual transactions for the period just to sum them on the frontend is wasteful. Fix: Use the envelope_summary view which has pre-aggregated spent_amount (maintained by the trigger). The budget Cards only need one query to the view — no need to load transactions for the summary page.
- **Deleting a period that has transactions** — Deleting a period with transactions leaves orphaned transactions with envelope_ids pointing to deleted envelopes. This causes foreign key constraint errors. Fix: Add a guard: before allowing period deletion, count transactions for all envelopes in that period. If count > 0, show an error message explaining that transactions must be deleted first. Alternatively, only allow archiving periods (is_archived = true) rather than hard deletion.

## Best practices

- Use a trigger for spent_amount maintenance rather than calculating it in the frontend. Database triggers are transactional and accurate even with concurrent writes.
- Display the zero-sum status prominently at the top of the budget page. Users should immediately see if they've over-allocated or have money left to assign. The ZeroSumBanner component in this guide is the right pattern.
- Show both allocated_amount and remaining_amount on each envelope Card. Users need to see both the budget and how much is left — just showing percentage is not enough for decision-making.
- Add a transaction date constraint that ties transactions to their period. This prevents the accidental assignment of future-dated expenses to a closed period.
- Use soft deletes (is_deleted) for envelopes instead of hard deletes. A deleted envelope with transactions should become inactive and hidden from the active budget, but the transaction history should still show the envelope name.
- Default the new period's income_amount to the previous period's income_amount. Most users have the same income each month, so pre-filling this field reduces friction.

## Frequently asked questions

### What is envelope budgeting and how is it different from a regular budget?

Envelope budgeting requires you to allocate all your income to specific categories (envelopes) before spending — every dollar has a job. A regular budget tracks spending after the fact against loose categories. Envelope budgeting enforces the zero-sum constraint: allocations must equal income, so you can't spend money you haven't planned for. It's more proactive and is especially effective for reducing impulse spending.

### How does the zero-sum constraint work in this app?

The app calculates the sum of all envelope allocations and compares it to the period income_amount. If allocations exceed income, the ZeroSumBanner shows a red warning. If allocations are less than income, a blue banner shows the unallocated remainder. The constraint is visual — you're not blocked from saving if you're over-allocated, but the warning makes the imbalance obvious.

### Can I move money between envelopes mid-month?

Not in the base build. Envelope transfers are the first customization idea in this guide. You'd add an envelope_transfers table and a trigger that adjusts both envelopes' allocated_amount. Ask Lovable to add this feature after the base build is complete.

### Why does spent_amount update automatically without me clicking anything?

A Supabase database trigger fires after every transaction insert or delete. The trigger function adds or subtracts the transaction amount from the envelope's spent_amount column. This happens on the database server side, so it's immediate and accurate regardless of which device or user added the transaction.

### Can I use this with my partner to manage household finances together?

The base build is single-user. The 'Shared household budget' customization idea in this guide adds a households table and shared envelope access. It requires updating the RLS policies to allow household member access. Prompt Lovable to add this feature as a follow-up step after the base build is working.

### What happens to unused envelope money at the end of the period?

Nothing automatically — it stays in the envelope record from the previous period. When you create a new period, you start fresh with new envelopes. The 'Rollover unused funds' customization idea adds a feature to carry over unspent amounts to the next period's allocation. Without rollover, users manually decide how to re-allocate any unspent money in the new period.

### Can I track multiple currencies in the same budget?

No, the base build uses a single currency throughout. All amounts in a budget period are implicitly the same currency. If you regularly spend in multiple currencies, add a currency column to transactions and a base_currency to budget_periods. Convert amounts to base currency using stored exchange rates. This is a significant extension that requires an Edge Function for exchange rate fetching.

### Where can I get help building more advanced budgeting features?

RapidDev builds production Lovable apps including personal finance tools with bank integrations, multi-currency support, and advanced analytics. Reach out if you need features that go beyond this guide.

---

Source: https://www.rapidevelopers.com/how-to-build-lovable/budgeting-tool
© RapidDev — https://www.rapidevelopers.com/how-to-build-lovable/budgeting-tool
