Skip to main content
RapidDev - Software Development Agency

How to Build a Inventory System with Lovable

Build a real-time inventory management system in Lovable where stock movements automatically update product levels via a Postgres trigger, low-stock alerts fire through Supabase Realtime, and bulk stock adjustments execute atomically via an Edge Function — complete with a searchable product catalog, movement history, and dashboard charts.

What you'll build

  • Product catalog with current stock levels and reorder points
  • Stock movements table with a trigger that updates product stock atomically
  • Realtime low-stock alert banner that fires when stock drops below reorder threshold
  • Edge Function for bulk stock adjustments with transaction safety
  • Movement history DataTable with filters for product, type, and date range
  • Dashboard with stock health overview Cards and a Recharts bar chart of top low-stock items
  • CSV export for current inventory snapshot
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a real-time inventory management system in Lovable where stock movements automatically update product levels via a Postgres trigger, low-stock alerts fire through Supabase Realtime, and bulk stock adjustments execute atomically via an Edge Function — complete with a searchable product catalog, movement history, and dashboard charts.

What you're building

Inventory systems have two cardinal rules: stock levels must always be accurate, and every change must be traceable. This build enforces both through a double-entry movement ledger.

Every stock change is an INSERT into stock_movements (product_id, quantity_change, movement_type, reference). A Postgres AFTER INSERT trigger recalculates and updates the product's current_stock column. This means stock_levels is always a derivable value — if the trigger is ever suspected of drift, you can recalculate from the full movements history.

Low-stock alerts use Supabase Realtime. The trigger also checks if the updated stock falls below the product's reorder_point and inserts a row into a low_stock_alerts table. The Lovable frontend subscribes to this table via a Realtime channel and shows a dismissible alert banner without any polling.

Bulk adjustments (e.g. quarterly stocktake corrections) use an Edge Function that wraps multiple stock_movements INSERTs inside a Postgres transaction. If any product in the batch has invalid data, the entire batch rolls back — no partial inventory states.

Final result

A real-time inventory dashboard where every stock change is auditable, low-stock alerts are instant, and bulk operations are atomic.

Tech stack

LovableFrontend dashboard
SupabaseDatabase with RLS and Realtime
Supabase Edge FunctionsBulk adjustment transactions (Deno)
shadcn/uiUI components
RechartsStock level charts
TanStack Table v8Product and movement DataTables

Prerequisites

  • Lovable Pro account for Edge Function generation
  • Supabase project with SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in Cloud tab → Secrets
  • A list of your product names and SKUs ready to seed the products table
  • Understanding of the movement types your business uses (received, sold, adjusted, damaged)
  • Optional: an existing orders or sales table to reference from stock movements

Build steps

1

Create the inventory schema with stock trigger

Ask Lovable to create the tables and the trigger that keeps stock_level in sync with every movement. The trigger is the foundation — all other features rely on it being correct.

prompt.txt
1Create an inventory management schema in Supabase.
2
3Tables:
4- products: id, sku (text unique), name, description, category, unit_cost (decimal), current_stock (int default 0), reorder_point (int default 10), reorder_quantity (int default 50), location (text), is_active (bool default true), created_at, updated_at
5- stock_movements: id, product_id (references products), quantity_change (int, positive = stock in, negative = stock out), movement_type ('received' | 'sold' | 'adjusted' | 'damaged' | 'returned'), reference_id (text, order/PO number), notes (text), created_by (references auth.users), created_at
6- low_stock_alerts: id, product_id (references products), current_stock (int), reorder_point (int), resolved_at (timestamptz), created_at
7
8Create a Postgres trigger function update_stock_after_movement() that fires AFTER INSERT on stock_movements:
91. UPDATE products SET current_stock = current_stock + NEW.quantity_change, updated_at = now() WHERE id = NEW.product_id
102. After update, check if new current_stock < reorder_point. If so, and if no unresolved alert exists for this product, INSERT into low_stock_alerts.
11
12RLS:
13- products: authenticated users can SELECT. Admin role required for INSERT/UPDATE/DELETE.
14- stock_movements: authenticated users SELECT and INSERT their own rows.
15- low_stock_alerts: authenticated users SELECT, service role INSERT/UPDATE.
16
17Add a constraint: CHECK(current_stock >= 0) on products to prevent negative stock.

Pro tip: The CHECK(current_stock >= 0) constraint will cause the trigger to fail — and roll back the movement — if a sale would result in negative stock. This is a feature: it enforces that you can only sell what you have. To allow backorders, remove the constraint and add a separate backorder_stock column.

Expected result: Tables are created. Inserting a stock_movement with quantity_change=-5 for a product updates current_stock by -5 and inserts a low_stock_alert if stock is now below reorder_point. The TypeScript types are generated.

2

Set up Realtime low-stock alerts

Ask Lovable to subscribe to the low_stock_alerts table and display a live alert banner. This requires enabling Realtime on the table in Supabase and subscribing on the client.

prompt.txt
1Add real-time low-stock alerts to the inventory dashboard.
2
31. In the Supabase dashboard, enable Realtime for the low_stock_alerts table (Supabase will handle this when I ask you to set up the subscription).
4
52. Create a custom hook src/hooks/useLowStockAlerts.ts:
6 - Subscribe to INSERT events on the low_stock_alerts table using supabase.channel('low-stock').on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'low_stock_alerts' }, callback).subscribe()
7 - Maintain a state array of unresolved alerts (where resolved_at is null)
8 - On initial load, fetch existing unresolved alerts
9 - When a new alert arrives via Realtime, add it to state
10 - Export a dismissAlert(id) function that updates resolved_at = now() and removes from state
11
123. In the main dashboard layout, render an AlertBanner component:
13 - If lowStockAlerts.length > 0, show a yellow Alert (shadcn/ui) at the top
14 - Alert content: 'X products are low on stock' with a link to the low-stock filter view
15 - Each individual alert can be dismissed (calls dismissAlert)
16 - Animate in with a smooth slide-down using CSS transition

Expected result: When a stock_movement causes stock to drop below reorder_point, the low-stock alert appears in the dashboard within 1-2 seconds without a page refresh. Dismissing an alert marks it resolved in the database.

3

Build the bulk adjustment Edge Function

Stocktakes and bulk corrections need to update many products at once atomically. Build an Edge Function that wraps a batch of movements in a single Postgres transaction.

supabase/functions/bulk-adjust-stock/index.ts
1// supabase/functions/bulk-adjust-stock/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8 'Content-Type': 'application/json',
9}
10
11type Adjustment = {
12 product_id: string
13 quantity_change: number
14 notes?: string
15}
16
17serve(async (req: Request) => {
18 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
19
20 try {
21 const supabase = createClient(
22 Deno.env.get('SUPABASE_URL') ?? '',
23 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
24 )
25
26 const { adjustments, reference_id, created_by } = await req.json() as {
27 adjustments: Adjustment[]
28 reference_id: string
29 created_by: string
30 }
31
32 if (!adjustments?.length || !reference_id) {
33 return new Response(JSON.stringify({ error: 'adjustments and reference_id required' }),
34 { status: 400, headers: corsHeaders })
35 }
36
37 const movements = adjustments.map((adj) => ({
38 product_id: adj.product_id,
39 quantity_change: adj.quantity_change,
40 movement_type: 'adjusted',
41 reference_id,
42 notes: adj.notes ?? 'Bulk stocktake adjustment',
43 created_by,
44 }))
45
46 // Supabase inserts in batch — trigger fires per row, all in one transaction
47 const { data, error } = await supabase
48 .from('stock_movements')
49 .insert(movements)
50 .select()
51
52 if (error) throw error
53
54 return new Response(JSON.stringify({ success: true, inserted: data.length }), { headers: corsHeaders })
55 } catch (err) {
56 const message = err instanceof Error ? err.message : 'Internal error'
57 return new Response(JSON.stringify({ error: message }), { status: 500, headers: corsHeaders })
58 }
59})

Pro tip: Supabase batch INSERTs are not wrapped in a single transaction by default. For true atomicity, create a Postgres function bulk_insert_movements(movements jsonb) using LANGUAGE plpgsql that loops through the array and calls INSERT in a single function body. Functions in Postgres run in an implicit transaction.

Expected result: The Edge Function accepts a JSON array of adjustments and inserts all stock_movements in one operation. If any row fails the CHECK constraint, none are committed.

4

Build the product catalog and movement history

Ask Lovable to create the two main pages: the product catalog where staff can add products and log single movements, and the movement history with filters.

prompt.txt
1Build two pages:
2
31. Products page (src/pages/Products.tsx):
4 - DataTable of all active products. Columns: SKU (monospace), Name, Category, Current Stock (red text if below reorder_point), Reorder Point, Location, Actions (Log Movement button)
5 - Search Input above table filtering by SKU or name
6 - 'Add Product' Button opening a Sheet with a form (react-hook-form + zod): SKU, name, description, category Select (Electronics/Clothing/Food/Other), unit_cost, reorder_point, reorder_quantity, location
7 - 'Log Movement' button opens a Dialog: movement type Select (received/sold/adjusted/damaged/returned), quantity Input (positive number), reference_id Input, notes Textarea. Submitting calls supabase.from('stock_movements').insert()
8 - Low-stock products row highlighted with amber background
9
102. Movements page (src/pages/Movements.tsx):
11 - DataTable of stock_movements joined with product name
12 - Columns: Date, Product Name, SKU, Type Badge (color-coded), Quantity Change (green for positive, red for negative), Reference ID, Notes, Created By
13 - Filter bar: product Select, movement_type multi-select, date range Popover with Calendar
14 - Export CSV Button that downloads filtered results

Expected result: Both pages render correctly. Logging a movement from the product page inserts a stock_movements row, the trigger fires, and the current_stock in the product table updates. The movements history reflects the new entry.

5

Build the dashboard overview

The main dashboard page gives managers a quick health view of the entire inventory. Ask Lovable to build the summary cards and charts.

prompt.txt
1Build a dashboard home page at src/pages/Dashboard.tsx.
2
3Layout:
4- Row of four stat Cards: Total Products (count), Total Stock Value (SUM of current_stock * unit_cost formatted as currency), Low Stock Items (count WHERE current_stock < reorder_point), Movements Today (count WHERE created_at > today)
5- Below cards: two-column layout
6 - Left: Recharts BarChart of the 10 products with the lowest stock as a percentage of reorder_point. X-axis = product name (truncated), Y-axis = current_stock. Bars colored red if below reorder_point.
7 - Right: recent movements feed last 10 stock_movements as a simple list showing product name, type Badge, quantity change, and relative time
8- Add a 'Quick Add Movement' floating action Button (bottom-right) that opens the Log Movement Dialog without navigating away
9
10All counts and aggregations are Supabase queries run in parallel with Promise.all on component mount. Show Skeleton loading placeholders while data is fetching.

Expected result: The dashboard shows live stock health across all cards and charts. The bar chart immediately reflects recent movements. The floating action button opens the movement dialog from any view on the dashboard.

Complete code

src/hooks/useLowStockAlerts.ts
1import { useEffect, useState } from 'react'
2import { supabase } from '@/integrations/supabase/client'
3
4type LowStockAlert = {
5 id: string
6 product_id: string
7 current_stock: number
8 reorder_point: number
9 created_at: string
10 products: { name: string; sku: string } | null
11}
12
13export function useLowStockAlerts() {
14 const [alerts, setAlerts] = useState<LowStockAlert[]>([])
15 const [loading, setLoading] = useState(true)
16
17 useEffect(() => {
18 const fetchAlerts = async () => {
19 const { data } = await supabase
20 .from('low_stock_alerts')
21 .select('*, products(name, sku)')
22 .is('resolved_at', null)
23 .order('created_at', { ascending: false })
24 setAlerts(data ?? [])
25 setLoading(false)
26 }
27
28 fetchAlerts()
29
30 const channel = supabase
31 .channel('low-stock-alerts')
32 .on(
33 'postgres_changes',
34 { event: 'INSERT', schema: 'public', table: 'low_stock_alerts' },
35 async (payload) => {
36 const { data } = await supabase
37 .from('low_stock_alerts')
38 .select('*, products(name, sku)')
39 .eq('id', payload.new.id)
40 .single()
41 if (data) setAlerts((prev) => [data, ...prev])
42 }
43 )
44 .subscribe()
45
46 return () => { supabase.removeChannel(channel) }
47 }, [])
48
49 const dismissAlert = async (id: string) => {
50 await supabase
51 .from('low_stock_alerts')
52 .update({ resolved_at: new Date().toISOString() })
53 .eq('id', id)
54 setAlerts((prev) => prev.filter((a) => a.id !== id))
55 }
56
57 return { alerts, loading, dismissAlert }
58}

Customization ideas

Automated purchase order generation

When a low_stock_alert is created, trigger an Edge Function that generates a purchase_orders row with the product's reorder_quantity and preferred_supplier. Email the purchase order to the supplier using Resend. The admin can approve or modify it from a purchase orders page before it becomes official.

Barcode scanning support

Add a barcode field to products. On the mobile view of the Log Movement page, add a camera input that reads barcodes using a WebRTC-based library. Scanning a barcode auto-populates the product selector, making warehouse operations much faster on mobile devices.

Multi-location inventory

Add a locations table and a location_id column to both products and stock_movements. Stock levels become per-location. Add a transfer movement type that creates a negative movement at the source location and a positive one at the destination location, wrapped in the bulk adjust Edge Function.

Supplier and purchase order tracking

Add suppliers and purchase_orders tables. Link purchase orders to stock movements with movement_type='received'. When you receive a purchase order, create movements for each line item. This gives you a complete inbound supply chain view.

Inventory valuation reports

Add a reports page with a monthly valuation table: each product, average cost (using FIFO or weighted average from movements), units, and total value. Export as PDF using the browser's print API. This satisfies common accounting requirements for inventory-heavy businesses.

Common pitfalls

Pitfall: Updating current_stock directly instead of going through stock_movements

How to avoid: Treat current_stock as a read-only computed value. All stock changes must be INSERTs into stock_movements. Deny direct UPDATE on products.current_stock by removing it from the RLS UPDATE policy columns list.

Pitfall: Forgetting to enable Realtime for the low_stock_alerts table

How to avoid: In your Supabase project, go to Database → Replication and add the low_stock_alerts table to the publication. Ask Lovable to include this in the setup prompt or enable it manually in the Supabase dashboard.

Pitfall: Not handling the case where a sale creates negative stock

How to avoid: In the Log Movement form, check available stock before submitting. In the bulk adjustment Edge Function, catch the constraint violation error and return a user-friendly message: 'Product [name] would go negative. Movement rejected.'

Pitfall: Using the service role key in client-side code

How to avoid: The service role key is only for Edge Functions (accessed via Deno.env.get). All client-side Supabase queries use the anon key. The anon key is safe to expose because RLS policies control what data is accessible.

Best practices

  • Never allow direct updates to the current_stock column from the application. All stock changes flow through stock_movements so the ledger is always complete and auditable.
  • Add a not-null reference_id requirement for movements of type 'sold' or 'received'. This ensures every stock change can be traced back to an order or purchase order.
  • Use optimistic updates in the UI for single movements: update the local state immediately, then confirm with the database response. If the database rejects the movement (e.g. negative stock), revert and show an error.
  • Index stock_movements on (product_id, created_at DESC) for fast movement history queries and on (created_at) for the dashboard's today count.
  • Set up a Supabase alert or pg_cron job that recalculates current_stock from the movements ledger weekly as a consistency check. Log any discrepancies to an audit table.
  • Export the bulk adjustment template as a CSV download. Staff fill in the spreadsheet and upload it. Parse the CSV in the browser and preview changes before submitting to the Edge Function.
  • Use Row Level Security to restrict which users can log which movement types. Warehouse staff can log 'received' and 'damaged'. Only managers can log 'adjusted'. Implement this via a role check in the movements INSERT policy.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building an inventory system in Supabase where stock levels are maintained via a trigger on a stock_movements table. I need a Postgres trigger function update_stock_after_movement() that fires AFTER INSERT on stock_movements, updates the product's current_stock, and also inserts a low_stock_alert if the new stock is below reorder_point (but only if no unresolved alert already exists). Show me the full plpgsql trigger function.

