A production-deployed, multi-tenant enterprise store management platform built for retail businesses that need structure, auditability, and role-based control across their operations.
Traqify is a full-stack TypeScript monorepo (a Next.js 14 App Router frontend + RESTful Express.js backend covering the complete retail lifecycle: product catalogue, POS order creation, inventory control, customer records, payment tracking, staff access management, public storefronts with Paystack checkout, financial reports, and a real-time analytics dashboard.
The system is live at https://traqify.vercel.app and its API at https://traqify-api.vercel.app.
CLIENT
+--------------------------------------+
| Next.js 14 (App Router) :3000 |
| TypeScript + Tailwind + Framer |
| Axios client (JWT + auto-refresh) |
+------------------+-------------------+
|
HTTP/HTTPS | REST JSON API
|
+------------------v-------------------+
| Express.js API Server :5000 |
| TypeScript + Prisma ORM |
| JWT middleware + RBAC middleware |
+------+----------+--------------------+
| |
+-----------+ +-----------+
| |
+--------v-----------+ +-----------v-----------+
| PostgreSQL (via | | Supabase Storage |
| Supabase) | | (product images, |
| | | avatars) |
| Prisma ORM | +-----------------------+
+--------------------+
|
+--------v-----------+
| Nodemailer |
| Gmail SMTP |
| HTML email |
| templates |
+--------------------+
AUTHENTICATION FLOWS
----------------------
Email/Password (OTP-first flow):
1. POST /send-otp -> OTP email (works before user exists)
2. POST /verify-email -> validate code -> redirect /register?verifiedEmail=...
3. POST /register -> create user (emailVerified:true) -> JWT (React state only)
4. POST /organizations -> create org -> POST /login -> fresh JWT with orgId
-> redirect /dashboard/[slug]/overview
Google OAuth 2.0:
GET /google-redirect -> accounts.google.com
-> GET /google-callback?code= -> exchange code -> userinfo
-> upsert user -> JWT + redirect to /auth-callback
Token Refresh:
Axios 401 interceptor -> POST /auth/refresh -> new access token
Automatic, transparent to all callersNext.js 14 (App Router), TypeScript, Tailwind CSS, Framer Motion, Recharts
Express.js, TypeScript, Prisma ORM, JWT, bcrypt
PostgreSQL (hosted on Supabase)
Supabase Storage for product images and avatars
| Concern | Technology | Notes |
|---|---|---|
| Frontend framework | Next.js 14 | App Router, SSR + Client Components |
| Language | TypeScript 5 | Strict mode, full type coverage |
| Styling | Tailwind CSS 3 | JIT, custom config |
| UI primitives | Radix UI / shadcn/ui | Accessible, unstyled components |
| Animations | Framer Motion | Page transitions, scroll animations, charts |
| Charts | Recharts | AreaChart, BarChart, PieChart |
| HTTP client | Axios | Interceptors for JWT + token refresh |
| Backend framework | Express.js 4 | TypeScript, modular routes |
| ORM | Prisma | Type-safe queries, migrations via db push |
| Database | PostgreSQL | Hosted on Supabase |
| Auth | JWT + bcrypt | Access + refresh token pair |
| OAuth | Google OAuth 2.0 | Redirect-based (no popup) |
| File storage | Supabase Storage | Product images, avatars |
| Nodemailer | Gmail SMTP, HTML templates | |
| Security | Helmet, rate-limit | Per-route rate limiting on auth endpoints |
| File upload | Multer (memoryStorage) | Images: JPG/PNG/WebP max 2 MB; downloadable files: any type max 4 MB |
| Font | Jost | Google Fonts |
Every database query — products, orders, customers, staff, payments, audit logs — is scoped by organizationId. No query runs without it. There is no global admin view; isolation is enforced at the ORM layer, not in frontend logic alone. The system supports creating multiple separate organizations (e.g., separate branches) under different slugs.
Four roles with granular middleware enforcement:
| Role | Capabilities |
|---|---|
| OWNER | Full access: all modules, staff management, org settings, audit logs |
| MANAGER | Products, inventory, orders, customers, staff invitations |
| CASHIER | Create orders, view own transactions, browse product catalog |
| AUDITOR | Read-only access to all data, audit logs, financial reports |
Auth endpoints (/api/auth/*) are rate-limited via express-rate-limit: 10 requests per 15 minutes per IP on sensitive routes (login, register, OTP send).
helmet is applied globally: sets X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, X-XSS-Protection, and Content Security Policy headers.
User id, email, name, password (bcrypt), phone, avatarUrl emailVerified, signInMethod (EMAIL | GOOGLE) role (OWNER | MANAGER | CASHIER | AUDITOR) isActive Boolean -- account restriction flag organizationId (FK -> Organization)? invitedById (FK -> User)? -- set when joined via invite lastLoginAt, createdAt Organization id, name, slug (unique), email, phone, address, website industry, size, description? logoUrl? storePublished Boolean ownerId (FK -> User) Product id, name, sku (unique per org) price, comparePrice?, description? imageUrl?, imageUrls String[] productType (SIMPLE | DOWNLOADABLE | VARIABLE) downloadUrl? status (published | draft), isActive Boolean categoryId (FK -> ProductCategory) organizationId Inventory id, quantity Int, lowStockAlert Int productId (1:1 -> Product) Order id, status (PENDING|APPROVED|COMPLETED|CANCELLED) totalAmount, paymentMethod?, notes? customerId?, organizationId, createdByUserId? OrderItem id, productId, quantity, unitPrice, subtotal orderId Payment id, amount Float, currency (default NGN) status (PENDING|COMPLETED|FAILED|REFUNDED) method?, reference?, notes? organizationId, orderId? Customer id, name, email?, phone?, address? source (MANUAL | PURCHASE) organizationId StaffInvite id, email, role, token (unique) status (PENDING | ACCEPTED | EXPIRED) expiresAt, organizationId, invitedById Review id, orderId, productId, organizationId rating Int (1-5), comment? customerName, customerEmail? status (PENDING | APPROVED | REJECTED) @@unique([orderId, productId]) ProductCategory id, name, slug, description? organizationId OTPCode id, email, code, expiresAt, used Boolean PasswordResetToken id, email, token, expiresAt, used Boolean AuditLog id, userId, organizationId action (CREATE | UPDATE | DELETE | LOGIN) entity String, entityId String, details String ipAddress?, userAgent? isRead Boolean (default false) createdAt Wishlist id, sessionId, email?, productIds String[] slug, organizationId sent30min, sent2hr, sentDay1, sentDay3 Boolean createdAt NewsletterSubscriber id, email, subscribedAt
git clone https://github.com/oyedokunken/traqify.git
cd traqifycd backend
npm install
cp .env.example .envOpen .env and fill in the required environment variables (DATABASE_URL, DIRECT_URL, SMTP_USER, SMTP_PASS, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, JWT_SECRET, JWT_REFRESH_SECRET).
Push the schema to your database:
npx prisma db pushStart the backend dev server:
npm run devcd frontend
npm install
cp .env.local.example .env.localFill in NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY.
Start the frontend:
npm run dev