ShopNex
ShopNex는 관리·결제·스토어 API·웹훅·앱 마켓을 갖춘 즉시 사용 가능한 자체 호스팅 상거래 플랫폼으로, 개발자 없이도 운영 가능합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.