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.
What you're building
Envelope budgeting is a cash-flow management method where you divide your income into named envelopes before spending. You allocate $500 to Groceries, $200 to Entertainment, and $1,200 to Rent at the start of each month. As you spend, money comes out of the specific envelope. When an envelope is empty, spending in that category stops.
The zero-sum constraint means the sum of all envelope allocations must equal your period income. The app tracks this in real time: if you have $3,000 in income and allocate $2,800 across envelopes, the summary Card shows $200 unallocated. If allocations exceed $3,000, a warning Banner appears.
Spent amounts update automatically. When a transaction is inserted with an envelope_id, a Supabase AFTER INSERT trigger on the transactions table runs a function that adds the transaction amount to envelopes.spent_amount. Similarly, deleting a transaction subtracts from spent_amount. This keeps spent amounts accurate without any frontend logic.
Final result
A clean envelope budgeting app where users plan their spending at the start of each period and track it in real time with auto-updating progress bars.
Tech stack
Prerequisites
- 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
Build steps
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.
1Build an envelope budgeting app. Create these Supabase tables:23- 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_at4- 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_at5- transactions: id, user_id, envelope_id (FK envelopes), amount (numeric, always positive), description, transaction_date (date), created_at67Create a PostgreSQL trigger function update_envelope_spent() that:8- On transactions INSERT: UPDATE envelopes SET spent_amount = spent_amount + NEW.amount WHERE id = NEW.envelope_id9- On transactions DELETE: UPDATE envelopes SET spent_amount = spent_amount - OLD.amount WHERE id = OLD.envelope_id10- Attach as AFTER INSERT OR DELETE trigger on transactions table1112RLS: all tables require user_id = auth.uid().1314Create 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.
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.
1Build the main budget view at src/pages/Budget.tsx:231. Period selector at top: Select dropdown showing all budget_periods for the user, 'New Period' Button42. Zero-sum summary Banner below period selector:5 - Show: Period Income: $X | Allocated: $Y | Unallocated: $Z6 - If sum of allocated_amount > income_amount: show red Banner 'Over-allocated by $Z. Reduce envelope amounts to match your income.'7 - If unallocated > 0: show blue Banner 'You have $Z to allocate.'8 - If exactly zero: show green Banner 'Your budget is balanced.'93. Envelope Cards grid (2-3 columns):10 - Each Card: envelope icon + name, allocated amount, spent amount, remaining amount11 - shadcn/ui Progress component: value = percent_used from envelope_summary view12 - Progress color: green if percent_used < 75, yellow if 75-99, red if >= 10013 - 'Add Transaction' Button on each Card14 - Overflow indicator: 'Over by $X' text in red if spent > allocated15 - Card drag handle for reordering (updates sort_order)164. 'Add Envelope' Button (floating, bottom right) opens a Dialog with: name Input, icon Emoji picker (simple Select of common emojis), color Select, allocated amount InputExpected 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.
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.
1// src/components/budgeting/AddTransactionDialog.tsx2import { useState } from 'react'3import { useForm } from 'react-hook-form'4import { zodResolver } from '@hookform/resolvers/zod'5import { z } from 'zod'6import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'7import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'8import { Input } from '@/components/ui/input'9import { Button } from '@/components/ui/button'10import { supabase } from '@/integrations/supabase/client'11import { useQueryClient } from '@tanstack/react-query'1213const schema = z.object({14 amount: z.coerce.number().positive('Amount must be greater than 0'),15 description: z.string().min(1, 'Description is required'),16 transaction_date: z.string().min(1, 'Date is required'),17})1819type FormValues = z.infer<typeof schema>2021interface Props {22 envelopeId: string23 envelopeName: string24 userId: string25 open: boolean26 onOpenChange: (open: boolean) => void27}2829export function AddTransactionDialog({ envelopeId, envelopeName, userId, open, onOpenChange }: Props) {30 const queryClient = useQueryClient()31 const [isSubmitting, setIsSubmitting] = useState(false)3233 const form = useForm<FormValues>({34 resolver: zodResolver(schema),35 defaultValues: {36 amount: undefined,37 description: '',38 transaction_date: new Date().toISOString().split('T')[0],39 },40 })4142 async function onSubmit(values: FormValues) {43 setIsSubmitting(true)44 const { error } = await supabase.from('transactions').insert({45 user_id: userId,46 envelope_id: envelopeId,47 amount: values.amount,48 description: values.description,49 transaction_date: values.transaction_date,50 })51 setIsSubmitting(false)52 if (!error) {53 queryClient.invalidateQueries({ queryKey: ['envelopes'] })54 form.reset()55 onOpenChange(false)56 }57 }5859 return (60 <Dialog open={open} onOpenChange={onOpenChange}>61 <DialogContent className="sm:max-w-md">62 <DialogHeader>63 <DialogTitle>Add to {envelopeName}</DialogTitle>64 </DialogHeader>65 <Form {...form}>66 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">67 <FormField control={form.control} name="amount" render={({ field }) => (68 <FormItem>69 <FormLabel>Amount</FormLabel>70 <FormControl><Input type="number" step="0.01" placeholder="0.00" {...field} /></FormControl>71 <FormMessage />72 </FormItem>73 )} />74 <FormField control={form.control} name="description" render={({ field }) => (75 <FormItem>76 <FormLabel>Description</FormLabel>77 <FormControl><Input placeholder="Coffee, groceries..." {...field} /></FormControl>78 <FormMessage />79 </FormItem>80 )} />81 <FormField control={form.control} name="transaction_date" render={({ field }) => (82 <FormItem>83 <FormLabel>Date</FormLabel>84 <FormControl><Input type="date" {...field} /></FormControl>85 <FormMessage />86 </FormItem>87 )} />88 <Button type="submit" className="w-full" disabled={isSubmitting}>89 {isSubmitting ? 'Adding...' : 'Add Transaction'}90 </Button>91 </form>92 </Form>93 </DialogContent>94 </Dialog>95 )96}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.
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.
1Build budget period management at src/pages/Periods.tsx:231. Periods list: show all budget_periods for the user as Cards with: period name, date range, income amount, total allocated, total spent, is_active Badge42. 'New Period' Dialog:5 - Period name Input (default: 'Month Year' from current date)6 - Start date and end date DatePickers (default: first and last day of next month)7 - Income amount Input8 - 'Copy envelopes from' Select: show previous periods. If selected, will copy envelope names, colors, icons, allocated amounts (not spent amounts) from that period9 - On submit: create budget_period, optionally copy envelopes with spent_amount = 0103. '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.114. Period detail view: clicking a period Card shows a read-only summary of that period's envelopes and total spending (for reviewing past periods)125. 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
1import { Alert, AlertDescription } from '@/components/ui/alert'2import { CheckCircle, AlertCircle, Info } from 'lucide-react'34interface Props {5 incomeAmount: number6 totalAllocated: number7}89export function ZeroSumBanner({ incomeAmount, totalAllocated }: Props) {10 const diff = incomeAmount - totalAllocated11 const absFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Math.abs(diff))1213 if (diff < 0) {14 return (15 <Alert variant="destructive" className="mb-4">16 <AlertCircle className="h-4 w-4" />17 <AlertDescription>18 Over-allocated by {absFormatted}. Reduce envelope amounts so they total your period income of{' '}19 {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(incomeAmount)}.20 </AlertDescription>21 </Alert>22 )23 }2425 if (diff > 0.01) {26 return (27 <Alert className="mb-4 border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950">28 <Info className="h-4 w-4 text-blue-600" />29 <AlertDescription className="text-blue-700 dark:text-blue-300">30 {absFormatted} unallocated. Assign it to an envelope to complete your zero-sum budget.31 </AlertDescription>32 </Alert>33 )34 }3536 return (37 <Alert className="mb-4 border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">38 <CheckCircle className="h-4 w-4 text-green-600" />39 <AlertDescription className="text-green-700 dark:text-green-300">40 Budget balanced. Every dollar is assigned.41 </AlertDescription>42 </Alert>43 )44}Customization ideas
Transfer between envelopes
Add an envelope transfer feature: move money from one envelope to another mid-period. Store transfers in an envelope_transfers table (from_envelope_id, to_envelope_id, amount, note). The trigger on this table adjusts allocated_amount on both envelopes. Show a transfer history per envelope.
Rollover unused funds
When creating a new period, add an option to roll over unspent envelope amounts. If the Groceries envelope has $50 remaining at month end, the new period's Groceries allocation is automatically $550 (the usual $500 plus $50 rollover). Store rolled_over_amount per envelope for transparency.
Recurring income entries
Add an income_sources table for recurring income (salary, freelance, dividends). A pg_cron Edge Function adds income automatically on scheduled dates, which increases the period's income_amount. Show income entries alongside expense transactions in the history view.
Shared household budget
Add a households table and member linking. Both partners can add transactions to shared envelopes. RLS is updated to allow household members to read and write envelopes for their household. Show who added each transaction using the created_by column.
Common pitfalls
Pitfall: Storing spent_amount without a trigger
How to avoid: 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.
Pitfall: Allowing allocated_amount to be zero or negative
How to avoid: 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.
Pitfall: Not handling the period end date when filtering transactions
How to avoid: 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.
Pitfall: Loading all transactions into the budget view
How to avoid: 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.
Pitfall: Deleting a period that has transactions
How to avoid: 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.
AI prompts to try
Copy these prompts to build this project faster.
I'm building an envelope budgeting app with PostgreSQL. I have an envelopes table with spent_amount and a transactions table. Help me write a PostgreSQL trigger function that updates spent_amount on the envelope when a transaction is inserted or deleted. The trigger should handle: INSERT (add amount), DELETE (subtract amount), and UPDATE (subtract old amount, add new amount). Show the complete CREATE OR REPLACE FUNCTION and CREATE TRIGGER statements.
Add a spending trend chart to the budgeting app. Below the envelope Cards, add a Recharts BarChart showing daily spending totals for the current budget period. X-axis shows each day of the period, Y-axis shows total amount spent that day across all envelopes. Add a reference line showing the 'ideal daily spending rate' (period income / number of days). This helps users see if they're spending ahead of or behind their ideal pace.
In Supabase, create a view called envelope_health that returns all envelopes for active periods with a computed health_status column: 'healthy' if percent_used < 75, 'warning' if 75-99, 'over_budget' if >= 100, 'empty' if spent_amount = 0. Also add days_remaining (end_date - today) and ideal_daily_remaining (remaining_amount / NULLIF(days_remaining, 0)). Use this view to power dashboard summary metrics.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation