Skip to main content
RapidDev - Software Development Agency
Lovable PromptsInternal ToolsIntermediate

Build an Inventory System in Lovable

A staff-only inventory tracker with SKUs, multi-location stock, an immutable movement ledger (receipt/sale/adjustment/transfer), atomic stock decrements via a Postgres RPC with a CHECK(qty >= 0) constraint, and Resend low-stock alert emails.

Time to MVP

~1 day

Credits

~100–180 credits for full chain

Difficulty

Intermediate

Cloud features

4

TL;DR

The one-paragraph version before you dive in.

Paste the starter prompt into Lovable Build mode and get a staff-only inventory tracker with SKUs, multi-location stock, a movement ledger, atomic stock decrements via a Postgres RPC, and color-coded low-stock badges. Build time is roughly one day. Expected credit burn is 100–180 credits on a Pro $25/mo plan.

Setup checklist

Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.

Cloud tab settings

Database

Stores the 5-table schema (products, locations, stock, movements, low_stock_alerts) and hosts the record_movement() SECURITY DEFINER RPC that atomically keeps stock consistent.

  1. 1Click the + button next to Preview in the top toolbar to open the Cloud tab
  2. 2Click Database — you will see an empty Table Editor
  3. 3Leave it empty — the starter prompt creates all 5 tables, the has_role() function, the record_movement() RPC, all RLS policies, and seeds 5 products + 2 locations with opening stock

Auth

Email+password staff login. Role (admin or staff) is stored in app_metadata so it's baked into the JWT and cannot be tampered with from the client.

  1. 1Cloud tab → Users & Auth
  2. 2Confirm Email sign-in is enabled (it is by default)
  3. 3Toggle 'Allow new users to sign up' to OFF — invite staff manually from the Users page
  4. 4After the build, set each user's app_metadata to {"role": "staff"} (or "admin") from Cloud tab → Users & Auth → click the user row → Edit

Storage

Holds product images uploaded from the Products module — one private bucket with admin-only read/write.

  1. 1Cloud tab → Storage
  2. 2Click Create bucket, name it 'product-images', leave Public OFF
  3. 3Note the name — the starter prompt references it in the products table image_path column

Edge Functions

check-low-stock runs daily via pg_cron to email buyers; import-csv handles bulk product and stock import from spreadsheets.

  1. 1Cloud tab → Edge Functions
  2. 2No setup needed before the starter — follow-up #1 scaffolds check-low-stock and follow-up #2 scaffolds import-csv
  3. 3After those follow-ups, add the RESEND_API_KEY secret in Cloud tab → Secrets before deploying

Secrets (Cloud tab → Secrets)

RESEND_API_KEY

Purpose: Enables the check-low-stock Edge Function to send daily low-stock alert emails to buyers

Where to get it: Sign up at resend.com → API Keys → Create API Key. Copy the key starting with re_.

Preflight checklist

  • You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded — that's the default)
  • You're on Pro $25/mo — the starter prompt alone runs ~50–70 credits, and the full chain with CSV import and transfers reaches 100–180 credits
  • You understand the core pattern: stock is a DERIVED value maintained by the record_movement() RPC — you will never write directly to the stock table from client code

The starter prompt — paste this first

Copy this. Paste it into Lovable Build mode (the default chat at the bottom-left of the editor). Hit send.

lovable-agent-mode.txt
~50–70 credits
Build a staff-only inventory management system. Stack: React + Vite + TypeScript + Tailwind + shadcn/ui (already scaffolded), React Router, Supabase JS client against Lovable Cloud.

## Database schema (create one migration)

Create 5 tables with RLS enabled on ALL of them:

1. `products` — id (uuid pk default gen_random_uuid()), sku (text unique not null), name (text not null), description (text), reorder_point (int not null default 0, check reorder_point >= 0), unit (text default 'each'), image_path (text, Storage path), archived (bool default false), created_at (timestamptz default now()).

2. `locations` — id (uuid pk default gen_random_uuid()), name (text not null), address (text), created_at (timestamptz default now()).

3. `stock` — id (uuid pk default gen_random_uuid()), product_id (uuid not null references products(id) on delete cascade), location_id (uuid not null references locations(id) on delete cascade), qty (int not null default 0, CHECK (qty >= 0)), updated_at (timestamptz default now()). UNIQUE(product_id, location_id).

4. `movements` — id (uuid pk default gen_random_uuid()), product_id (uuid not null references products(id)), location_id (uuid not null references locations(id)), delta (int not null), reason (text not null, check reason in ('receipt','sale','transfer_in','transfer_out','adjustment','return','damage')), reference (text), actor_id (uuid references auth.users(id)), created_at (timestamptz default now()).

5. `low_stock_alerts` — id (uuid pk default gen_random_uuid()), product_id (uuid references products(id)), location_id (uuid references locations(id)), triggered_at (timestamptz default now()), resolved_at (timestamptz), qty_at_trigger (int), reorder_point_at_trigger (int).

Create a SECURITY DEFINER plpgsql function `has_role(check_role text) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER STABLE` that reads `(auth.jwt() -> 'app_metadata' ->> 'role')` and returns whether it matches. NEVER use SQL-language — it gets inlined.

Create the critical SECURITY DEFINER plpgsql function `record_movement(p_product_id uuid, p_location_id uuid, p_delta int, p_reason text, p_reference text DEFAULT NULL) RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS`:
```sql
BEGIN
  -- Insert the movement row
  INSERT INTO movements(product_id, location_id, delta, reason, reference, actor_id)
  VALUES (p_product_id, p_location_id, p_delta, p_reason, p_reference, auth.uid());

  -- Atomically UPSERT stock with row-level lock on existing row
  INSERT INTO stock(product_id, location_id, qty, updated_at)
  VALUES (p_product_id, p_location_id, p_delta, now())
  ON CONFLICT (product_id, location_id) DO UPDATE
    SET qty = stock.qty + p_delta,
        updated_at = now();
  -- The CHECK (qty >= 0) on the stock table rejects oversells at DB level

  -- Alert if newly below reorder_point and no open alert exists
  IF (SELECT qty FROM stock WHERE product_id = p_product_id AND location_id = p_location_id) <
     (SELECT reorder_point FROM products WHERE id = p_product_id)
  THEN
    IF NOT EXISTS (
      SELECT 1 FROM low_stock_alerts
      WHERE product_id = p_product_id AND location_id = p_location_id AND resolved_at IS NULL
    ) THEN
      INSERT INTO low_stock_alerts(product_id, location_id, qty_at_trigger, reorder_point_at_trigger)
      SELECT p_product_id, p_location_id,
             (SELECT qty FROM stock WHERE product_id = p_product_id AND location_id = p_location_id),
             (SELECT reorder_point FROM products WHERE id = p_product_id);
    END IF;
  ELSE
    -- Auto-resolve alert if back above reorder_point
    UPDATE low_stock_alerts
    SET resolved_at = now()
    WHERE product_id = p_product_id AND location_id = p_location_id AND resolved_at IS NULL;
  END IF;
END;
```

RLS policies:
- products: SELECT and UPDATE by has_role('staff') OR has_role('admin'); INSERT/DELETE by has_role('admin') only
- locations: SELECT by has_role('staff') OR has_role('admin'); INSERT/DELETE by has_role('admin')
- stock: SELECT by has_role('staff') OR has_role('admin'); NO INSERT/UPDATE/DELETE policies for authenticated users — all writes go through record_movement() RPC only
- movements: SELECT by has_role('staff') OR has_role('admin'); INSERT only via record_movement() function (no direct INSERT policy)
- low_stock_alerts: SELECT by has_role('staff') OR has_role('admin'); INSERT/UPDATE by service role only (handled inside record_movement)

