Build a real Hardware POS in one afternoon.
Follow this crash course and ship HardwarePOS — a point-of-sale system a hardware shop in Kampala could use today. Inventory, sales, payment methods, downloadable receipts, deployed to a custom domain. Powered by VibeKit + your favorite AI coding agent.
HardwarePOS — a real shop POS.
Not a tutorial toy. Real auth, real database, real transactions, real receipts. A hardware shop owner could install this Monday morning and start using it.
Features you'll ship
- Email + Google OAuth sign-in (Better Auth)
- Inventory: products with SKU, price (UGX), stock, category
- Six seeded categories: Tools, Hardware, Paint, Plumbing, Electrical, Other
- Low-stock alerts (configurable per product)
- POS sale screen: product search, cart, live total
- Three payment methods: Cash, Mobile Money, Card
- Optional customer name + phone capture
- Receipt PDF download (@react-pdf/renderer)
- Sales history with date-range filtering
- Dashboard: today's revenue, top products, weekly chart
- Light + dark mode
- Deployed to Vercel + custom domain
Skills you'll learn
- Planning a real product with Claude before any code
- Reading a phase-by-phase build plan
- Modeling transactional data in Prisma v7 (Sale + SaleItem pattern)
- Wiring auth-guarded API routes with Zod validation
- Atomic stock decrements inside Prisma transactions
- Building a fast product search with React Query
- Generating styled PDFs with @react-pdf/renderer
- Aggregating data with groupBy for dashboards
- Currency formatting (UGX, no decimals, comma-separated)
- Running a senior-level pre-deploy audit
- Deploying with Vercel + Cloudflare DNS + SSL
The full path.
Click any module to jump in. They build on each other — follow them in order on your first run.
- MODULE 015 min
Set up the accounts you'll need
- MODULE 0215 min
Plan with Claude (claude.ai)
- MODULE 0310 min
Initialize the project
- MODULE 0430 min
Phase 1 — Foundation
- MODULE 0530 min
Phase 2 — Products & Inventory
- MODULE 0645 min
Phase 3 — POS Sale flow + Receipt PDF
- MODULE 0725 min
Phase 4 — Dashboard + Sales History
- MODULE 0840 min
Pre-deploy review + Deploy
Check your environment first
Before signing up for accounts, make sure your machine has the four tools VibeKit needs: Node 20+, pnpm 9+, git, and gh CLI. The fastest way to check is to paste the OS-specific prompt at vibekit.desishub.com/setup into your AI coding agent — it scans your machine, reports what's installed, and gives you one-line install commands for anything missing. Without installing anything itself.
If you already have all four installed, skip to Module 01.
Set up the accounts you'll need
All of these have free tiers that cover the entire course. Sign up first so you don't break flow later.
- Anthropic Claude (chat)Free tier works for the planning step
- Claude Code or CursorPick whichever AI coding agent you prefer
- NeonPostgres database — free tier
- VercelDeployment — free hobby tier
- ResendTransactional email — free tier
- GitHub accountFor pushing your code + Vercel auto-deploy
Local tools
- Node.js 20+ (or 22+) —
node -vto check - pnpm —
npm i -g pnpmif missing - git — already installed on most systems
Plan with Claude (claude.ai)
VibeKit's planning step turns a one-line idea into 4 production-ready files. You paste a prompt, answer questions, and Claude does the rest.
Step 1 — Open the planning prompt
Go to /docs/quickstart and copy CLAUDE_PROMPT.md from the first code block (or grab it directly from the repo).
Step 2 — Open Claude
Go to claude.ai/new. Paste the entire CLAUDE_PROMPT.md as your first message. Then on a new line, paste the HardwarePOS brief:
Append after CLAUDE_PROMPT.md contentHardwarePOS briefI want to build HardwarePOS — a point-of-sale system for a small hardware shop in
Uganda. The shop owner uses it to ring up sales of items like nails, paint,
plumbing fittings, electrical supplies, and hand tools. Single user (the shop
owner / cashier) — no team features, no customer-facing storefront, no online
ordering. Strictly in-shop POS.
Core flows:
1. POS Sale (the main screen): search products by name or SKU, add to cart,
adjust quantities, see live total. Choose payment method (Cash / Mobile Money /
Card). Capture optional customer name and phone. Complete sale, then download
a receipt PDF.
2. Inventory: list products with name, SKU, category, price (UGX), and stock
quantity. Add new products, edit price/stock, delete. Low-stock alerts when
stock falls below a configurable threshold per product.
3. Sales history: list of past sales with date, total, payment method, items
count, customer (if captured). Filter by date range and payment method. View
a single sale's full line items. Export the day's sales to a PDF report.
4. Dashboard: today's sales total + transaction count, top 5 products this week,
low-stock alert count, weekly revenue chart (last 7 days).
Seed the database with these categories on first run: Tools, Hardware, Paint,
Plumbing, Electrical, Other.
No image uploads — text-only products (name + SKU + category is enough).
No e-commerce / cart abandonment / online ordering / customer accounts.
Currency: UGX (Ugandan Shillings) with comma-separated formatting and no decimals
(e.g., 25,000 not 25,000.00).
Single user, single device. Light + dark mode. Aesthetic: clean dashboard like
Linear or Vercel — bold large numbers so the cashier can read totals at a glance.
Brand color: indigo (#4F46E5).Step 3 — Paste a Dribbble reference (mandatory)
Claude will now ask for a UI reference image. This is non-skippable — even though the HardwarePOS brief is detailed, words like "clean dashboard" are too vague to design from. A real Dribbble shot tells Claude exactly which color palette, font weight, card style, and button shape to match.
- Claude will suggest 2–3 search terms (e.g.
"pos dashboard ui","retail point of sale"). - Open dribbble.com/search and search one of them.
- Pick a shot whose aesthetic you'd want HardwarePOS to match. Open the shot in full size.
- Right-click → Copy Image, then paste it into the Claude chat. (Don't paste a Dribbble URL — paste the image itself.)
- Claude will analyze it and echo back the palette + typography + card spec it extracted. Confirm or correct.
Step 4 — Wait for Claude's confirmation summary
The HardwarePOS brief above is detailed, so Claude will probably skip the interview. Instead, it will write a structured summary like this:
## What I understood
App: HardwarePOS — a single-user POS for a hardware shop in Uganda
Primary user: the shop owner / cashier
Core features:
- POS sale flow (search → cart → payment → receipt)
- Inventory with low-stock alerts
- Sales history with date filters
- Dashboard with daily revenue + top products + weekly chart
Data model: Category, Product, Sale, SaleItem (with snapshot fields)
Integrations: Better Auth (email + Google OAuth), Resend (auth emails only),
Payments: NONE (payment method is just a captured label),
File uploads: NONE, Dark mode: Yes
Visual design: indigo (#4F46E5), Geist + JetBrains Mono,
"fast, focused, large numbers", Linear/Vercel inspiration
Out of scope (v1): online ordering, customer accounts, multi-cashier roles
Does this match your intent? Reply 'Yes, generate the files' to proceed,
or tell me what to adjust.If anything's wrong, correct it now (e.g., "Add Excel export to the sales history"). Otherwise, reply:
Yes, generate the filesStep 5 — Download the 4 files
Claude will produce 4 downloadable Artifacts (one per file) in the right-side panel. Each has a download icon — click it to save the file.
terminalCreate the project folder firstmkdir hardware-pos && cd hardware-pos
# Then drop the 4 downloaded files into this folder:
# - project-description.md
# - project-phases.md
# - design-style-guide.md
# - prompt.mdhardware-pos/, hit enter. Done.Initialize the project
Scaffold the Next.js 16 project, copy the framework's coding constitution into it, open your coding agent.
Step 1 — Scaffold Next.js
terminalFrom inside the hardware-pos folderpnpm create next-app@latest . --typescript --tailwind --app --eslint --import-alias "@/*" --turbopack --no-src-dirAccept the prompts. When it finishes, you have a base Next.js 16 project.
Step 2 — Copy the framework files
Clone the VibeKit repo to grab the framework files:
terminalOne-time clone (delete after copying)git clone https://github.com/MUKE-coder/vibekit.git /tmp/vibekit
cp /tmp/vibekit/master_prompt.md ./master_prompt.md
cp /tmp/vibekit/jb-components.md ./jb-components.md
cp /tmp/vibekit/pre-deploy-review.md ./pre-deploy-review.mdStep 3 — Install the VibeKit rules for your AI agent
VibeKit ships rules for every major AI coding agent. Pick your agent below and run the one-line install. The rules auto-load whenever you open the agent in this project — no need to paste long prompts every session.
terminalProject-local install (recommended)mkdir -p .claude/skills/vibekit
curl -fsSL https://raw.githubusercontent.com/MUKE-coder/vibekit/main/skill/SKILL.md \
-o .claude/skills/vibekit/SKILL.mdRestart Claude Code. Type /vibekit to invoke, or it auto-loads when framework files are detected.
Using a different agent (Continue, Cody, Junie, etc.)? See skill/README.md for the full install table — same one-line curl, just a different filename.
Step 4 — Verify your project root
You should now have these 7 files in your project root:
ls -laExpected fileshardware-pos/
├── master_prompt.md # framework — coding rules
├── jb-components.md # framework — component registry
├── pre-deploy-review.md # framework — security audit prompt
├── project-description.md # generated by Claude
├── project-phases.md # generated by Claude
├── design-style-guide.md # generated by Claude
├── prompt.md # generated by Claude — paste this next
│
# ONE of these from Step 3 (depending on your agent):
├── .claude/skills/vibekit/SKILL.md # Claude Code
├── .cursor/rules/vibekit.mdc # Cursor
├── AGENTS.md # Codex CLI / universal
├── .clinerules # Cline
├── .windsurfrules # Windsurf
├── GEMINI.md # Gemini CLI
└── package.json + Next.js scaffoldStep 5 — Open in your coding agent
Open the hardware-pos folder in Claude Code (claude in the project terminal), Cursor, Cline, or whichever agent you chose.
Phase 1 — Foundation
First big build moment. Your agent reads all 7 files, then executes Phase 1: Prisma + Neon, Better Auth, layout shell, design tokens, custom 404/error pages.
Step 1 — Get a Neon database URL
- Go to console.neon.tech and create a new project.
- Copy the connection string (starts with
postgres://). - Use the direct (non-pooled) connection — Prisma migrations require it.
Step 2 — Paste the build prompt
In your coding agent, paste the entire contents of prompt.md as your first message. The agent will:
- Read
master_prompt.md,design-style-guide.md,jb-components.md,project-description.md,project-phases.md - Execute Phase 1 tasks
- Stop after Phase 1 for your confirmation
Step 3 — Provide secrets when asked
The agent creates .env.local and asks for values. Provide:
.env.localPhase 1 minimum env varsDATABASE_URL="postgres://USER:PASS@ep-xxx.neon.tech/neondb?sslmode=require"
BETTER_AUTH_SECRET="<run: openssl rand -base64 32>"
BETTER_AUTH_URL="http://localhost:3000"
# Optional but recommended
RESEND_API_KEY=""
RESEND_FROM_EMAIL="onboarding@resend.dev"
# For Google OAuth
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""Step 4 — Push the schema + start dev
terminalPhase 1 verificationpnpm db:push
pnpm db:generate
pnpm devOpen http://localhost:3000 and verify:
- Landing page redirects unauthenticated users to /auth/sign-in
- Sign up with email or Google works
- You land on /dashboard (currently empty)
- Sidebar layout with theme toggle (light/dark)
Phase 2 — Products & Inventory
Build the inventory side first — the cashier needs products to exist before they can sell. Categories + Products with full CRUD, plus low-stock badges.
Step 1 — Confirm Phase 1 done, start Phase 2
Phase 1 is verified working. Proceed to Phase 2 — Products & Inventory.
Build:
- Category and Product Prisma models (schema below)
- A seed script that inserts the 6 categories on first run: Tools, Hardware, Paint, Plumbing, Electrical, Other
- API routes /api/categories and /api/products with auth guards + Zod validation
- /inventory page using JB Data Table to list products with columns: SKU, Name, Category, Price (UGX), Stock, Status (low-stock badge)
- /inventory/new and /inventory/[id]/edit pages with React Hook Form + Zod
- A formatUGX(amount: number) utility in lib/format.ts that returns "UGX 25,000" style strings (no decimals, comma separators)
Stop after Phase 2 is complete and ask me to verify.Step 2 — Verify the schema
The agent should produce a Prisma schema like this. If it differs, ask for these exact fields:
prisma/schema.prismaCategories + Productsmodel Category {
id String @id @default(cuid())
name String @unique
color String @default("#4F46E5")
products Product[]
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model Product {
id String @id @default(cuid())
sku String
name String
priceUgx Int // store as integer, no decimals
stockQuantity Int @default(0)
lowStockThreshold Int @default(5)
categoryId String
category Category @relation(fields: [categoryId], references: [id])
saleItems SaleItem[]
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, sku]) // SKUs unique per user
@@index([userId])
@@index([categoryId])
}Step 3 — Test inventory CRUD
- Verify the 6 categories appear in the sidebar / category dropdown
- Add 8–10 products across categories (e.g.
NAIL-3IN3-inch nails 800 UGX stock 200;PAINT-WHT-4LWhite paint 4L 45,000 UGX stock 12) - Edit a product's stock down to 2 — confirm the low-stock badge appears
- Delete a product — confirm it's removed
- Search by SKU and by name — both should work in the data table
formatUGX() utility handles display. Tell your agent if it tries to use Decimal/Float for currency — that's a foot-gun.Phase 3 — POS Sale flow + Receipt PDF
The core of the app. Cashier searches a product, adds to cart, sets quantity, picks payment, completes sale. Stock decrements atomically. PDF receipt downloads immediately.
Step 1 — Add the Sale + SaleItem schema
Phase 2 is verified. Proceed to Phase 3 — POS Sale flow.
Add to the Prisma schema:
model Sale {
id String @id @default(cuid())
totalUgx Int
paymentMethod PaymentMethod
customerName String?
customerPhone String?
items SaleItem[]
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([userId, createdAt])
}
model SaleItem {
id String @id @default(cuid())
saleId String
sale Sale @relation(fields: [saleId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id])
productName String // snapshot of product name at sale time
productSku String // snapshot of SKU at sale time
unitPriceUgx Int // snapshot of price at sale time
quantity Int
lineTotalUgx Int
@@index([saleId])
@@index([productId])
}
enum PaymentMethod { CASH MOBILE_MONEY CARD }
Then run pnpm db:push and pnpm db:generate.Step 2 — Build the POS screen
Build /pos as the main POS screen with this layout:
LEFT (60%) — Product search + grid:
- Search input at top (search by SKU or product name)
- Product grid below: each card shows SKU, name, price (UGX), stock. Click adds 1 to cart (or increments quantity if already in cart). Out-of-stock products are visually disabled.
- Use React Query for product data with staleTime 30000.
RIGHT (40%) — Cart:
- "New Sale" header
- List of cart items: product name, qty controls (+/-), unit price, line total, remove button
- Optional customer name + phone fields
- Payment method select: Cash / Mobile Money / Card
- Live total in big numbers (32px font, accent color)
- "Complete Sale" button (disabled when cart is empty)
API:
POST /api/sales accepts { items: [{ productId, quantity }], paymentMethod, customerName?, customerPhone? } and:
1. Validates with Zod
2. Wraps everything in db.$transaction:
a. Reads each product (FOR UPDATE not needed — Prisma handles this with the txn)
b. Verifies stock >= quantity for each item; throws 400 if not
c. Creates the Sale with snapshot SaleItems (copy productName, sku, unitPriceUgx)
d. Decrements product stockQuantity by quantity for each item
3. Returns the new sale id
After successful sale, redirect to /sales/[id] which shows the sale detail + a "Download Receipt" button.
Use the existing JB Searchable Select for the payment method dropdown.Step 3 — Receipt PDF with @react-pdf/renderer
Build /api/sales/[id]/receipt that returns a PDF response using @react-pdf/renderer.
The receipt should look like a real till receipt:
- Centered header: shop name placeholder ("HARDWARE POS"), "RECEIPT" subtitle, date/time
- Sale ID (last 8 chars), payment method, optional customer name/phone
- Line items table: SKU | Item | Qty | Unit Price | Line Total
- Total row in bold
- Footer: "Thank you for your purchase"
- Use Helvetica or the closest equivalent in @react-pdf — small fonts (10–11pt)
- A4 portrait page size
- Currency formatted via formatUGX()
Wire the "Download Receipt" button on the sale detail page to fetch this endpoint and download the file as receipt-<saleId>.pdf.Step 4 — Test a real sale end-to-end
- Open /pos, search "nail", add to cart
- Adjust quantity to 5, search "paint", add to cart
- Verify the live total matches manually (5 × nail price + 1 × paint price)
- Type a customer name and phone
- Pick "Mobile Money" as payment method
- Click Complete Sale → you land on /sales/[id]
- Download the receipt PDF and open it
- Go to /inventory — verify stock decremented for both products
Phase 4 — Dashboard + Sales History
With sales flowing, the dashboard becomes useful. Today's revenue, top products, low-stock counter, weekly chart. Plus the full sales history.
Step 1 — Dashboard analytics
Build /dashboard with these sections:
STAT CARDS ROW (4 cards across):
- Today's Revenue (UGX) — sum of Sale.totalUgx where DATE(createdAt) = today
- Today's Transactions — count of sales today
- Low-Stock Alerts — count of products where stockQuantity <= lowStockThreshold
- This Week's Revenue — sum of Sale.totalUgx where createdAt >= start of current ISO week
Each card: label (uppercase mono 11px), big number (28px semibold), small comparison vs yesterday/last week (12px secondary).
WEEKLY REVENUE CHART:
- Bar chart of daily revenue for the last 7 days
- Use a simple SVG bar chart in a custom component (no external chart library needed for 7 bars)
- Labels: day name (Mon, Tue, ...). Y-axis: UGX values formatted with formatUGX.
- Bar color: var(--accent), with bg-muted fill for the active day's bar
TOP 5 PRODUCTS THIS WEEK:
- Aggregate SaleItem rows where Sale.createdAt >= start of week
- groupBy productId, sum quantity, sum lineTotalUgx
- Display as a table: rank, name, units sold, revenue
- Order by units sold descending
LOW-STOCK LIST:
- List of products where stockQuantity <= lowStockThreshold
- Show name, SKU, current stock, threshold
- Link to /inventory/[id]/edit
All queries should be in API routes, scoped to session.user.id, served via React Query.Step 2 — Sales history page
Build /sales — full sales history list:
- JB Data Table with columns: Date (formatted "Jan 15, 2024 14:30"), Sale ID (last 8 chars), Items count, Payment Method (badge), Total (UGX, right-aligned monospace), Customer (if any), Action (View)
- Server-side pagination via /api/sales (page, limit, default 20)
- Filters above the table:
- Date range picker (default: last 30 days)
- Payment method dropdown (All / Cash / Mobile Money / Card)
- Filter state lives in URL query params so refresh preserves it
- "Export today's sales as PDF" button at the top — generates a daily report PDF with a header summary + sales table
Clicking a row goes to /sales/[id] (already exists from Phase 3) showing full line items and the receipt download button.Step 3 — Test the analytics
- Make 3–5 sales of varying amounts and payment methods
- Refresh /dashboard — verify the 4 stat cards reflect those sales
- Check the weekly revenue chart — today's bar should be highlighted
- Verify the top-5 products list orders correctly
- Drop a product's stock to below threshold — verify it appears in low-stock list
- On /sales, filter to "Mobile Money only" — verify the URL updates and the table filters
- Export today's sales as PDF — open the file, verify totals match the dashboard
Pre-deploy review + Deploy
The two highest-leverage steps: catch security holes before launch, then ship.
Step 1 — Run the pre-deploy audit
Open Step 7 of the quickstart (or open pre-deploy-review.md in your project root). Paste the entire prompt into your coding agent.
The agent writes findings to pre-deploy-review-report.md. Expected for HardwarePOS:
- Critical — missing rate limiting on auth, possibly missing transaction wrapping on the sale endpoint (this would let stock go negative under concurrent sales)
- High — missing index on Sale.createdAt for the dashboard date queries, N+1 on the sales history when fetching items count
- Medium — missing Zod refinements (e.g. quantity must be > 0), verbose console.log on success paths
Step 2 — Fix every Critical
For each Critical, paste back to the agent:
Fix Critical issue #1 from pre-deploy-review-report.md. Apply the suggested fix exactly, run a quick test, confirm the issue is resolved. Do not introduce changes outside the scope of this fix.Re-run the audit until Critical count = 0. High and Medium can wait until after launch.
Step 3 — Push to GitHub
terminalInitial commit + pushgit init
git add .
git commit -m "Initial commit — HardwarePOS built with VibeKit"
gh repo create hardware-pos --private --source=. --push
# OR manually create on github.com and:
# git remote add origin https://github.com/YOU/hardware-pos.git
# git push -u origin mainStep 4 — Import to Vercel + set env vars
- Go to vercel.com/new
- Import the
hardware-posrepo. Framework auto-detects as Next.js. - Build command:
prisma generate && prisma migrate deploy && next build - Don't deploy yet — set env vars first.
Vercel → Settings → Environment VariablesProduction envDATABASE_URL=<your Neon prod connection string>
BETTER_AUTH_SECRET=<a NEW 32+ char string, NOT the dev one>
BETTER_AUTH_URL=https://hardware-pos.vercel.app
RESEND_API_KEY=<from resend.com>
RESEND_FROM_EMAIL=noreply@yourshop.com
GOOGLE_CLIENT_ID=<from Google Cloud Console>
GOOGLE_CLIENT_SECRET=<from Google Cloud Console>
NEXT_PUBLIC_APP_URL=https://hardware-pos.vercel.appStep 5 — Add OAuth redirect URI
In Google Cloud Console → APIs & Services → Credentials → your OAuth client → Authorized redirect URIs, add:
https://hardware-pos.vercel.app/api/auth/callback/googleStep 6 — Deploy + smoke test
Hit Deploy. Wait ~2 min. Visit the URL and run through the full flow:
- Sign up + verify welcome email
- Add 5 products in /inventory
- Make a real sale via /pos with Mobile Money payment
- Download the receipt PDF
- Verify the dashboard updated
- Toggle dark mode on a phone
Step 7 — Custom domain (optional)
- Buy a domain on Cloudflare Registrar (or your provider)
- Vercel → Settings → Domains → add your domain
- Cloudflare DNS → add the records Vercel shows (set to DNS only, grey cloud, not orange)
- Update
BETTER_AUTH_URLandNEXT_PUBLIC_APP_URLto the custom domain - Add the new redirect URI in Google Cloud Console
- Redeploy
You shipped it.
That's a real POS — auth, transactions, atomic stock decrements, downloadable receipts, audited for security, deployed to a custom domain. A hardware shop owner could actually use this.
Share what you built in the community — and tag it with #shipped-with-vibekit.
Take it further.
The patterns you just learned scale to anything. Some natural next steps for HardwarePOS or your next build:
Add real Mobile Money
Install JB DGateway Shop and wire actual MoMo settlement to the sale flow. ~45 min.
Multi-cashier support
Add a 'cashier' role + sale.cashierId so you can see who rang up which sales. ~30 min.
Barcode scanner input
Hook a USB scanner into the SKU search — it's just keyboard input. ~15 min.
Stock-in / restock flow
Track inventory deliveries with a separate StockMovement model. ~45 min.
Daily Z-report (end of day)
Closing report with sales by payment method + cash drawer reconciliation. ~30 min.
Build something else
Restart from Module 02 with a new app idea. The flow is fully repeatable.