ShopNex
A ready-to-use, self-hostable commerce platform: admin, hosted checkout, Storefront API, webhooks and an app marketplace. No developers required to run a shop.
Offline POS, batch & expiry stock, M-Pesa and the PPB register — on Android, Windows or the browser.
CommerceTypeScript · RustGitHub @kimutaiwycliffOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Pharmacy POS and inventory management system for Kenyan pharmacies
Taken from the repository README
Loading the full README.
Read the full README ↗Multi-tenant pharmacy management for Kenya — point-of-sale, inventory, clinical appointments, and a SaaS billing/operator layer, in one app.
PharmaTrack is a production-grade SaaS that pharmacies sign up for and run their day-to-day on: an offline-capable POS (cash, M‑Pesa, card, split), batch/expiry-aware inventory with a controlled-substances register, a product catalogue, suppliers, a bookings system for injections/family-planning with SMS/email reminders, reporting, staff & shift management, and a platform console for the operator to manage tenants and subscriptions.
It is fully self-hosted — Postgres, Redis, object storage and the app all run on your own VM via Docker Compose, managed through make commands. No cloud lock-in.
📘 New here and just want to use the platform? Read the User Guide — everything a pharmacy needs to know after signing up.
Point of sale
Inventory
Catalogue
Appointments & reminders
Management
Africa/Nairobi timeSaaS / platform
Roles: owner, manager, pharmacist, cashier, plus a separate platform admin (SaaS operator).
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router) + React 19, TypeScript |
| Styling | Tailwind CSS v4 with design tokens, light/dark via next-themes |
| Data/state | TanStack Query, Zustand, Zod |
| Database | PostgreSQL 16 — Row-Level Security, two roles (app_owner / app_authenticated) |
| ORM | Drizzle ORM (postgres.js driver) |
| Migrations | dbmate (raw SQL — allows hand-written RLS policies/functions) |
| Auth | Better Auth (organization + admin plugins, Drizzle adapter) + a custom PIN-login plugin |
| Cache/queue | Redis (ioredis) — product lookups, rate limiting, BullMQ jobs |
| Object storage | MinIO (S3-compatible, self-hosted) — product images + DB backups |
| Receipts/charts | @react-pdf/renderer, Recharts |
| Observability | GlitchTip / Sentry SDK (optional, env-gated) + pino |
| Billing | Paystack (optional, env-gated) |
| Messaging | Africa's Talking (SMS + WhatsApp) + Resend (email) |
| Proxy | Caddy 2 (automatic TLS) |
| Tooling | pnpm workspaces, Vitest, ESLint, GitHub Actions CI, Docker Compose, Makefile |
apps/web), a BullMQ worker (apps/worker), and shared packages (db, types, …).organization is the tenant. Every domain table is org-scoped and enforced in the database with RLS. Tenancy is request-scoped via Postgres GUCs (app.organization_id / app.role / app.branch_id) set by withTenant() in packages/db, so isolation doesn't depend on application code being perfect.app_owner (migrations/seed/admin, bypasses RLS as object owner) and app_authenticated (all app queries, RLS-enforced)./api/auth/sign-in/pin). Invite/reset links land on /auth/set-password?token=….apps/web/proxy.ts (Next 16's renamed middleware) does an optimistic cookie gate; public routes include auth, health, cron and webhooks.pharmatrack/
├── apps/
│ ├── web/ # Next.js application
│ │ ├── app/
│ │ │ ├── (auth)/ # login, set-password, reset, /auth/callback
│ │ │ ├── (dashboard)/ # dashboard, inventory, products, suppliers,
│ │ │ │ # appointments, staff, shifts, reports, settings
│ │ │ ├── (pos)/ # POS terminal
│ │ │ ├── (platform)/ # SaaS operator console (/platform)
│ │ │ └── api/ # route handlers (REST), cron, webhooks, health
│ │ ├── components/ # UI, feature components, theme, platform
│ │ ├── lib/ # auth (Better Auth), redis, api-auth, billing,
│ │ │ # notifications, appointments, storage (MinIO)
│ │ └── proxy.ts # auth middleware (Next 16)
│ └── worker/ # BullMQ workers + cron scheduler
├── packages/
│ ├── db/ # Drizzle schema, withTenant()/dbAdmin(), RLS test
│ └── types/ # shared TypeScript types
├── infra/
│ ├── compose.core.yml # postgres, redis, minio + (app profile) web, worker
│ ├── db/init/ # role bootstrap on fresh DB init
│ └── migrations/ # dbmate SQL migrations (001_… onward)
├── docs/ # USER_GUIDE, QA_CHECKLIST, ADRs
├── Dockerfile # standalone production image
├── Caddyfile # reverse proxy + auto-TLS
├── Makefile # make up / db-migrate / test-rls / build-web …
└── CLAUDE.md # master engineering reference
pnpm install
cp .env.example .env
The DB / Redis / MinIO defaults already work for local dev (they point at the host
ports the compose stack publishes — Postgres 55432, Redis 56379, MinIO 9000).
The only values you must set:
BETTER_AUTH_SECRET → any random 32+ character stringCRON_SECRET → any random stringRESEND_API_KEY + EMAIL_FROM → only if you want invite/reset emails to actually sendmake up # starts postgres + redis + minio (Docker)
make db-migrate # applies infra/migrations 001–007: schema + RLS + seeds (plans + ~95 KEML drug catalog)
make test-rls # optional — proves two-org tenant isolation
pnpm dev # http://localhost:3000
Open http://localhost:3000 → you're redirected to /setup to create the
platform (operator) admin (email + password). You land in /platform, where you
provision a pharmacy (org + branch + 14-day trial + default services + owner).
Note: the dev server runs with Webpack and a bounded heap — intentional, so Next 16's Turbopack dev watcher can't exhaust memory on low-RAM machines. Production builds are unaffected.
Provisioning emails the owner a "set your password" link, so:
/auth/set-password?token=…, sets a password, and signs in.TOKEN=$(docker exec pharmatrack-postgres-1 psql -U app_owner -d pharmatrack -tAc \
'select identifier from verification order by "createdAt" desc limit 1;' | sed 's/reset-password://')
curl -s -X POST http://localhost:3000/api/auth/reset-password \
-H 'Content-Type: application/json' \
-d "{\"newPassword\":\"owner-pass-123\",\"token\":\"$TOKEN\"}"
Then sign in at /login with the owner's email + that password. (You can also set a
phone + 4-digit PIN in Settings → Profile and use the Quick PIN Login tab.)Image upload needs a MinIO bucket — everything else works without it:
docker run --rm --network pharmatrack_default --entrypoint sh minio/mc -c "\
mc alias set m http://minio:9000 minioadmin minioadmin && \
mc mb -p m/pharmatrack-products && mc anonymous set download m/pharmatrack-products"
| Command | Does |
|---|---|
make down | stop the stack |
make clean | stop and wipe data volumes (fresh start) |
make logs | follow container logs |
make db-shell | psql into the database |
make build-web && make start-web | run the production standalone server locally (service worker registers) |
Defined in .env.example. Highlights:
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | ✅ | Postgres as app_owner (migrations, admin, Better Auth adapter) |
DATABASE_AUTHENTICATED_URL | ✅ | Postgres as app_authenticated (RLS-enforced app queries) |
BETTER_AUTH_SECRET | ✅ | Session signing secret (rotate regularly) |
BETTER_AUTH_URL / NEXT_PUBLIC_APP_URL | ✅ | Canonical app URL (cookies, links, callbacks) |
REDIS_URL | ➖ | Cache / rate-limit / BullMQ (degrades to no-cache if unset) |
MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET_PRODUCTS | ➖ | Product image uploads |
RESEND_API_KEY / EMAIL_FROM | ➖ | Transactional + reminder email |
MPESA_* | ➖ | M‑Pesa Daraja STK push at the till |
AFRICASTALKING_* | ➖ | SMS + WhatsApp reminders |
PAYSTACK_SECRET_KEY / PAYSTACK_WEBHOOK_SECRET | ➖ | Automated subscription billing |
CRON_SECRET | ➖ | Protects the reminder cron endpoint |
GLITCHTIP_DSN / NEXT_PUBLIC_GLITCHTIP_DSN | ➖ | Error tracking |
Every optional integration degrades gracefully — the app runs without it; the related feature simply stays inactive until configured.
infra/migrations/*.sql, applied in order by dbmate (make db-migrate).packages/db holds the Drizzle schema that mirrors those migrations and is the typed query layer; withTenant(orgId, fn) opens an RLS-scoped transaction, dbAdmin() is the privileged pool.app_owner / app_authenticated) are created by infra/db/init on fresh DB init.make test-rls runs the two-org isolation suite (packages/db/tests/rls-isolation.mts) — keep it green before merging schema changes.pnpm --filter web typecheck # tsc --noEmit
pnpm --filter web lint # eslint
pnpm --filter web test # vitest
make test-rls # two-org RLS isolation suite
pnpm --filter web build # production (standalone) build
GitHub Actions (.github/workflows/ci.yml) runs typecheck → lint → test → build on every push and PR.
For manual release testing, use the QA checklist.
Self-hosted on a single VM (or split: app VM + ops VM) via Docker Compose. The repo ships a multi-stage Dockerfile (Next standalone output, non-root, healthcheck) and infra/compose.core.yml.
cp .env.example .env # fill production values
make up # postgres + redis + minio
make db-migrate # apply migrations
make up-app # build + run web + worker (the `app` profile)
Caddyfile terminates TLS (automatic Let's Encrypt) and reverse-proxies the web service — point APP_DOMAIN's A record at the VM. TLS is required: the offline PWA service worker, the camera barcode scanner, and M‑Pesa callbacks all need HTTPS.
To run the production server locally without Docker:
make build-web && make start-web # Next standalone server, loads .env
GET /api/health for load balancers / uptime monitors.GET /api/cron/appointment-reminders with the CRON_SECRET bearer token (the worker also schedules jobs).See DEPLOYMENT.md for the full runbook (VM sizing, DNS, backups, rollback).
PharmaTrack is built to be operated as a service:
subscription (status: trialing/active/past_due/suspended/cancelled) on a plan./platform (platform admins only) lists tenants, provisions new pharmacies (creates the org + owner invite + branch + services + subscription), changes plans/status, extends paid-until/trial, suspends/reactivates, and records payments./api/webhooks/paystack activates and extends the subscription on charge.success).The first platform admin is created at /setup (self-locks afterwards); grant further operator access by adding a user's id to the platform_admin table.
| Integration | Enables | How to turn on |
|---|---|---|
| M‑Pesa (Daraja) | STK push payments at the till | Set MPESA_* |
| Africa's Talking | SMS + WhatsApp appointment reminders | Set AFRICASTALKING_* |
| Resend | Transactional + reminder email | Set RESEND_API_KEY / EMAIL_FROM |
| Paystack | Card + M‑Pesa subscription billing | Set PAYSTACK_SECRET_KEY; add the webhook {APP_URL}/api/webhooks/paystack |
| MinIO | Product image uploads + DB backups | Set MINIO_* |
| GlitchTip / Sentry | Error tracking | Set GLITCHTIP_DSN / NEXT_PUBLIC_GLITCHTIP_DSN |
| Redis | Faster lookups, rate limiting, background jobs | Set REDIS_URL |
.env.Built for Kenyan pharmacies. For day-to-day usage, see the User Guide.