Seed data: insert 5 products (different SKUs and units), 2 locations (e.g., 'Main Warehouse', 'Retail Store'), and opening stock for each product × location via calls to record_movement() with reason='receipt'.

## Layout

Create `src/layouts/AppLayout.tsx` — fixed left sidebar (220px) with nav links (Stock, Products, Locations, Movements, Alerts, Import, Settings) and a bottom user menu. Main area scrolls. Location filter pill in top-right header (defaults to 'All locations').

Create `src/components/StaffGuard.tsx` — calls supabase.auth.getUser(), reads app_metadata.role, shows a centered spinner while resolving, redirects to /unauthorized if role NOT IN ('admin','staff').

Create `src/components/AdminGuard.tsx` — same pattern but requires has_role('admin'); used for Products edit, Locations, Import, Settings.

## Pages and routes

Create these routes:
- `/stock` — current stock matrix grouped by product × location. QuantityBadge shows green if qty >= reorder_point, amber if 1 ≤ qty < reorder_point, red if qty = 0. Location filter pill at top filters the matrix. Sortable by product name or qty.
- `/products` — product catalog table (sku, name, unit, reorder_point, archived toggle). Admin-only edit via Sheet: name, sku, price (not stored — display only), reorder_point, unit, image upload (Storage bucket 'product-images', 40×40 thumbnail in table).
- `/locations` — location list + Add location form. Admin-only.
- `/movements` — chronological ledger with filter by product, location, reason, and date range. Each row shows: product name, location, delta (green positive / red negative), reason badge, reference, actor, timestamp.
- `/alerts` — open (resolved_at IS NULL) and resolved tabs. Each alert shows product, location, qty_at_trigger vs reorder_point. 'Mark resolved' button updates resolved_at directly (staff can resolve manually, even though the RPC auto-resolves).
- `/import` — CSV file picker, parse client-side with PapaParse, show preview table (CSVImportPreview with green valid / red error rows), Confirm button calls import-csv Edge Function (wired in follow-up #2). Admin-only.
- `/settings` — default location picker + alert recipient emails list. Admin-only.
- `/unauthorized` — one-line message with Sign out button.

## Components

Create:
- `src/components/QuantityBadge.tsx` — pill with emerald-100/emerald-700 (good), amber-100/amber-700 (low), red-100/red-700 (zero). Accepts qty and reorderPoint props.
- `src/components/MovementForm.tsx` — shadcn Dialog. Fields: product picker (combobox), location picker (Select), delta with +/- buttons and numeric input, reason dropdown (receipt/sale/adjustment/return/damage), reference text. On submit, calls `supabase.rpc('record_movement', {p_product_id, p_location_id, p_delta, p_reason, p_reference})`. Shows current qty for the selected product+location below the form.
- `src/components/StockTable.tsx` — shadcn Table in a 32px-row compact layout. Columns: SKU, Product Name, Location, Qty (rendered as QuantityBadge), Reorder Point, Last Movement date. Sortable headers.
- `src/components/CSVImportPreview.tsx` — renders parsed CSV rows as a table with validation error highlights (red row background + error column for invalid SKU, missing delta, invalid reason).
- `src/hooks/useRecordMovement.ts` — TanStack Query useMutation that calls record_movement RPC and invalidates the stock and movements queries on success.

## Styling

Light mode default with optional dark mode via shadcn ThemeProvider. Primary color: slate-900. Quantity badges as described above. 32px compact table rows. Product images as 40×40 square thumbnails with object-cover.

Generate the migration first (confirm it includes the full record_movement function SQL), then the layout + guards, then pages in the order listed above.

What this prompt generates

  • Creates a SQL migration with 5 tables, has_role() SECURITY DEFINER function, record_movement() SECURITY DEFINER RPC with atomic UPSERT + auto-alert logic, all RLS policies, and seed data
  • Scaffolds AppLayout with sidebar nav, location filter, StaffGuard, and AdminGuard
  • Generates 7 page components (Stock, Products, Locations, Movements, Alerts, Import, Settings) wired to React Router
  • Builds MovementForm with record_movement RPC call, StockTable with QuantityBadge color coding, and CSVImportPreview validation UI
  • Wires all stock mutations through the RPC — no direct client writes to the stock table are possible

Paste into: Lovable Build mode (the default chat at the bottom-left of the editor)

Expected output

What Lovable will generate after the starter prompt runs successfully.

Files
supabase/migrations/0001_inventory_schema.sql

5 tables + has_role() + record_movement() SECURITY DEFINER RPC + CHECK constraint + RLS policies + seed data

src/layouts/AppLayout.tsx

Sidebar nav with location filter pill and user menu

src/components/StaffGuard.tsx

Redirects non-staff to /unauthorized while session loads

src/components/AdminGuard.tsx

Redirects non-admin away from edit/import/settings routes

src/components/StockTable.tsx

Compact 32px-row table with sortable columns and QuantityBadge cells

src/components/MovementForm.tsx

Dialog for recording stock movements via record_movement RPC

src/components/QuantityBadge.tsx

Color-coded pill: green (good) / amber (low) / red (zero)

src/components/CSVImportPreview.tsx

Parsed CSV preview table with per-row validation errors

src/hooks/useRecordMovement.ts

TanStack Query mutation that calls record_movement RPC and invalidates stock + movements queries

src/pages/Stock.tsx

Stock matrix with location filter and QuantityBadge columns

src/pages/Products.tsx

Product catalog with admin edit Sheet and image upload

src/pages/Locations.tsx

Location list and add form (admin only)

src/pages/Movements.tsx

Chronological ledger with multi-filter controls

src/pages/Alerts.tsx

Open and resolved low-stock alerts with manual resolve button

src/pages/Import.tsx

CSV file picker + CSVImportPreview + Confirm import (admin only)

src/pages/Settings.tsx

Default location + alert recipients (admin only)

Routes
/stock

Current stock levels matrix with location filter and color-coded badges

/products

Product catalog with admin CRUD sheet

/locations

Location list and add form

/movements

Chronological ledger with filters

/alerts

Low-stock alerts with manual resolve

/import

CSV import with dry-run preview

/settings

Default location and alert recipients

/unauthorized

Fallback for non-staff users

DB Tables
products
ColumnType
iduuid primary key default gen_random_uuid()
skutext unique not null
nametext not null
reorder_pointint not null default 0
unittext default 'each'
image_pathtext
archivedbool default false

RLS: SELECT by staff/admin; INSERT/UPDATE/DELETE by admin only

locations
ColumnType
iduuid primary key default gen_random_uuid()
nametext not null
addresstext

RLS: SELECT by staff/admin; INSERT/DELETE by admin only

stock
ColumnType
iduuid primary key default gen_random_uuid()
product_iduuid not null references products(id) on delete cascade
location_iduuid not null references locations(id) on delete cascade
qtyint not null default 0
updated_attimestamptz default now()

RLS: SELECT by staff/admin; NO INSERT/UPDATE/DELETE policies for authenticated users — writes ONLY through record_movement() SECURITY DEFINER RPC

movements
ColumnType
iduuid primary key default gen_random_uuid()
product_iduuid not null references products(id)
location_iduuid not null references locations(id)
deltaint not null
reasontext not null
referencetext
actor_iduuid references auth.users(id)

RLS: SELECT by staff/admin; INSERT only via record_movement() function — no direct INSERT policy for authenticated users

low_stock_alerts
ColumnType
iduuid primary key default gen_random_uuid()
product_iduuid references products(id)
location_iduuid references locations(id)
triggered_attimestamptz default now()
resolved_attimestamptz
qty_at_triggerint
reorder_point_at_triggerint

RLS: SELECT by staff/admin; INSERT/UPDATE by service role only (record_movement handles this internally)

Components
StaffGuard

Session-aware role check — spinner while loading, redirects non-staff

AdminGuard

Requires admin role for edit/import/settings routes

StockTable

Compact sortable table with QuantityBadge cells and location filter

MovementForm

Dialog that records stock movements via record_movement RPC

QuantityBadge

Color-coded pill based on qty vs reorder_point threshold

CSVImportPreview

Client-side CSV parse with per-row validation before Edge Function submit

Follow-up prompts

Paste these into Agent Mode one by one, in order, after the starter prompt finishes.

1

Wire low-stock email alerts via daily cron

Daily 06:00 UTC cron email listing all open low-stock alerts to the configured buyer email

~50 credits
prompt
Create a Supabase Edge Function at `supabase/functions/check-low-stock/index.ts` (Deno). It should:
1. Query `low_stock_alerts` WHERE resolved_at IS NULL, joined to products (sku, name, reorder_point) and locations (name).
2. Group alerts by buyer email (read from a `settings` table or a hardcoded ALERT_RECIPIENT_EMAIL secret from Deno.env.get).
3. For each recipient, send one Resend email (RESEND_API_KEY from Deno.env.get) with a table of SKU, product name, current qty, reorder point, and location name. Subject: 'Low stock alert — X items need reordering'.
4. Return a JSON summary of how many emails were sent.

Schedule it via pg_cron: in a new SQL migration, run `SELECT cron.schedule('check-low-stock-daily', '0 6 * * *', $$SELECT net.http_post(url:='<EDGE_FUNCTION_URL>/check-low-stock', headers:='{"Authorization": "Bearer " || current_setting(''app.service_role_key'')}'::jsonb)$$);`. Document that the URL must be replaced with the actual function URL from Cloud tab → Edge Functions after deploy.

When to use: After 3 or more low-stock alerts have triggered in testing

2

Add CSV import for products and opening stock

Transactional CSV import via Edge Function with per-row validation and full rollback on any error

~70 credits
prompt
Create a Supabase Edge Function at `supabase/functions/import-csv/index.ts` (Deno). It should:
1. Accept a multipart/form-data POST with a CSV file field.
2. Parse the CSV with the csv-parse npm module (import via esm.sh). Expected columns: sku, name, unit, reorder_point, location_name, qty (opening stock).
3. For each row, validate: sku is present and non-empty, qty is a non-negative integer, location_name matches an existing location. Collect all errors into a per-row errors array.
4. If any row has errors, return 422 with the full per-row errors array — do not import any rows.
5. If all rows are valid, call record_movement() for each row (reason='receipt', reference='csv_import_${timestamp}') inside a transaction using supabase_functions.batch RPC pattern, wrapping all calls in a BEGIN/COMMIT block. If any single call fails, roll back ALL rows and return the error.

In the /import page, after CSVImportPreview shows green for all rows, the Confirm button POSTs to import-csv. Show a loading state during upload. On success, show 'X rows imported' toast. On error, re-render CSVImportPreview with the server-returned per-row errors.

When to use: When migrating from a spreadsheet — typically the very first action after the app is built

3

Add stock transfer between locations

Atomic transfer between locations that inserts paired transfer_out/transfer_in movements in a single transaction

~50 credits
prompt
Extend record_movement() to support transfers, or create a new SECURITY DEFINER function `record_transfer(p_product_id uuid, p_from_location uuid, p_to_location uuid, p_qty int, p_reference text DEFAULT NULL) RETURNS void` that:
1. Inserts a movements row for the from-location with delta = -p_qty and reason='transfer_out'.
2. Inserts a movements row for the to-location with delta = +p_qty and reason='transfer_in'.
3. UPSERTs both stock rows atomically in a single BEGIN/COMMIT block.
4. Checks the from-location stock has enough qty before decrementing (raise exception 'Insufficient stock' if not).

Add a 'Transfer stock' button to the /stock page that opens a shadcn Dialog (TransferForm) with: product picker, from-location selector, to-location selector, qty input (max = current qty at from-location), and optional reference field. On submit, calls `supabase.rpc('record_transfer', {...})`. Invalidate stock and movements queries on success.

When to use: When you operate from 2 or more locations and need to move stock between them

4

Add barcode scan via device camera

Phone-as-scanner for warehouse receiving — scan barcode, tap +, confirm receipt in 3 taps

~60 credits
prompt
Install @zxing/library for QR/barcode scanning. Add a new /movements/quick-receipt route with a BarcodeScanPage component that:
1. Renders a ZXing BarcodeReader component targeting the device camera. On scan, receives the decoded text (the SKU barcode value).
2. Fetches the product matching that SKU from Supabase.
3. If found, pre-fills a compact MovementForm with reason='receipt', delta=1, the scanned product, and the default location from settings.
4. Shows a +/- quantity button (tap + to increment before confirming) and a large 'Confirm Receipt' button.
5. On confirm, calls record_movement RPC and shows a success toast ('1 unit of Product X received'). Resets the scanner for the next scan.

Add a 'Scan' button to the /movements page that navigates to /movements/quick-receipt. On desktop, show a text input fallback since camera may not be available.

When to use: When you want to use a phone or tablet for receiving goods without typing SKUs

5

Add stock report CSV export

CSV stock export via Edge Function with current location filter applied, for monthly accounting

~30 credits
prompt
Create a Supabase Edge Function at `supabase/functions/export-stock-report/index.ts` (Deno). It should:
1. Accept GET with optional query params: location_id (filter), include_archived (bool).
2. Query stock joined to products and locations. Include columns: sku, product_name, unit, location_name, qty, reorder_point, last_movement_at (from the latest movements row for that product+location).
3. Return a text/csv response with those columns and Content-Disposition: attachment; filename="stock-report-{date}.csv".

Add an 'Export CSV' button on the /stock page (top-right, shadcn Button variant outline). On click, fetch the edge function URL with current location filter, trigger a browser download via anchor with download attribute. Show loading state on the button while the function runs.

When to use: When you need to send current stock levels to an accountant or submit to a buyer

Common errors

Real error strings you'll see. Find yours, paste the fix prompt.

new row violates check constraint "stock_qty_check"

Someone attempted to sell or adjust stock below zero. The CHECK (qty >= 0) constraint on the stock table correctly blocked the oversell. This is the system working as designed — your UI did not prevent submitting a negative-delta that exceeds current stock.

Fix — paste into Lovable Agent Mode
In MovementForm, before submitting a negative delta, query current stock qty for the selected product+location: `const { data } = await supabase.from('stock').select('qty').eq('product_id', productId).eq('location_id', locationId).single();`. If `(data.qty + delta) < 0`, disable the Confirm button and show 'Only X units in stock at this location'. Do NOT remove the DB constraint — it is the last line of defense against concurrent sells that both pass the client check.
permission denied for table stock (when trying to add a receipt)

Your MovementForm is calling supabase.from('stock').upsert(...) directly. The stock table intentionally has no INSERT/UPDATE policy for authenticated users — all writes must go through the record_movement() RPC.

Fix — paste into Lovable Agent Mode
Replace all direct `supabase.from('stock').*` write calls with `supabase.rpc('record_movement', {p_product_id, p_location_id, p_delta, p_reason, p_reference})`. The RPC is SECURITY DEFINER so it bypasses RLS internally while still being callable only by authenticated users. Check the MovementForm's submit handler and the useRecordMovement hook — both must use rpc(), not from().
Two users sell the last unit at the same time — one gets a 500 error

Without the atomic record_movement() RPC, both clients read qty=1, compute new qty=0, and call UPDATE simultaneously. One succeeds; the other hits the CHECK constraint and gets a 42P01 error that surfaces as a 500 in the UI.

Fix — paste into Lovable Agent Mode
This is correct behavior — the constraint correctly rejected the duplicate sell. Surface the error cleanly: in useRecordMovement's onError handler, check if the PostgreSQL error code is '23514' (check_violation) and show a user-friendly toast: 'Out of stock — this item was sold by someone else at the same moment'. Do not remove the DB constraint.
CSV import partially succeeds and leaves stock in an inconsistent state

The import-csv Edge Function processed rows sequentially without a transaction wrapper. Rows 1–49 inserted movements successfully; row 50 failed (invalid SKU), but rows 1–49 were already committed.

Fix — paste into Lovable Agent Mode
In import-csv Edge Function, wrap the full import loop in a Supabase RPC that handles all rows as a batch with a BEGIN/COMMIT block and RAISE EXCEPTION on any per-row error. Create a `import_movements_batch(p_rows jsonb) RETURNS jsonb` SECURITY DEFINER function that iterates the JSON array, calls record_movement for each row inside a single transaction, and raises an exception with a per-row error report if any row fails. If any row fails, Postgres rolls back all preceding inserts automatically.
Low-stock alerts fire for the same SKU every time the daily cron runs

The check-low-stock Edge Function is inserting a new low_stock_alerts row on every run instead of checking for an existing unresolved alert for the same product+location.

Fix — paste into Lovable Agent Mode
This means the record_movement() function's de-duplication check isn't working. Verify the function includes: `IF NOT EXISTS (SELECT 1 FROM low_stock_alerts WHERE product_id = p_product_id AND location_id = p_location_id AND resolved_at IS NULL) THEN INSERT ...`. If the function was generated without this check, add a new migration that replaces record_movement() with the corrected version from the starter prompt.

Cost reality

What this build actually costs — no surprises on your card.

Recommended Lovable plan

Pro $25/mo recommended — the record_movement RPC and CHECK constraint pattern usually work on the first try; most iteration credits go into UI polish (StockTable column sort, location filter, badge styling). The full starter typically fits within the Pro allowance.

Monthly run cost breakdown

~100–180 credits estimated (starter ~50–70, follow-ups 1–3 ~170 if all done; most ship after follow-up #3 with CSV import and transfers). Note: this is a cousin-substitution category with no exactly documented Lovable inventory build — estimates are extrapolated from admin-panel and Edge Function patterns. total
ItemCost
Lovable Pro

Can drop to Free after launch — the app continues to run on Cloud

$25/mo while iterating
Supabase / Lovable Cloud

500MB DB handles ~5M movements rows; internal-tool traffic is light

$0/mo at MVP scale
Resend

Daily low-stock alert digests — 3K/mo is generous for an internal alert tool

$0/mo up to 3K emails
Custom domain

Point inventory.yourcompany.com at the published app

~$10–15/yr

Scaling notes: The 500MB DB Free cap allows roughly 5 million movements rows. A busy warehouse writing 20 movements/day per 100 SKUs generates about 700K rows/year — the Free tier covers 7+ years before you need Supabase Pro $25/mo. The more likely ceiling is the Edge Function timeout (60s default) for CSV imports of more than 10K rows — chunk those into batches of ~500 rows per RPC call using the batched import pattern from follow-up #2.

Production checklist

Steps to take before you share the URL with real users.

Security

  • Verify the stock table has no INSERT/UPDATE client policies

    Cloud tab → Database → Table Editor → stock → Policies. Confirm there are only SELECT policies for authenticated users and NO INSERT, UPDATE, or DELETE policies. All writes must go through record_movement(). If any write policy exists, drop it.

  • Set staff/admin role in app_metadata for every team member

    Cloud tab → Users & Auth → click each user row → Edit → set app_metadata to {"role": "staff"} or {"role": "admin"}. Admin users get access to Products edit, Locations, Import, and Settings.

Domain & SSL

  • Point a custom subdomain at the published app

    Publish → top-right toolbar → Settings → Custom domain. Add a CNAME record at your DNS provider pointing inventory.yourcompany.com to your lovable.app URL. SSL is auto-provisioned.

Monitoring

  • Confirm the daily cron and Edge Function run successfully

    Cloud tab → Edge Functions → check-low-stock → Logs. Verify one run per day at ~06:00 UTC with a 200 status. If it fails, check that RESEND_API_KEY is set in Cloud tab → Secrets and that the pg_cron schedule URL was updated to the actual Edge Function URL.

Backups

  • Confirm daily backups are running before going live with real inventory

    Cloud tab → Database → Backups. Verify the most recent backup is from today. Export a manual backup before any bulk CSV import to ensure you can restore if the import goes wrong.

Frequently asked questions

Why store a qty column separately from movements instead of always computing SUM(delta)?

Computing SUM(delta) for every product+location on every page load requires scanning the full movements table — fine at 1,000 rows, very slow at 1 million. The stock table with a materialized qty is your fast read path: O(1) lookup for current stock. The movements table is your audit trail and source of truth for reconciliation. The record_movement() RPC keeps both in sync atomically — you never compute from movements in the UI, you just display stock.qty.

How do I prevent negative stock when two users sell the same SKU simultaneously?

The CHECK (qty >= 0) constraint on the stock table is your primary defense. When two concurrent sells of the last unit both reach record_movement(), the PostgreSQL row-level lock inside the UPSERT serializes them: one succeeds and the other hits the CHECK constraint with a 23514 error. Catch that error code in your UI and show 'Out of stock — this item was just sold by another team member'. Do not remove the constraint and do not rely solely on client-side checks.

Can I use a barcode scanner from my phone?

Yes — follow-up #4 adds a /movements/quick-receipt route using @zxing/library that activates your phone's camera, decodes QR or 1D barcodes, looks up the SKU, and pre-fills a receipt form. Open the published app (not the preview) from your phone browser — the camera permission dialog appears on first use. On desktop, a text input fallback lets you type or paste SKU codes. No app install required.

How do I import my existing inventory from a spreadsheet?

Follow-up #2 adds a CSV import route. Export your spreadsheet as CSV with columns: sku, name, unit, reorder_point, location_name, qty. Go to /import, drop in the file, review the per-row validation preview (green = valid, red = error with explanation), then click Confirm. The Edge Function validates all rows first, and only imports if every row passes — if row 47 fails, nothing is imported and you get a specific error for that row to fix.

What if I have stock across 5 or more warehouses?

The schema handles unlimited locations from day one — the stock table is keyed by (product_id, location_id), so you can add a third, fourth, or tenth location from /locations and immediately record movements at that location. The /stock page's location filter pill lets you scope the view to a single location. The transfer follow-up (follow-up #3) adds an atomic transfer flow between any two locations.

Should I sync this with Shopify or Square?

Follow-up #6 (in the brief) outlines a Shopify sync via the native Cloud App connector: an Edge Function polls Shopify inventory and reconciles each SKU via record_movement() with reason='adjustment'. This is appropriate when your online store sells the same physical SKUs as your Lovable system. For Square (POS), there's no native connector — you'd use the Square REST API from an Edge Function on a schedule. Both sync patterns work, but expect to spend 80–100 credits on the integration and ongoing maintenance when Shopify/Square change API formats.

When should I upgrade to a real WMS like NetSuite or Fishbowl?

Upgrade when you need WMS-grade capabilities: pick paths, wave picking, multi-warehouse replenishment, lot/serial number tracking for FDA/ISO compliance, EDI integration with suppliers, or real-time POS integration across many terminals. At that point you're past $100K/year in inventory complexity and Lovable's 60-second Edge Function timeout and 500MB DB become real constraints. If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.

Need a production-grade version?

RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

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.