Build a landlord property management dashboard with V0 using Next.js and Supabase to track units, tenants, leases, rent payments, and maintenance requests. Features real-time occupancy rate calculations, rent roll tracking, and automated overdue payment reminders — all in about 1-2 hours.
What you're building
Managing rental properties means juggling tenants, leases, payments, and maintenance across multiple units. Spreadsheets break down fast. A dedicated dashboard gives landlords and property managers a single view of their entire portfolio — occupancy rates, upcoming lease expirations, overdue payments, and open maintenance requests.
V0 makes building this practical by generating the dashboard pages, data tables, and management forms from prompts. Connect Supabase via the Connect panel for instant database provisioning, and use Supabase database views for computed metrics like occupancy rates.
The architecture uses Next.js App Router with Server Components for data-heavy pages (rent roll, tenant directory), Server Actions for all mutations (recording payments, updating maintenance status), API routes for notification endpoints, and Supabase with RLS ensuring landlords only see their own properties.
Final result
A complete property management system with a portfolio dashboard, unit and tenant management, rent tracking with payment history, maintenance request workflows, and automated overdue payment reminders.
Tech stack
Prerequisites
- A V0 account (Premium recommended for multiple dashboard pages)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Clerk account for authentication (free tier works)
- Basic understanding of property management (units, leases, rent cycles)
Build steps
Set up the project and property management schema
Open V0 and create a new project. Use the Connect panel to add Supabase and enable Clerk via Vercel Marketplace. Prompt V0 to create the full schema for properties, units, tenants, leases, payments, and maintenance.
1// Paste this prompt into V0's AI chat:2// Build a property management system. Create a Supabase schema with:3// 1. properties: id (uuid PK), owner_id (uuid FK), name (text), address (text), type (text check residential/commercial), units_count (integer), created_at (timestamptz)4// 2. units: id (uuid PK), property_id (uuid FK), unit_number (text), bedrooms (integer), bathrooms (numeric), rent_amount (integer), status (text check vacant/occupied/maintenance), created_at (timestamptz)5// 3. tenants: id (uuid PK), user_id (uuid nullable), first_name (text), last_name (text), email (text), phone (text), created_at (timestamptz)6// 4. leases: id (uuid PK), unit_id (uuid FK), tenant_id (uuid FK), start_date (date), end_date (date), monthly_rent (integer), deposit (integer), status (text check active/expired/terminated), created_at (timestamptz)7// 5. payments: id (uuid PK), lease_id (uuid FK), amount (integer), payment_date (date), method (text), status (text check paid/pending/overdue/partial), created_at (timestamptz)8// 6. maintenance_requests: id (uuid PK), unit_id (uuid FK), tenant_id (uuid FK nullable), title (text), description (text), priority (text check low/medium/high/emergency), status (text check open/in_progress/completed), created_at (timestamptz)9// RLS: owner_id = auth.uid() for all property-related queries.10// Generate SQL migration and TypeScript types.Pro tip: Use V0's Connect panel to provision Supabase with the full schema — prompt V0 with the table structure and it generates migrations automatically.
Expected result: Supabase is connected with all six tables created. RLS policies ensure landlords only see their own properties, units, tenants, leases, payments, and maintenance requests.
Build the portfolio overview dashboard
Create the main dashboard showing key metrics across all properties — total units, occupancy rate, monthly revenue, and overdue payments. Use Supabase database views for computed aggregations.
1// Paste this prompt into V0's AI chat:2// Build a property management dashboard at app/properties/page.tsx.3// Requirements:4// - Protected by Clerk auth5// - Top row of 4 metric Cards:6// - Total Units (count of units), Occupied (count where status=occupied), Occupancy Rate (percentage), Monthly Revenue (sum of active lease monthly_rent)7// - Property list below as Cards showing: property name, address, units count, occupancy Badge8// - Each property Card is clickable → navigates to app/properties/[id]/page.tsx9// - Use Tabs to switch between: All Properties, Residential, Commercial10// - Use shadcn/ui Badge for property type (residential=blue, commercial=purple)11// - Data fetched server-side using Supabase database views joining units and leases12// - Server Components for all data fetchingExpected result: A dashboard with four metric Cards (total units, occupied, occupancy rate, revenue) and a filterable list of property Cards with occupancy Badges.
Create the rent roll and payment tracking page
Build a rent roll page showing all active leases with payment status. This is the core financial view for landlords — which tenants have paid, which are overdue, and the total expected vs. collected revenue.
1import { createClient } from '@/lib/supabase/server'2import { Card } from '@/components/ui/card'3import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'4import { Badge } from '@/components/ui/badge'56const statusColors = {7 paid: 'default',8 pending: 'secondary',9 overdue: 'destructive',10 partial: 'outline',11} as const1213export default async function PaymentsPage() {14 const supabase = await createClient()1516 const { data: leases } = await supabase17 .from('leases')18 .select(`19 id, monthly_rent, status,20 units!inner(unit_number, properties!inner(name)),21 tenants!inner(first_name, last_name, email),22 payments(amount, payment_date, status)23 `)24 .eq('status', 'active')25 .order('created_at', { ascending: false })2627 const totalExpected = leases?.reduce((sum, l) => sum + l.monthly_rent, 0) ?? 028 const totalCollected = leases?.reduce((sum, l) => {29 const paid = l.payments?.filter((p: { status: string }) => p.status === 'paid')30 .reduce((s: number, p: { amount: number }) => s + p.amount, 0) ?? 031 return sum + paid32 }, 0) ?? 03334 return (35 <div className="p-6 space-y-6">36 <div className="grid grid-cols-3 gap-4">37 <Card className="p-4">38 <p className="text-sm text-muted-foreground">Expected</p>39 <p className="text-2xl font-bold">${(totalExpected / 100).toLocaleString()}</p>40 </Card>41 <Card className="p-4">42 <p className="text-sm text-muted-foreground">Collected</p>43 <p className="text-2xl font-bold">${(totalCollected / 100).toLocaleString()}</p>44 </Card>45 <Card className="p-4">46 <p className="text-sm text-muted-foreground">Outstanding</p>47 <p className="text-2xl font-bold text-destructive">48 ${((totalExpected - totalCollected) / 100).toLocaleString()}49 </p>50 </Card>51 </div>5253 <Table>54 <TableHeader>55 <TableRow>56 <TableHead>Property</TableHead>57 <TableHead>Unit</TableHead>58 <TableHead>Tenant</TableHead>59 <TableHead>Rent</TableHead>60 <TableHead>Status</TableHead>61 </TableRow>62 </TableHeader>63 <TableBody>64 {leases?.map((lease) => (65 <TableRow key={lease.id}>66 <TableCell>{(lease.units as any).properties.name}</TableCell>67 <TableCell>{(lease.units as any).unit_number}</TableCell>68 <TableCell>69 {(lease.tenants as any).first_name} {(lease.tenants as any).last_name}70 </TableCell>71 <TableCell>${(lease.monthly_rent / 100).toLocaleString()}</TableCell>72 <TableCell>73 <Badge variant={statusColors[(lease.payments?.[0] as any)?.status ?? 'pending'] ?? 'secondary'}>74 {(lease.payments?.[0] as any)?.status ?? 'pending'}75 </Badge>76 </TableCell>77 </TableRow>78 ))}79 </TableBody>80 </Table>81 </div>82 )83}Expected result: A rent roll page showing expected, collected, and outstanding revenue Cards plus a Table of all active leases with tenant names, rent amounts, and payment status Badges.
Build the maintenance request queue
Create a maintenance request page where tenants submit requests and landlords manage them through a status workflow (open → in progress → completed). Priority levels help triage emergency repairs.
1// Paste this prompt into V0's AI chat:2// Build a maintenance request page at app/maintenance/page.tsx.3// Requirements:4// - Show all maintenance requests in a shadcn/ui Table5// - Columns: unit number, tenant name, title, priority Badge (emergency=red, high=orange, medium=blue, low=gray), status Badge (open=yellow, in_progress=blue, completed=green), created_at6// - Filter by status using shadcn/ui Tabs: All, Open, In Progress, Completed7// - Filter by priority using Select dropdown8// - Sort by priority (emergency first) then created_at9// - Clicking a row opens a Dialog with full description and status update controls10// - Status update: Select for new status, Textarea for notes, Button to save11// - "New Request" Button opens Dialog with: Select for unit, Input for title, Textarea for description, Select for priority12// - Server Actions: createMaintenanceRequest(), updateMaintenanceStatus()13// - Use Server Components for data fetchingPro tip: Use V0's Design Mode (Option+D) to visually adjust the maintenance table row heights, Badge colors, and Dialog widths for free.
Expected result: A maintenance queue with filterable Table, priority and status Badges, and a Dialog for updating request status. Emergency requests appear at the top.
Add the tenant onboarding and lease management flow
Build forms for adding new tenants and creating leases. Include a tenant directory page with search and the lease creation flow that links tenants to units.
1// Paste this prompt into V0's AI chat:2// Build tenant and lease management:3// 1. Tenant directory at app/tenants/page.tsx:4// - Searchable Table of all tenants with first_name, last_name, email, phone, active lease unit5// - "Add Tenant" Button opens Dialog with Input fields for name, email, phone6// - Clicking a tenant row shows their lease history and payment history7// - Server Action: createTenant()8// 2. Lease creation at app/properties/[id]/page.tsx:9// - In the property detail view, each unit Card has an "Add Lease" Button (only for vacant units)10// - Dialog with: Select for tenant (from tenants table), Calendar for start_date and end_date, Input for monthly_rent and deposit11// - Creating a lease automatically sets unit status to 'occupied'12// - AlertDialog for lease termination ("This will mark the unit as vacant")13// - Server Actions: createLease(), terminateLease()14// 3. Both use Server Components for data fetching, 'use client' for interactive formsExpected result: A tenant directory with search and add functionality, plus a lease creation Dialog on each vacant unit that links tenants to units and updates occupancy status.
Complete code
1'use server'23import { createClient } from '@/lib/supabase/server'4import { auth } from '@clerk/nextjs/server'5import { revalidatePath } from 'next/cache'67export async function createUnit(8 propertyId: string,9 data: {10 unit_number: string11 bedrooms: number12 bathrooms: number13 rent_amount: number14 }15) {16 const supabase = await createClient()1718 await supabase.from('units').insert({19 property_id: propertyId,20 ...data,21 status: 'vacant',22 })2324 revalidatePath(`/properties/${propertyId}`)25}2627export async function recordPayment(28 leaseId: string,29 amount: number,30 method: string31) {32 const supabase = await createClient()3334 await supabase.from('payments').insert({35 lease_id: leaseId,36 amount,37 payment_date: new Date().toISOString().split('T')[0],38 method,39 status: 'paid',40 })4142 revalidatePath('/payments')43}4445export async function updateMaintenanceStatus(46 requestId: string,47 status: string48) {49 const supabase = await createClient()5051 await supabase52 .from('maintenance_requests')53 .update({ status })54 .eq('id', requestId)5556 revalidatePath('/maintenance')57}Customization ideas
Add lease renewal reminders
Create a scheduled API route that checks for leases expiring within 60 days and sends email reminders to both landlord and tenant via Resend.
Build a tenant portal
Create a separate tenant-facing interface where tenants can view their lease, submit maintenance requests, and see payment history.
Add document management
Use Supabase Storage to attach lease agreements, inspection photos, and maintenance receipts to their respective records.
Integrate online rent payments
Add Stripe via Vercel Marketplace so tenants can pay rent online with automatic status updates when payments are confirmed via webhook.
Common pitfalls
Pitfall: Calculating occupancy rates client-side by fetching all units
How to avoid: Use Supabase database views that join units and leases server-side and return pre-computed occupancy rates. Only the aggregated number reaches the client.
Pitfall: Not updating unit status when creating or terminating a lease
How to avoid: In the createLease Server Action, update both the leases table AND the units table (status='occupied') in the same operation. Do the reverse in terminateLease.
Pitfall: Storing rent amounts as floating-point dollars
How to avoid: Store all monetary amounts as integers in cents. Display by dividing by 100. This eliminates rounding errors in calculations.
Best practices
- Store all monetary amounts as integers in cents to avoid floating-point rounding errors
- Use Supabase database views for computed metrics like occupancy rates and revenue projections
- Use V0's Connect panel for provisioning Supabase with the full schema — prompt V0 with the table structure for auto-generated migrations
- Set RLS policies ensuring owners only see their own properties (owner_id = auth.uid())
- Use Server Components for all data-heavy pages (rent roll, tenant directory) to keep financial data server-side
- Use V0's Design Mode (Option+D) to visually adjust dashboard Card spacing and Table row heights for free
AI prompts to try
Copy these prompts to build this project faster.
I'm building a property management system with Next.js App Router and Supabase. Help me create a Supabase database view that calculates occupancy rates and monthly revenue per property. Join properties, units, and leases tables. Return property name, total units, occupied units, occupancy percentage, and sum of active lease monthly rents. Also write a view for upcoming lease expirations within 60 days.
Create a property detail page showing all units in a grid of Cards. Each Card shows unit number, bedrooms/bathrooms, rent amount, and status Badge (vacant=green, occupied=blue, maintenance=orange). Vacant units have an 'Add Lease' Button that opens a Dialog with tenant Select, date pickers, and rent Input. Use a Server Action that creates the lease and updates the unit status to occupied.
Frequently asked questions
What V0 plan do I need for a property management system?
V0 Free works for the basic build, but Premium ($20/month) is recommended because the system has multiple complex pages (dashboard, rent roll, maintenance, tenants) that benefit from prompt queuing.
Can tenants access the system to submit maintenance requests?
The base build is landlord-focused. To add a tenant portal, create a separate set of pages with tenant-specific RLS policies that only show their own lease, payments, and the ability to create maintenance requests for their unit.
How do I handle multiple properties across different locations?
The schema supports multiple properties per owner. The dashboard shows aggregate metrics across all properties, and each property has its own detail page with unit-level management.
How do I deploy the property management system?
Click Share then Publish to Production in V0. Set CLERK_SECRET_KEY in the Vars tab without NEXT_PUBLIC_ prefix. Supabase credentials are auto-configured from the Connect panel.
Can I add online rent payment processing?
Yes. Add Stripe via Vercel Marketplace for one-click setup. Create a checkout route that generates a Stripe Payment Intent for the rent amount, and a webhook handler that updates the payment status to paid when confirmed.
Can RapidDev help build a custom property management platform?
Yes. RapidDev has built 600+ apps including property management systems with tenant portals, online payments, document management, and accounting integrations. Book a free consultation to discuss your needs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation