The System

A production-deployed, multi-tenant enterprise store management platform built for retail businesses that need structure, auditability, and role-based control across their operations.

What is Traqify?

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.

Architecture

                              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 -> store tokens in OAuthSession (2 min TTL)
    -> redirect /auth-callback?code=<opaque>
    -> GET /oauth-exchange/:code -> tokens returned, code deleted

  Token Refresh:
    Axios 401 interceptor -> POST /auth/refresh -> new access token
    Tokens carry tokenVersion; incremented on logout/password change
    Automatic, transparent to all callers

Frontend

Next.js 14 (App Router), TypeScript, Tailwind CSS, Framer Motion, Recharts

Backend

Express.js, TypeScript, Prisma ORM, JWT, bcrypt

Database

PostgreSQL (hosted on Supabase)

Storage

Supabase Storage for product images and avatars

Tech Stack

ConcernTechnologyNotes
Frontend frameworkNext.js 14App Router, SSR + Client Components
LanguageTypeScript 5Strict mode, full type coverage
StylingTailwind CSS 3JIT, custom config
UI primitivesRadix UI / shadcn/uiAccessible, unstyled components
AnimationsFramer MotionPage transitions, scroll animations, charts
ChartsRechartsAreaChart, BarChart, PieChart
HTTP clientAxiosInterceptors for JWT + token refresh
Backend frameworkExpress.js 4TypeScript, modular routes
ORMPrismaType-safe queries, migrations via db push
DatabasePostgreSQLHosted on Supabase
AuthJWT + bcryptAccess + refresh token pair
OAuthGoogle OAuth 2.0Redirect-based (no popup)
File storageSupabase StorageProduct images, avatars
EmailNodemailerGmail SMTP, HTML templates
SecurityHelmet, rate-limitPer-route rate limiting on auth endpoints
File uploadMulter (memoryStorage)Images: JPG/PNG/WebP max 2 MB; downloadable files: any type max 4 MB
FontJostGoogle Fonts

Key Features

Multi-tenancy

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.

Role-Based Access Control (RBAC)

Four roles with granular middleware enforcement:

RoleCapabilities
OWNERFull access: all modules, staff management, org settings, audit logs
MANAGERProducts, inventory, orders, customers, staff invitations
CASHIERCreate orders, view own transactions, browse product catalog
AUDITORRead-only access to all data, audit logs, financial reports

Authentication

  • Email + password with OTP email verification (6-digit, 10-minute expiry)
  • Google OAuth 2.0 via redirect flow (no third-party popups)
  • JWT access token (15 minutes, kept in memory) + rotating httpOnly refresh cookie
  • Axios interceptor catches 401, refreshes once through the cookie and retries the request
  • Account lock by admin with notification email

Products and Inventory

  • Create products with name, SKU, category, price, compare-at price, description
  • Product types: SIMPLE, DOWNLOADABLE (download URL or direct file upload), VARIABLE (attribute builder)
  • Multi-image support: up to 4 images per product; first image is cover; drag-to-reorder
  • Auto-SKU generation from product name
  • Image upload (JPG / PNG / WebP, max 2 MB) to Supabase Storage (products bucket)
  • Downloadable file upload via dedicated POST /api/products/upload-file endpoint (any MIME type, max 4 MB)
  • Edit page at /products/[id]/edit with full feature parity to Add Product; category is immutable after creation
  • Publish confirmation modal on Add Product when status is set to Published
  • Save confirmation modal on Edit Product before any save; adapts text for publish vs draft saves
  • Per-product inventory with configurable low-stock alert threshold
  • Inventory adjustment log
  • Low-stock dashboard badge
  • Category management page (/dashboard/[slug]/categories)

Orders

  • POS-style order creation: search products, set quantities, attach customers
  • Order status flow: PENDING → APPROVED → COMPLETED / CANCELLED
  • Clickable rows — click anywhere on a row to open the order detail modal
  • Approve confirmation modal before status change; delete confirmation modal
  • Inventory auto-decremented on order creation
  • Order detail modal with full item and customer breakdown
  • Email confirmation to customer with org branding (logo, org name)
  • Admin email notification to the org owner when any new order is placed (dashboard or store)

Public Store

  • Each org gets /store/[slug] as a public product catalog
  • Responsive: mobile off-canvas drawer menu with category nav, cart/wishlist counts
  • Store navbar: full-width logo (or text fallback), cart badge, wishlist badge, category tabs (desktop)
  • Store info section below products: org name, description, and contact details linkable via #store-info
  • Filters: keyword search, category (left sidebar), dual price range slider (min/max derived from actual product prices)
  • Sort bar: Newest first, Oldest, Price (low→high / high→low), Name (A–Z)
  • Product cards: hover image cycling, wishlist heart overlay, discount % badge; separate View / Add-to-cart / Wishlist actions; object-contain images
  • Product detail drawer: image gallery with thumbnail strip, wishlist toggle, Add to cart
  • Wishlist: localStorage + backend sync; email capture; reminder emails at 30min/2hr/1day/3days
  • Cart with quantity controls; scroll-to-top button
  • Checkout: breadcrumb navigation, arithmetic CAPTCHA security check, Paystack payment popup, Secured by Paystack badge
  • Paystack payment: inline popup (no redirect), backend verification before order creation; successful payments create orders as APPROVED
  • Confirmation email to customer on order placement

Analytics Dashboard

  • Live clock (HH:MM:SS) and timezone displayed next to the greeting and date — updates every second
  • Period filter on charts: 7 / 30 / 90 days (applies to revenue, orders, and customer growth charts simultaneously)
  • Open Storefront button — opens the public store in a new tab; shows a modal if the store is unpublished with a direct link to publish
  • Revenue area chart, order growth area chart, customer growth line chart — all powered by live API data
  • Low-stock alert banner when any product is at or below alert threshold
  • First-time welcome modal: role-aware — owners see setup instructions; invited members see their role and access scope
  • Welcome-back modal on every new session (sessionStorage-gated)

Audit Logs

  • Every create / update / delete / login event logged with: user ID, organization ID, action type, entity name, entity ID, human-readable detail, IP address, user agent, and timestamp
  • Captures: product creates/edits, order status changes, customer updates, staff invites sent/accepted/cancelled/expired, password changes, account restrictions, report exports
  • Searchable and paginated; clickable rows navigate to a full detail page
  • Read/unread state per log entry; bulk mark-read with confirmation modal
  • Notification bell in topbar: shows latest 3 unread audit events; click navigates to detail
  • Visible to OWNER and AUDITOR only

Security Model

Authentication layers

  • OTP email verification — 6-digit code generated with crypto.randomInt, SHA-256 hashed before storage, expires after 10 minutes, invalidated after 5 failed attempts
  • bcrypt password hashing — cost factor 12; no plain-text passwords stored anywhere
  • JWT access token — 15 minute lifetime; carries userId, email, organizationId, role, and tokenVersion
  • Token version invalidation — tokenVersion incremented on logout and password change; all outstanding tokens for a user are rejected immediately regardless of their remaining TTL
  • Refresh token: 30-day opaque value stored as a SHA-256 hash, sent as an httpOnly cookie and rotated on every use; reusing an old one revokes the session
  • Google OAuth 2.0 — tokens stored in a short-lived OAuthSession; browser receives only an opaque one-time code; tokens are never placed in the URL
  • Startup validation — server throws before accepting traffic if JWT_SECRET, DATABASE_URL, PAYSTACK_SECRET_KEY, or FRONTEND_URL are missing
  • Invited user registration block — users with a pending staff invitation cannot create new accounts; they must use their invitation link or sign in

Authorization layers

  • authenticate middleware — verifies the JWT on every protected route; attaches req.user
  • requireOrg middleware — enforces that req.user.organizationId is set; prevents cross-org access
  • RBAC middleware — four guards: isOwnerOnly, isOwnerOrManager, isAtLeastAuditor, isAtLeastCashier; applied per-route, not per-controller
  • Data-layer isolation — all Prisma queries include organizationId in the where clause; no query trusts the frontend to scope data

Rate limiting

  • Global: 200 requests per 15 minutes per IP across all routes
  • Auth: 20 requests per 15 minutes per IP on all /api/auth/* routes

HTTP security headers

Helmet applied globally: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, X-XSS-Protection, Content Security Policy.

File upload security

  • Product images and avatars: JPEG/PNG/WebP only, max 5 MB
  • Downloadable product files: explicit MIME allowlist (PDF, ZIP, Office, images, audio, video), max 4 MB
  • Files stored in Supabase Storage via multer memoryStorage; server never writes files to disk

Payment security

  • Paystack transaction verified server-side before any order is created
  • Server recalculates order total independently; verified amount must match or the checkout is rejected

Database Schema

User
  id, email, name, password (bcrypt), phone, avatarUrl
  emailVerified, signInMethod (EMAIL | GOOGLE)
  role (OWNER | MANAGER | CASHIER | AUDITOR)
  isActive Boolean                         -- account restriction flag
  tokenVersion Int                         -- incremented on logout / password change
  passwordChangedAt DateTime?
  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

OTPVerification
  id, email, otp_hash (SHA-256), expiresAt, used Boolean
  attempts Int                             -- invalidated after 5 failures

OAuthSession
  id, code (unique), userId, expiresAt
  userData, expiresAt                      -- 2 minute TTL; deleted on exchange

PasswordResetToken
  id, email, token, expiresAt, used Boolean

AuditLog
  id, userId, organizationId
  action (CREATE | UPDATE | DELETE | LOGIN | EXPORT)
  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, organizationId (FK -> Organization)?
  subscribedAt

Running Locally

Prerequisites

  • Node.js 18 or higher
  • npm 9 or higher
  • A free Supabase project (PostgreSQL + Storage)
  • A Gmail account with App Password enabled
  • A Google Cloud project with OAuth 2.0 Web Client credentials

Step 1: Clone the repository

git clone https://github.com/oyedokunken/traqify.git
cd traqify

Step 2: Set up the backend

cd backend
npm install
cp .env.example .env

Open .env and fill in the required environment variables (DATABASE_URL, DIRECT_URL, SMTP_USER, SMTP_PASS, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, JWT_SECRET).

Push the schema to your database:

npx prisma db push

Start the backend dev server:

npm run dev

Step 3: Set up the frontend

cd frontend
npm install
cp .env.local.example .env.local

Fill in API_ORIGIN (the API's address) and PROXY_SHARED_SECRET (the same value as the API).

Start the frontend:

npm run dev