Build a real Hardware POS mobile app in one afternoon.
Follow this crash course and ship HardwarePOS Mobile — an Expo / React Native point-of-sale app a hardware shop in Kampala could install Monday morning. Inventory, sales, DGateway mobile money, dashboard. Submitted to the App Store + Play Store by sundown. Powered by VibeKit Native + your favourite AI coding agent.
HardwarePOS Mobile — a real shop POS.
Not a tutorial toy. Real auth, real database, real DGateway mobile money, real transactions, real OTA updates. A hardware shop owner could install this from the Play Store and start using it.
Features you'll ship
- Email + password sign-in (Better Auth + Expo plugin)
- Inventory: products with SKU, price (UGX), stock, category
- Six seeded categories: Tools, Hardware, Paint, Plumbing, Electrical, Other
- Low-stock push notifications via expo-notifications
- POS sale screen: product search, cart, live total
- Three payment methods: Cash, DGateway mobile money, Stripe card
- DGateway STK push flow: "Check customer's phone" status screen
- Atomic stock decrement in Prisma transactions
- Sales history with date-range filtering
- Dashboard: today's revenue, top products, weekly chart
- Dark-mode native UI, 60fps scrolling, haptic feedback
- Shipped to App Store + Play Store via EAS Submit
Skills you'll learn
- Planning a real mobile product with Claude before any code
- Reading a phase-by-phase mobile build plan
- Bootstrapping Expo SDK 55 + NativeWind v4 + expo-router
- Wiring Prisma v7 with Neon's HTTP serverless driver
- Better Auth + @better-auth/expo (SecureStore session)
- Expo API Routes — same repo, no separate backend
- Modelling transactional data (Sale + SaleItem pattern)
- Auth-guarded Expo API Routes with Zod validation
- Installing VibeKit Native registry components on demand
- DGateway server proxy + HMAC-verified webhook
- Aggregating data with Prisma groupBy for dashboards
- EAS Build + EAS Submit + EAS Update + EAS Hosting
- Running a senior-level mobile pre-submission audit
The full path.
Click any module to jump in. They build on each other — follow them in order on your first run.
- MODULE 018 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 0535 min
Phase 2 — Inventory + POS screens
- MODULE 0650 min
Phase 3 — API Routes + DGateway
- MODULE 0730 min
Phase 4 — Dashboard + Polish
- MODULE 0845 min
Phase 5 — Pre-deploy + ship
Check your environment first
Before signing up for accounts, make sure your machine has the mobile toolchain VibeKit Native needs. The fastest way to check is to paste the OS-specific prompt at native.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.
Minimum tools
- Node 20+, pnpm 9+, git
- Expo CLI (no separate install — comes via
npx) - EAS CLI:
pnpm add -g eas-cli - iOS only: Xcode 16+ with iOS Simulator (macOS only — Windows users skip iOS and use Android)
- Android: Android Studio + an emulator OR a real Android phone with USB debugging
- Optional: Expo Go app on your phone for quick QR-code testing
If everything's installed, skip to Module 01.
Set up the accounts you'll need
All free tiers 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
- Expo accountRequired for EAS Build / Submit / Update / Hosting — free tier covers dev
- Neon (Postgres)Free tier (3 GB storage, autoscale)
- Resend (email)Transactional email — free tier 3,000/mo
- SentryCrash reporting — free tier 5K events/mo
- DGateway sandboxMobile money sandbox — request a dgw_test_ key
- Stripe (optional)Card payments — test keys are free
- GitHub accountFor source control + EAS auto-build hooks
- Apple Developer ($99/yr)Only when you submit to App Store — defer until Module 08
- Google Play Console ($25 one-time)Only when you submit to Play Store — defer until Module 08
Plan with Claude (claude.ai)
Open claude.ai and start a new conversation. Paste the contents of CLAUDE_PROMPT.md from the VibeKit Native repo. Then paste the HardwarePOS Mobile brief at the bottom.
The brief — paste this after CLAUDE_PROMPT.md
MY IDEAHardwarePOS Mobile briefI want to build HardwarePOS Mobile — an Expo / React Native point-of-sale app for
a small hardware shop in Uganda. The shop owner uses it on a phone or tablet 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 that works
even on a slow connection.
Platforms: iOS + Android (Expo Web optional for testing).
Core flows:
1. POS Sale (the main screen): search products by name or SKU, tap to add to
cart, adjust quantities with steppers, see live total. Pick payment method
(Cash / Mobile Money via DGateway / Card via Stripe). For mobile money,
the customer's phone number is captured and an STK push prompt is sent;
the cashier sees a "Check your customer's phone" screen until payment
completes. Complete sale, then offer a printable / shareable receipt.
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.
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.
Auth: email + password via Better Auth. Single user role for now.
Payments:
- DGateway (mobile money — UGX) for the Ugandan market — required.
- Stripe (cards / Apple Pay / Google Pay) for tourists / card customers — optional.
No image uploads — text-only products (name + SKU + category is enough).
Currency: UGX with comma-separated formatting and no decimals (25,000 not 25,000.00).
Dark mode only (faster to ship; the cashier works in dim shop light anyway).
Push notifications: Yes (low-stock alerts at end of day).
Deep linking: No (single-user, no shared content).
Offline support: reads work offline (TanStack Query persister); writes need
connectivity (sales can't be recorded without confirming payment).
Aesthetic: clean dashboard like Linear / Vercel — bold large numbers so the
cashier can read totals at a glance. Brand color: indigo (#6366F1).
Deployment: EAS Build production for both iOS + Android, EAS Submit to App
Store + Google Play, EAS Update for OTA JS-only patches, EAS Hosting for the
Expo API Routes backend.What Claude does next
- Confirms it has read the four reference URLs (README, design-style-guide, vibekit-native-components, master_prompt)
- Asks 6–10 mobile-specific questions: platforms, auth method, payment providers, push notifications, deep links, dark mode, visual reference
- Summarises what it's going to build and asks you to confirm
- Generates 4 Artifacts:
project-description.md,project-phases.md,design-style-guide.md,prompt.md
Save all 4 generated files into a new project folder on your machine. Name the folder hardware-pos-mobile.
Initialize the project
Step 1 — Create the folder + open in your editor
terminalProject foldermkdir hardware-pos-mobile && cd hardware-pos-mobile
# Move the 4 generated files (project-description, project-phases,
# design-style-guide, prompt) from Claude into this folder.Step 2 — Copy the framework files
Clone the VibeKit Native repo to grab the framework files:
terminalOne-time clone (delete after copying)git clone https://github.com/MUKE-coder/vibekit-native.git /tmp/vibekit-native
cp /tmp/vibekit-native/master_prompt.md ./master_prompt.md
cp /tmp/vibekit-native/vibekit-native-components.md ./vibekit-native-components.md
cp /tmp/vibekit-native/pre-deploy-review.md ./pre-deploy-review.mdStep 3 — Install the VibeKit Native rules for your AI agent
VibeKit Native 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-mobile/
├── master_prompt.md # framework — coding rules
├── vibekit-native-components.md # framework — component registry
├── pre-deploy-review.md # framework — pre-submission 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-native/SKILL.md # Claude Code
├── .cursor/rules/vibekit-native.mdc # Cursor
├── AGENTS.md # Codex CLI / universal
├── .clinerules # Cline
├── .windsurfrules # Windsurf
├── GEMINI.md # Gemini CLI
└── # No Expo scaffold yet — Phase 1 creates itStep 5 — Open in your coding agent
Open the hardware-pos-mobile 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 the framework files, then executes Phase 1: Expo init + NativeWind + expo-router + Prisma v7 + Neon HTTP adapter + Better Auth + Expo plugin + EAS init + Sentry. Sign-in works, you have a dev build running.
Step 1 — Get a Neon database URL
- Go to console.neon.tech and create a new project.
- Copy the connection string (starts with
postgresql://). Use the pooled connection — the Neon HTTP serverless driver handles it. - Keep the tab open — you'll paste this in a moment.
Step 2 — Get a DGateway sandbox key
- Visit dgatewayadmin.desispay.com and create an app.
- Generate an API key. Use the
dgw_test_*key for development. - Save both the key and the webhook secret (shown once at generation).
Step 3 — 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,vibekit-native-components.md,project-description.md,project-phases.md - Execute Phase 1 tasks (Expo init, NativeWind, expo-router, Prisma + Neon, Better Auth + Expo plugin, EAS init, root auth gate, .env files, Sentry)
- Stop after Phase 1 for your confirmation
Step 4 — Provide secrets when asked
The agent creates .env.local and asks for values. Provide:
.env.localPhase 1 minimum env vars# Database (Neon — paste the pooled connection string)
DATABASE_URL=postgresql://user:pass@host-pooler.neon.tech/db?sslmode=require
# Better Auth (server-side)
BETTER_AUTH_SECRET=<run: openssl rand -base64 32>
BETTER_AUTH_URL=http://localhost:8081
# Mobile app config (safe to bundle — prefix EXPO_PUBLIC_)
EXPO_PUBLIC_API_URL=http://localhost:8081
EXPO_PUBLIC_APP_SCHEME=hardwarepos
# DGateway (server-side ONLY — never EXPO_PUBLIC_)
DGATEWAY_API_KEY=dgw_test_...
DGATEWAY_API_URL=https://dgatewayapi.desispay.com
DGATEWAY_WEBHOOK_SECRET=whsec_...
APP_URL=http://localhost:8081
# Sentry (DSN is safe to bundle)
EXPO_PUBLIC_SENTRY_DSN=https://...@sentry.io/...Step 5 — Run the dev build
terminalStart the Metro bundler + simulator# Migrate the database
pnpm prisma migrate dev --name init
# Start Expo
pnpm expo start
# Then press 'i' to launch iOS simulator, or 'a' for Android emulator,
# or scan the QR with Expo Go on your physical phone.Phase 2 — Inventory + POS screens
Your agent installs registry categories and builds every screen listed in project-description.md with mock data. No API calls yet — that's Phase 3.
Registry installs (your agent runs these)
terminalPhase 2 component installs# Foundation primitives
npx vibekit-native install ui
# Shared layouts (screen-header, search-bar, filter-sheet, filter-sort-bar)
npx vibekit-native install shared
# Commerce (product-card, cart-item, price-display, order-card,
# order-summary, checkout-form, wishlist-button, review-card,
# order-timeline, product-header)
npx vibekit-native install commerce
# Bottom tabs + drawer
npx vibekit-native install nav
# Stat cards + chart-line / chart-bar / chart-pie + dashboard shell + data-table
npx vibekit-native install dashboard
# Payments (DGateway mobile money + Stripe)
npx vibekit-native install paymentsScreens your agent builds
(tabs)/index.tsx— POS sale screen (search bar, product grid, cart drawer)(tabs)/inventory.tsx— product list with low-stock badges(tabs)/history.tsx— sales history with date filter(tabs)/dashboard.tsx— stat cards, top products, weekly chartinventory/new.tsx+inventory/[id].tsx— product create / editsale/[id].tsx— single sale detail with line itemscheckout.tsx— payment method picker → DGateway / Stripe / Cash
src/lib/mocks/. Hardcoded arrays of products and sales — enough to verify every screen renders before wiring real APIs.Phase 3 — API Routes + DGateway
Every entity gets a CRUD +api.ts route under app/api/. Zod-validated, cursor-paginated, auth-gated. Then the DGateway proxy + HMAC-verified webhook. Then every screen swaps its mock data for a TanStack Query hook.
API routes your agent creates
app/api/products/+api.ts— GET (list with cursor + search), POST (create)app/api/products/[id]+api.ts— GET / PATCH / DELETE single productapp/api/sales/+api.ts— GET (list with date filter), POST (atomic stock decrement inside a Prisma transaction)app/api/sales/[id]+api.ts— GET single sale + line itemsapp/api/dashboard/+api.ts— aggregations via Prisma groupByapp/api/checkout/start+api.ts— DGateway STK push proxyapp/api/checkout/status/[reference]+api.ts— payment status proxyapp/api/webhooks/dgateway+api.ts— HMAC-SHA256 verified webhook with idempotent dedupe
The atomic stock decrement pattern
app/api/sales/+api.ts (POST)Prisma transaction — never sell what you don't haveexport async function POST(request: Request) {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
const parsed = CreateSaleSchema.safeParse(await request.json());
if (!parsed.success) return Response.json({ error: 'Invalid input' }, { status: 400 });
const { items, paymentMethod, customerName, customerPhone, total } = parsed.data;
// Single transaction: check stock + decrement + create sale, all-or-nothing
const sale = await prisma.$transaction(async (tx) => {
for (const item of items) {
const product = await tx.product.findUnique({ where: { id: item.productId } });
if (!product) throw new Error('Product not found');
if (product.stock < item.quantity) throw new Error(`Insufficient stock for ${product.name}`);
}
// Decrement in one shot
await Promise.all(
items.map((item) =>
tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } },
}),
),
);
return tx.sale.create({
data: {
userId: session.user.id,
total,
paymentMethod,
customerName,
customerPhone,
items: { create: items.map((i) => ({ productId: i.productId, quantity: i.quantity, unitPrice: i.unitPrice })) },
},
include: { items: true },
});
});
return Response.json({ data: sale }, { status: 201 });
}The DGateway webhook with HMAC verification
app/api/webhooks/dgateway+api.tsConstant-time HMAC compare + idempotent dedupeimport crypto from 'node:crypto';
import { prisma } from '@/src/lib/prisma';
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get('X-DGateway-Signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.DGATEWAY_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
if (
!signature ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(rawBody);
// Dedupe — DGateway retries up to 3 times
const existing = await prisma.paymentEvent.findUnique({
where: { reference: event.reference },
});
if (existing) return new Response(null, { status: 200 });
await prisma.$transaction([
prisma.paymentEvent.create({
data: {
reference: event.reference,
status: event.status,
provider: event.provider,
providerRef: event.provider_ref,
payload: event,
},
}),
prisma.sale.updateMany({
where: { paymentReference: event.reference },
data: { paymentStatus: event.status },
}),
]);
return new Response(null, { status: 200 });
}Test with DGateway sandbox phone numbers
DGateway provides deterministic phone numbers when you use a dgw_test_* key:
256111777111— always succeeds (use this for the happy path)256111777222— always fails (test the error UX)256111777333— times out / expires (test the 5-minute ceiling)
Phase 4 — Dashboard + Polish
Wire the dashboard with real aggregations. Add Reanimated entrance animations to lists. Wire expo-haptics on every CTA. Configure expo-notifications for the end-of-day low-stock push.
Dashboard aggregations
- Today's revenue:
prisma.sale.aggregate({ _sum: { total: true }, where: { createdAt: { gte: startOfDay() } } }) - Top 5 products this week:
prisma.saleItem.groupBy({ by: ['productId'], _sum: { quantity: true }, orderBy: { _sum: { quantity: 'desc' } }, take: 5 }) - Low-stock count:
prisma.product.count({ where: { stock: { lte: prisma.product.fields.lowStockThreshold } } }) - Weekly revenue chart: 7-day cursor query → group by day → feed to
chart-line
Polish checklist
- Reanimated
FadeInstagger on product list rows (respectsuseReducedMotion) Haptics.impactAsync(ImpactFeedbackStyle.Light)on every primary buttonHaptics.notificationAsync(NotificationFeedbackType.Success)on sale complete- Pull-to-refresh on every list
- Empty states with custom illustration (image-first 80/20)
- Skeleton loaders matching each card's silhouette
expo-notificationspermission request after first sign-in, not on cold start- Background task that fires the low-stock push at 6 PM
- Splash screen, adaptive icon, status bar style
Phase 5 — Pre-deploy + ship
Pre-deploy audit. Deploy the Expo API Routes to EAS Hosting. Build production binaries with EAS Build. Submit to TestFlight + Internal Testing. Promote to production.
Step 1 — Run the pre-deploy review
Open Claude Code (or your agent) and paste the contents of pre-deploy-review.md. It performs a 24-section senior audit covering cold-start perf, native correctness, auth, DB, API routes, webhooks, accessibility, env vars, Sentry, push, deep links, store-ready assets, EAS config.
Fix every 🔴 Critical and 🟠 High before moving on.
Step 2 — Migrate the production database
terminalApply Prisma migrations to the production Neon database# Switch DATABASE_URL to the production Neon URL temporarily
export DATABASE_URL="postgresql://..."
pnpm prisma migrate deploy
pnpm prisma db seedStep 3 — Set production env vars in EAS
terminalPush secrets to EAS Secret (never commit them)eas secret:create --scope project --name DATABASE_URL --value "postgresql://..." --type string
eas secret:create --scope project --name BETTER_AUTH_SECRET --value "$(openssl rand -base64 32)" --type string
eas secret:create --scope project --name DGATEWAY_API_KEY --value "dgw_live_..." --type string
eas secret:create --scope project --name DGATEWAY_WEBHOOK_SECRET --value "whsec_..." --type string
# ... and any othersStep 4 — Deploy the API routes to EAS Hosting
terminalOne command, returns a stable URLeas deploy --prod
# Copy the returned URL (e.g., https://hardware-pos-mobile.expo.app)
# Update app.json:
# "extra": { "apiUrl": "https://hardware-pos-mobile.expo.app" }Step 5 — Build production binaries
terminalEAS Build — iOS + Android togethereas build --profile production --platform all
# Takes 20-30 minutes. EAS handles signing for both stores.
# When done you'll get .ipa (iOS) + .aab (Android) downloadable from the EAS dashboard.Step 6 — Submit to the stores
terminalEAS Submit — push the latest build to TestFlight + Play Internaleas submit --profile production --platform ios
eas submit --profile production --platform android
# iOS goes to TestFlight Internal Testing first.
# Android goes to Play Console Internal Testing.
# Walk through both with 3+ humans before promoting to production.Step 7 — OTA updates for the small stuff
terminalEAS Update — no store review for JS-only changes# Fixed a typo? Renamed a button?
eas update --branch production --message "Fix checkout button label"
# Users get the patch on next app open. No 24-72hr review wait.
# (Native changes — new permissions, new deps — still require a rebuild + resubmit.)You're live
Once approved, your app is in the App Store and Google Play. The hardware shop downloads it, signs in, starts ringing up sales. DGateway proceeds settle to the merchant account within 24 hours. You ship updates via OTA without ever waiting for review.
Welcome to mobile dev.