Crash course · 8 modules · ~4 hours

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.

Watch the full crash course on YouTube as you follow the modules below · Watch on YouTube
WHAT YOU'LL BUILD

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
Total time
~4 hrs
Modules
8
Lines you write
~0
Cost (free tiers)
$0
MODULE 00 · 5 min · OPTIONAL BUT RECOMMENDED

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.

MODULE 01 · 8 min

Set up the accounts you'll need

All free tiers cover the entire course. Sign up first so you don't break flow later.

Tip
$0 to start. Both store fees can wait until Module 08. You can build, test on simulators, and run dev builds with zero spend.
MODULE 02 · 15 min

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 brief
I 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

  1. Confirms it has read the four reference URLs (README, design-style-guide, vibekit-native-components, master_prompt)
  2. Asks 6–10 mobile-specific questions: platforms, auth method, payment providers, push notifications, deep links, dark mode, visual reference
  3. Summarises what it's going to build and asks you to confirm
  4. Generates 4 Artifacts: project-description.md, project-phases.md, design-style-guide.md, prompt.md
Tip
Visual reference is mandatory. Claude asks for a Dribbble link / app screenshot / competitor app you want to match. Have one ready before you start — examples: Dribbble mobile shots, the Square POS app, the Stripe Terminal app, the Shopify POS app.

Save all 4 generated files into a new project folder on your machine. Name the folder hardware-pos-mobile.

MODULE 03 · 10 min

Initialize the project

Step 1 — Create the folder + open in your editor

terminalProject folder
mkdir 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.md

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

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

Tip
Switching between agents on the same project? Install for all of them at once — see the "Multi-agent setup" section in skill/README.md. One canonical file, symlinked to each agent's expected path.

Step 4 — Verify your project root

You should now have these 7 files in your project root:

ls -laExpected files
hardware-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 it

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

MODULE 04 · 30 min

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

  1. Go to console.neon.tech and create a new project.
  2. Copy the connection string (starts with postgresql://). Use the pooled connection — the Neon HTTP serverless driver handles it.
  3. Keep the tab open — you'll paste this in a moment.

Step 2 — Get a DGateway sandbox key

  1. Visit dgatewayadmin.desispay.com and create an app.
  2. Generate an API key. Use the dgw_test_* key for development.
  3. 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:

  1. Read master_prompt.md, design-style-guide.md, vibekit-native-components.md, project-description.md, project-phases.md
  2. Execute Phase 1 tasks (Expo init, NativeWind, expo-router, Prisma + Neon, Better Auth + Expo plugin, EAS init, root auth gate, .env files, Sentry)
  3. 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.
Tip
Phase 1 confirms when you can sign up, sign in, and land on a placeholder tab screen. If you can't, paste the error into your agent and ask it to fix before moving on.
MODULE 05 · 35 min

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 payments

Screens 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 chart
  • inventory/new.tsx + inventory/[id].tsx — product create / edit
  • sale/[id].tsx — single sale detail with line items
  • checkout.tsx — payment method picker → DGateway / Stripe / Cash
Tip
Mock data is in src/lib/mocks/. Hardcoded arrays of products and sales — enough to verify every screen renders before wiring real APIs.
MODULE 06 · 50 min

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 product
  • app/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 items
  • app/api/dashboard/+api.ts — aggregations via Prisma groupBy
  • app/api/checkout/start+api.ts — DGateway STK push proxy
  • app/api/checkout/status/[reference]+api.ts — payment status proxy
  • app/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 have
export 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 dedupe
import 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)
Tip
Phase 3 confirms when: you can create a product, ring up a sale with the test phone number, see the "Check customer's phone" screen, and watch the sale move from pending → completed in the history list when the webhook fires.
MODULE 07 · 30 min

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 FadeIn stagger on product list rows (respects useReducedMotion)
  • Haptics.impactAsync(ImpactFeedbackStyle.Light) on every primary button
  • Haptics.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-notifications permission 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
Tip
Test on a real device. Simulators don't show haptics, simulators run hot (often hiding perf issues), and push notifications need a real device + APNs / FCM tokens. Plug in an iPhone or Android phone before signing off Phase 4.
MODULE 08 · 45 min

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 seed

Step 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 others

Step 4 — Deploy the API routes to EAS Hosting

terminalOne command, returns a stable URL
eas 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 together
eas 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 Internal
eas 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.)
Tip
Apple review takes 24–72 hours. Google Play Internal Testing is instant; promotion to production takes a few hours. Plan accordingly — the first submission is the slow one; after that, OTA + EAS Build covers 95% of changes.

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.