Lovable Prompt

Add a stocktake page at /stocktake. It should load all active products and display them in an editable grid with columns: SKU, Name, Current Stock (read-only), Counted Stock (number Input editable by user), Difference (auto-calculated Current minus Counted, shown in red/green). At the bottom, show a summary: X products with discrepancies, total units variance. A Submit Stocktake Button sends all non-zero differences to the bulk-adjust-stock Edge Function with movement_type='adjusted' and reference_id='STOCKTAKE-{date}'.

Build Prompt

In Supabase, create a pg_cron job that runs every Monday at 8am. It should query all products where current_stock < reorder_point and resolved_at is null in low_stock_alerts, then insert rows into a weekly_reorder_report table with product_id, current_stock, reorder_point, reorder_quantity, and report_date = current_date. This table can be read by an Edge Function that emails the purchasing manager a formatted reorder list every Monday morning.

Frequently asked questions

How does the stock trigger handle concurrent movements?

Postgres processes each INSERT into stock_movements serially within the transaction. The trigger uses UPDATE products SET current_stock = current_stock + NEW.quantity_change, which is an atomic increment. If two movements for the same product arrive simultaneously, Postgres serializes them at the row lock level, so neither is lost or double-applied.

What if I need to allow negative stock for backorders?

Remove the CHECK(current_stock >= 0) constraint from the products table. Add a separate column allow_backorder (boolean default false) per product. Modify the trigger to only enforce non-negative stock when allow_backorder is false. The UI can show negative stock in a distinct color to differentiate it from normal levels.

How do I seed the products table with my existing inventory data?

Prepare a CSV with columns matching your products table schema. In the Supabase dashboard, go to Table Editor → products → Import Data and upload the CSV. Alternatively, ask Lovable to build a CSV import page that parses the file client-side and batches INSERT calls in groups of 100 rows.

Can Supabase Realtime handle a high volume of stock movements?

Supabase Realtime is suitable for alert-level notifications (low-stock events are infrequent). For the movement feed in the dashboard, polling every 30 seconds is more appropriate than Realtime for high-volume systems, as Realtime connections are limited on the free tier. Use Realtime for the low_stock_alerts table and polling for movement history.

How do I connect inventory to an order management system?

Create an Edge Function process-order that accepts an array of line items (product_id, quantity). For each item, it inserts a stock_movement with type='sold' and the order ID as reference_id. Call this function from your order checkout flow after payment is confirmed. If any product has insufficient stock, the entire function should return an error before any movements are inserted.

How do I generate a snapshot of current inventory for accounting?

The dashboard's Export CSV button should query SELECT sku, name, current_stock, unit_cost, (current_stock * unit_cost) as total_value FROM products WHERE is_active = true ORDER BY category, name. Format it as CSV in the browser and trigger a download. Include the export timestamp in the filename for version control.

What is the best way to track inventory that has an expiry date?

Add an expiry_date column to stock_movements (for received items) and to a separate inventory_lots table. When stock is sold, consume the oldest lots first (FIFO). Add a pg_cron job that checks daily for lots expiring within 30 days and creates low_stock_alerts with type='expiring'. The movement history then shows which lot each unit came from.

Is there help available for building a more complex inventory system?

RapidDev builds Lovable apps with complex backend logic including multi-location inventory, supplier portals, and ERP integrations. Reach out if your inventory requirements go beyond this guide.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.