# How to Build Inventory system with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

## Step-by-step guide

### 1. 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.

```
// Paste this prompt into V0's AI chat:
// Build an inventory management system. Create a Supabase schema with these tables:
// 1. products: id (uuid PK), sku (text UNIQUE), name (text), description (text), category (text), unit_price (numeric), reorder_point (int), created_at (timestamptz)
// 2. warehouses: id (uuid PK), name (text), location (text)
// 3. inventory: id (uuid PK), product_id (uuid FK to products), warehouse_id (uuid FK to warehouses), quantity (int), last_counted_at (timestamptz)
// 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)
// Add RLS policies so authenticated users can read all inventory but only insert movements.
// 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.

### 2. 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.

```
// Paste this prompt into V0's AI chat:
// Create an inventory dashboard at app/inventory/page.tsx.
// Requirements:
// - Fetch all products joined with inventory levels grouped by warehouse
// - Display in a shadcn/ui DataTable with columns: SKU, Product Name, Category, Warehouse, Quantity, Reorder Point, Status
// - 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 = 0
// - Add column sorting (click headers) and a search Input to filter by SKU or product name
// - Add a Select dropdown to filter by warehouse
// - Add a Button 'Receive Stock' that links to /inventory/receive
// - Show summary Cards at the top: Total Products, Low Stock Items, Out of Stock Items
```

**Expected result:** The inventory page displays a sortable, filterable DataTable with color-coded stock status Badges and summary cards at the top.

### 3. 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.

```
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const { product_id, warehouse_id, quantity, reference } = await req.json()

  if (!product_id || !warehouse_id || !quantity || quantity <= 0) {
    return NextResponse.json(
      { error: 'Invalid input' },
      { status: 400 }
    )
  }

  const { data, error } = await supabase.rpc('receive_stock', {
    p_product_id: product_id,
    p_warehouse_id: warehouse_id,
    p_quantity: quantity,
    p_reference: reference || 'Manual receipt',
  })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ success: true, data })
}
```

> 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.

### 4. 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.

```
// Paste this prompt into V0's AI chat:
// Create a product detail page at app/inventory/[productId]/page.tsx.
// Requirements:
// - Fetch the product by ID from Supabase with all inventory records joined by warehouse
// - Show product info in a Card: name, SKU, category, unit price, reorder point
// - Show stock per warehouse in a Table with columns: Warehouse, Quantity, Status Badge, Last Counted
// - Below that, show a movement history Table with columns: Date, Type (Badge colored by type), Quantity, Reference, User
// - Movement types: 'in' = green Badge, 'out' = red Badge, 'transfer' = blue Badge, 'adjustment' = yellow Badge
// - Add a Dialog button for quick stock adjustment (select warehouse, enter quantity +/-, add reference note)
// - 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.

### 5. 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.

```
-- Run this in Supabase SQL Editor
CREATE OR REPLACE FUNCTION receive_stock(
  p_product_id uuid,
  p_warehouse_id uuid,
  p_quantity int,
  p_reference text DEFAULT 'Manual receipt'
)
RETURNS json AS $$
DECLARE
  v_inventory_id uuid;
  v_new_quantity int;
BEGIN
  -- Upsert inventory record with row lock
  INSERT INTO inventory (product_id, warehouse_id, quantity, last_counted_at)
  VALUES (p_product_id, p_warehouse_id, p_quantity, now())
  ON CONFLICT (product_id, warehouse_id)
  DO UPDATE SET
    quantity = inventory.quantity + p_quantity,
    last_counted_at = now()
  RETURNING id, quantity INTO v_inventory_id, v_new_quantity;

  -- Insert movement record
  INSERT INTO movements (product_id, warehouse_id, movement_type, quantity, reference, created_at)
  VALUES (p_product_id, p_warehouse_id, 'in', p_quantity, p_reference, now());

  RETURN json_build_object('inventory_id', v_inventory_id, 'new_quantity', v_new_quantity);
END;
$$ 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.

### 6. 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.

```
// Paste this prompt into V0's AI chat:
// Add a low-stock alerts section to the inventory dashboard.
// Requirements:
// - Query products where inventory.quantity <= products.reorder_point
// - Display as a list of AlertDialog-style Cards with: product name, SKU, current quantity, reorder point, warehouse
// - Each card has a 'Reorder' Button that pre-fills the receive stock form
// - Sort by urgency (out of stock first, then lowest percentage of reorder point)
// - Show the count in a red Badge on a 'Low Stock Alerts' tab
// - If no low-stock items, show a success message with a check icon
```

> Pro 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 example

File: `app/api/movements/route.ts`

```typescript
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const { product_id, warehouse_id, quantity, movement_type, reference } =
    await req.json()

  if (!product_id || !warehouse_id || !quantity || quantity <= 0) {
    return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
  }

  const rpcName =
    movement_type === 'out' ? 'dispatch_stock' : 'receive_stock'

  const { data, error } = await supabase.rpc(rpcName, {
    p_product_id: product_id,
    p_warehouse_id: warehouse_id,
    p_quantity: quantity,
    p_reference: reference || `Manual ${movement_type}`,
  })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ success: true, data })
}

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url)
  const productId = searchParams.get('product_id')

  let query = supabase
    .from('movements')
    .select('*, products(name, sku), warehouses(name)')
    .order('created_at', { ascending: false })
    .limit(50)

  if (productId) {
    query = query.eq('product_id', productId)
  }

  const { data, error } = await query

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ data })
}
```

## Common mistakes

- **Updating inventory quantity with separate INSERT and UPDATE queries** — Two separate queries can cause race conditions when multiple users update the same product simultaneously, leading to incorrect stock counts. Fix: Use a Supabase RPC function (PostgreSQL stored procedure) that inserts the movement and updates the quantity in a single transaction with row-level locking.
- **Using NEXT_PUBLIC_ prefix for the SUPABASE_SERVICE_ROLE_KEY** — The service role key bypasses Row Level Security. Exposing it in the browser lets anyone read and modify all inventory data. Fix: 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.
- **Not adding a unique constraint on product_id + warehouse_id in the inventory table** — Without this constraint, receiving stock for the same product at the same warehouse can create duplicate inventory rows instead of updating the existing one. Fix: Add a UNIQUE constraint on (product_id, warehouse_id) in the inventory table and use ON CONFLICT in your upsert queries.
- **Allowing negative inventory quantities without validation** — Dispatching more stock than available leads to negative quantities that break reporting and cause fulfillment errors. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/inventory-system
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/inventory-system
