Build a warehouse inventory management system with V0 using Next.js, Supabase, and shadcn/ui. Track stock levels across warehouses, log inventory movements, and get low-stock alerts — all with atomic database transactions to prevent race conditions. Takes about 1-2 hours to complete.
What you're building
Small business owners lose thousands of dollars to stockouts and overstock because they track inventory in spreadsheets. A proper inventory system gives you real-time visibility into stock levels, automates low-stock alerts, and creates an audit trail of every movement.
V0 makes building this dramatically faster. You describe your inventory tables and UI in the chat, and V0 generates Next.js pages with shadcn/ui DataTables, forms, and filters. Connect Supabase via the Connect panel for instant database setup, and you have a working system on Vercel without touching a terminal.
The architecture uses Next.js App Router with Server Components for the stock overview (zero JavaScript on initial load), a Supabase RPC function for atomic stock updates, and client components only where interactivity is needed — forms, filters, and the warehouse picker.
Final result
A fully functional inventory management system with stock tracking across multiple warehouses, movement logging, low-stock alerts, and goods receipt processing.
Tech stack
Prerequisites
- A V0 account (Premium or higher recommended for multiple prompts)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Basic understanding of inventory concepts (SKUs, stock levels, movements)
- A list of products and warehouse locations you want to track
Build steps
Set up the project and database schema with Supabase
Open V0 and create a new project. Use the Connect panel to add Supabase — this auto-provisions your database URL and keys. Then prompt V0 to create the full inventory schema with products, warehouses, inventory levels, and movements.
1// Paste this prompt into V0's AI chat:2// Build an inventory management system. Create a Supabase schema with these tables:3// 1. products: id (uuid PK), sku (text UNIQUE), name (text), description (text), category (text), unit_price (numeric), reorder_point (int), created_at (timestamptz)4// 2. warehouses: id (uuid PK), name (text), location (text)5// 3. inventory: id (uuid PK), product_id (uuid FK to products), warehouse_id (uuid FK to warehouses), quantity (int), last_counted_at (timestamptz)6// 4. movements: id (uuid PK), product_id (uuid FK to products), warehouse_id (uuid FK to warehouses), movement_type (text CHECK IN 'in','out','transfer','adjustment'), quantity (int), reference (text), created_at (timestamptz), created_by (uuid FK to auth.users)7// Add RLS policies so authenticated users can read all inventory but only insert movements.8// Generate the SQL migration and TypeScript types.Pro tip: Use V0's Connect panel to wire up Supabase in one click — it auto-populates your NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the Vars tab.
Expected result: Supabase is connected, all four tables are created with proper foreign keys, and RLS policies are in place.
Build the stock overview dashboard with DataTable
Prompt V0 to create the main inventory page showing all products with their current stock levels, warehouse locations, and status indicators. This page uses Server Components for fast initial load and a client-side DataTable for sorting and filtering.
1// Paste this prompt into V0's AI chat:2// Create an inventory dashboard at app/inventory/page.tsx.3// Requirements:4// - Fetch all products joined with inventory levels grouped by warehouse5// - Display in a shadcn/ui DataTable with columns: SKU, Product Name, Category, Warehouse, Quantity, Reorder Point, Status6// - Status column shows a Badge: green 'In Stock' if quantity > reorder_point, yellow 'Low Stock' if quantity <= reorder_point and > 0, red 'Out of Stock' if quantity = 07// - Add column sorting (click headers) and a search Input to filter by SKU or product name8// - Add a Select dropdown to filter by warehouse9// - Add a Button 'Receive Stock' that links to /inventory/receive10// - Show summary Cards at the top: Total Products, Low Stock Items, Out of Stock ItemsExpected result: The inventory page displays a sortable, filterable DataTable with color-coded stock status Badges and summary cards at the top.
Create the goods receipt form for incoming stock
Build the stock receiving page where users log incoming inventory. The form needs product selection, warehouse picker, quantity input, and a reference field. On submit, it creates a movement record and updates the stock level atomically.
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { product_id, warehouse_id, quantity, reference } = await req.json()1112 if (!product_id || !warehouse_id || !quantity || quantity <= 0) {13 return NextResponse.json(14 { error: 'Invalid input' },15 { status: 400 }16 )17 }1819 const { data, error } = await supabase.rpc('receive_stock', {20 p_product_id: product_id,21 p_warehouse_id: warehouse_id,22 p_quantity: quantity,23 p_reference: reference || 'Manual receipt',24 })2526 if (error) {27 return NextResponse.json({ error: error.message }, { status: 500 })28 }2930 return NextResponse.json({ success: true, data })31}Pro tip: Use Supabase RPC functions for atomic operations. Create a PostgreSQL function that inserts the movement AND updates the inventory quantity in a single transaction — this prevents race conditions when multiple users receive stock simultaneously.
Expected result: Submitting the receipt form creates a movement record and updates the inventory quantity atomically. The stock overview reflects the new quantity immediately.
Build the product detail page with movement history
Create a dynamic page that shows a single product's details, current stock per warehouse, and a full history of all movements. Use Next.js dynamic routes with a Server Component for the initial data fetch.
1// Paste this prompt into V0's AI chat:2// Create a product detail page at app/inventory/[productId]/page.tsx.3// Requirements:4// - Fetch the product by ID from Supabase with all inventory records joined by warehouse5// - Show product info in a Card: name, SKU, category, unit price, reorder point6// - Show stock per warehouse in a Table with columns: Warehouse, Quantity, Status Badge, Last Counted7// - Below that, show a movement history Table with columns: Date, Type (Badge colored by type), Quantity, Reference, User8// - Movement types: 'in' = green Badge, 'out' = red Badge, 'transfer' = blue Badge, 'adjustment' = yellow Badge9// - Add a Dialog button for quick stock adjustment (select warehouse, enter quantity +/-, add reference note)10// - Use Tabs to switch between 'Stock Levels' and 'Movement History'Expected result: The product detail page shows stock levels per warehouse with status badges, and a full movement history with color-coded movement type indicators.
Create the Supabase RPC function for atomic stock updates
This is the critical piece that prevents race conditions. Create a PostgreSQL function in Supabase that handles both the movement insert and quantity update in a single transaction, using row-level locking to handle concurrent updates safely.
1-- Run this in Supabase SQL Editor2CREATE OR REPLACE FUNCTION receive_stock(3 p_product_id uuid,4 p_warehouse_id uuid,5 p_quantity int,6 p_reference text DEFAULT 'Manual receipt'7)8RETURNS json AS $$9DECLARE10 v_inventory_id uuid;11 v_new_quantity int;12BEGIN13 -- Upsert inventory record with row lock14 INSERT INTO inventory (product_id, warehouse_id, quantity, last_counted_at)15 VALUES (p_product_id, p_warehouse_id, p_quantity, now())16 ON CONFLICT (product_id, warehouse_id)17 DO UPDATE SET18 quantity = inventory.quantity + p_quantity,19 last_counted_at = now()20 RETURNING id, quantity INTO v_inventory_id, v_new_quantity;2122 -- Insert movement record23 INSERT INTO movements (product_id, warehouse_id, movement_type, quantity, reference, created_at)24 VALUES (p_product_id, p_warehouse_id, 'in', p_quantity, p_reference, now());2526 RETURN json_build_object('inventory_id', v_inventory_id, 'new_quantity', v_new_quantity);27END;28$$ LANGUAGE plpgsql SECURITY DEFINER;Expected result: The RPC function is created in Supabase. Calling supabase.rpc('receive_stock', {...}) atomically updates stock and logs the movement.
Add low-stock alert notifications and deploy
Add a visual alert section to the dashboard showing all products below their reorder point. Then configure environment variables and publish to Vercel for your team to use.
1// Paste this prompt into V0's AI chat:2// Add a low-stock alerts section to the inventory dashboard.3// Requirements:4// - Query products where inventory.quantity <= products.reorder_point5// - Display as a list of AlertDialog-style Cards with: product name, SKU, current quantity, reorder point, warehouse6// - Each card has a 'Reorder' Button that pre-fills the receive stock form7// - Sort by urgency (out of stock first, then lowest percentage of reorder point)8// - Show the count in a red Badge on a 'Low Stock Alerts' tab9// - If no low-stock items, show a success message with a check iconPro tip: Use Design Mode (Option+D) to adjust the alert card colors, spacing, and badge styles without spending any credits. Visual tweaks are free in V0.
Expected result: The dashboard shows a Low Stock Alerts tab with urgent items sorted by severity. Clicking Reorder navigates to the receive form pre-filled with the product details.
Complete code
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { product_id, warehouse_id, quantity, movement_type, reference } =11 await req.json()1213 if (!product_id || !warehouse_id || !quantity || quantity <= 0) {14 return NextResponse.json({ error: 'Invalid input' }, { status: 400 })15 }1617 const rpcName =18 movement_type === 'out' ? 'dispatch_stock' : 'receive_stock'1920 const { data, error } = await supabase.rpc(rpcName, {21 p_product_id: product_id,22 p_warehouse_id: warehouse_id,23 p_quantity: quantity,24 p_reference: reference || `Manual ${movement_type}`,25 })2627 if (error) {28 return NextResponse.json({ error: error.message }, { status: 500 })29 }3031 return NextResponse.json({ success: true, data })32}3334export async function GET(req: NextRequest) {35 const { searchParams } = new URL(req.url)36 const productId = searchParams.get('product_id')3738 let query = supabase39 .from('movements')40 .select('*, products(name, sku), warehouses(name)')41 .order('created_at', { ascending: false })42 .limit(50)4344 if (productId) {45 query = query.eq('product_id', productId)46 }4748 const { data, error } = await query4950 if (error) {51 return NextResponse.json({ error: error.message }, { status: 500 })52 }5354 return NextResponse.json({ data })55}Customization ideas
Barcode scanner integration
Add a barcode scanning component using the device camera and a library like quagga2 to quickly look up products by SKU during receiving and stocktaking.
Automated reorder notifications
Set up a Vercel Cron Job that checks stock levels daily and sends email alerts via Resend when products drop below their reorder points.
Stock transfer between warehouses
Add a transfer workflow that decrements stock from one warehouse and increments at another in a single atomic transaction with a transfer reference linking both movements.
Inventory valuation report
Build a reporting page that calculates total inventory value (quantity x unit_price) per warehouse and category, displayed with Recharts bar and pie charts.
Common pitfalls
Pitfall: Updating inventory quantity with separate INSERT and UPDATE queries
How to avoid: Use a Supabase RPC function (PostgreSQL stored procedure) that inserts the movement and updates the quantity in a single transaction with row-level locking.
Pitfall: Using NEXT_PUBLIC_ prefix for the SUPABASE_SERVICE_ROLE_KEY
How to avoid: Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without any prefix. Only use it in API routes (server-side). Use NEXT_PUBLIC_SUPABASE_ANON_KEY for client-side reads.
Pitfall: Not adding a unique constraint on product_id + warehouse_id in the inventory table
How to avoid: Add a UNIQUE constraint on (product_id, warehouse_id) in the inventory table and use ON CONFLICT in your upsert queries.
Pitfall: Allowing negative inventory quantities without validation
How to avoid: Add a CHECK constraint (quantity >= 0) on the inventory table and validate available quantity in the RPC function before processing outbound movements.
Best practices
- Use Supabase RPC functions for all stock-changing operations to guarantee atomic updates and prevent data inconsistencies
- Enable RLS on all tables and scope policies to authenticated users — the anon key should only allow read access to non-sensitive data
- Use V0's Design Mode (Option+D) to adjust DataTable column widths, Badge colors, and card spacing without spending credits
- Add a unique composite index on (product_id, warehouse_id) in the inventory table for fast lookups and to enforce one row per product per warehouse
- Store movement references (PO numbers, transfer IDs) in every movement record for a complete audit trail
- Use Server Components for the stock overview page — the DataTable data loads server-side with zero client JavaScript overhead
- Set reorder_point per product rather than a global threshold, since different products have different lead times and demand patterns
- Use V0's prompt queuing to build the stock overview, product detail, and receipt form in sequence without waiting between prompts
AI prompts to try
Copy these prompts to build this project faster.
I'm building a warehouse inventory management system with Next.js App Router and Supabase. I need a PostgreSQL function that atomically handles stock receiving — it should insert a movement record and update (or create) the inventory quantity in one transaction with proper row locking. The tables are: products (id, sku, name), warehouses (id, name), inventory (product_id, warehouse_id, quantity), movements (product_id, warehouse_id, movement_type, quantity, reference). Please write the SQL function and the TypeScript API route that calls it.
Create a Supabase RPC function called receive_stock that takes product_id, warehouse_id, quantity, and reference as parameters. It should upsert into the inventory table (insert if new product-warehouse combo, update quantity if exists) and insert into the movements table — all in one transaction. Then build a Next.js API route at app/api/movements/route.ts that calls this function. Include proper error handling for invalid inputs and insufficient stock on outbound movements.
Frequently asked questions
How does the inventory system prevent two people from updating the same stock at once?
The system uses a Supabase RPC function (PostgreSQL stored procedure) that runs the movement insert and quantity update in a single database transaction. PostgreSQL's row-level locking ensures that concurrent updates are serialized — one completes before the other starts, preventing incorrect stock counts.
Can I track inventory across multiple warehouses?
Yes. The schema uses a separate warehouses table and the inventory table stores quantity per product-warehouse combination. The stock overview DataTable can be filtered by warehouse using the Select dropdown, and product detail pages show stock levels at each location.
Do I need a paid V0 plan for this project?
You can start on the free tier, but Premium ($20/month) is recommended because the inventory system requires multiple prompts for the dashboard, product detail page, receipt form, and movement history. The free tier's limited credits may run out mid-build.
How do I add barcode scanning to the inventory system?
Add a client component that uses the device camera with a library like quagga2 or html5-qrcode. When a barcode is scanned, look up the product by SKU in Supabase and pre-fill the receipt or adjustment form. Import the library dynamically with next/dynamic and ssr: false since it requires browser APIs.
Can I get email alerts when stock is low?
Yes. Set up a Vercel Cron Job that runs daily, queries products where inventory.quantity is at or below reorder_point, and sends an email summary via the Resend API. Store RESEND_API_KEY in the Vars tab without a NEXT_PUBLIC_ prefix.
How do I deploy the inventory system to production?
Click Share in the top-right corner of V0, then Publish, then Publish to Production. Your app deploys to Vercel in 30-60 seconds. Alternatively, connect to GitHub via the Git panel — V0 creates a branch, you merge the PR, and Vercel auto-deploys from main.
Can RapidDev help build a custom inventory system?
Yes. RapidDev has built over 600 apps including inventory and warehouse management systems with barcode scanning, multi-location transfers, and ERP integrations. Book a free consultation to discuss your specific requirements and get a production-ready system.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation