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.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Pharmacy POS and inventory management system for Kenyan pharmacies
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 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.