Traqify is a production-deployed, multi-tenant enterprise store management platform built for retail businesses that need structure, auditability, and role-based control across their operations.
It 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
- Sequence Diagrams
- Component Diagram
- Use Case Diagram
- State Diagrams
- Deployment Architecture
- Tech Stack
- Features
- Database Schema
- API Reference
- Folder Structure
- Running Locally
- Environment Variables
- Google OAuth Setup
- Deployment
- License
graph TB
subgraph Client["Client (Browser)"]
FE["Next.js 14 App Router<br/>TypeScript + Tailwind CSS<br/>Framer Motion + Recharts<br/>Axios with JWT interceptor"]
end
subgraph Server["API Server (Express.js)"]
MW["Helmet + Rate Limit<br/>JWT authenticate<br/>requireOrg + RBAC guards"]
CTRL["Controllers<br/>auth, org, products, inventory<br/>orders, customers, staff<br/>reports, audit, store<br/>payments, reviews, newsletter"]
MW --> CTRL
end
PG[("PostgreSQL<br/>Supabase")]
S3[("Supabase Storage<br/>products + avatars buckets")]
MAIL["Nodemailer<br/>Gmail SMTP<br/>Branded HTML templates"]
GOAUTH["Google OAuth 2.0"]
PAY["Paystack"]
FE -->|"REST JSON / Bearer JWT"| MW
CTRL -->|"Prisma ORM"| PG
CTRL --> S3
CTRL --> MAIL
FE -->|"OAuth redirect"| GOAUTH
GOAUTH -->|"code callback"| MW
FE -->|"Paystack.js inline"| PAY
PAY -->|"server-side verify"| CTRL
Email + OTP registration:
POST /api/auth/send-otp- generates a 6-digit OTP, stores it with a 10-minute TTL, and emails it (works before the user record exists)POST /api/auth/verify-email- validates the code, marks the email verified, redirects to/register?verifiedEmail=...POST /api/auth/register- creates the user withemailVerified: true, returns access and refresh tokensPOST /api/organizations- creates the organization and links the ownerPOST /api/auth/login- issues a fresh JWT that carriesorganizationId
Google OAuth 2.0:
GET /api/auth/google-redirect- redirects the browser to the Google consent screenGET /api/auth/google-callback?code=- exchanges the code, fetches the user profile, upserts the user record, stores tokens in a short-livedOAuthSessionrecord, and redirects the browser to/auth-callback?code=<opaque>GET /api/auth/oauth-exchange/:code- frontend calls this once with the opaque code; server returns the access token, refresh token, and user; the code is deleted immediately after use
Token refresh:
The Axios client intercepts every 401 response, silently calls POST /api/auth/refresh with the stored refresh token, saves the new access token, and retries the original request. This is transparent to all callers.
See Sequence Diagrams below for step-by-step visual flows.
sequenceDiagram
actor User
participant FE as Next.js Frontend
participant API as Express API
participant DB as PostgreSQL
participant MAIL as Gmail SMTP
User->>FE: Enter email on register page
FE->>API: POST /api/auth/send-otp
API->>DB: INSERT OTPVerification (6-digit, 10 min TTL)
API->>MAIL: Send OTP email
MAIL-->>User: OTP code in inbox
User->>FE: Submit OTP code
FE->>API: POST /api/auth/verify-email
API->>DB: Mark OTP used, flag emailVerified
API-->>FE: Redirect to /register?verifiedEmail=...
User->>FE: Fill name and password
FE->>API: POST /api/auth/register
API->>DB: INSERT User (emailVerified true)
API-->>FE: access token + refresh token
User->>FE: Fill organization details
FE->>API: POST /api/organizations
API->>DB: INSERT Organization, link ownerId
FE->>API: POST /api/auth/login
API-->>FE: New JWT with organizationId
FE-->>User: Redirect to /dashboard/slug/overview
sequenceDiagram
actor User
participant FE as Next.js Frontend
participant API as Express API
participant GOOGLE as Google OAuth 2.0
participant DB as PostgreSQL
User->>FE: Click Continue with Google
FE->>API: GET /api/auth/google-redirect
API-->>FE: 302 redirect to Google consent screen
FE->>GOOGLE: Browser follows redirect
User->>GOOGLE: Grant consent
GOOGLE->>API: GET /api/auth/google-callback?code=...
API->>GOOGLE: Exchange code for tokens
GOOGLE-->>API: Access token and user profile
API->>DB: Upsert User (create or update)
API->>DB: INSERT OAuthSession (code, tokens, 2 min TTL)
API-->>FE: 302 redirect to /auth-callback?code=<opaque>
FE->>API: GET /api/auth/oauth-exchange/:code
API->>DB: Read OAuthSession, delete record
API-->>FE: access token + refresh token + user
FE-->>User: Dashboard or create-organization page
sequenceDiagram
actor Staff
participant FE as Next.js Frontend
participant API as Express API
participant DB as PostgreSQL
participant MAIL as Gmail SMTP
Staff->>FE: Search products, add to cart
Staff->>FE: Attach customer (optional)
Staff->>FE: Click Create Order
FE->>API: POST /api/orders (CASHIER+ role)
API->>DB: INSERT Order + OrderItems
API->>DB: DECREMENT inventory quantities
API->>DB: INSERT AuditLog
opt Customer email is present
API->>MAIL: Send order confirmation to customer
end
API->>MAIL: Send admin notification to org owner
API-->>FE: 201 Created (status PENDING)
FE-->>Staff: Success toast, order listed
sequenceDiagram
actor Customer
participant FE as Store Frontend
participant PAY as Paystack
participant API as Express API
participant DB as PostgreSQL
participant MAIL as Gmail SMTP
Customer->>FE: Add to cart, proceed to checkout
Customer->>FE: Fill details and solve arithmetic CAPTCHA
FE->>PAY: Initialize Paystack inline popup
PAY-->>Customer: Payment form
Customer->>PAY: Submit card details
PAY-->>FE: onSuccess callback with reference
FE->>API: POST /api/store/slug/checkout with reference
API->>PAY: Verify transaction server-side
PAY-->>API: status SUCCESSFUL
API->>DB: INSERT Order (status APPROVED)
API->>DB: UPSERT Customer (source PURCHASE)
API->>DB: DECREMENT inventory
API->>DB: INSERT AuditLog
API->>MAIL: Send order confirmation email
API-->>FE: 201 Created
FE-->>Customer: Order success screen
sequenceDiagram
participant FE as Next.js Frontend
participant AXIOS as Axios Interceptor
participant API as Express API
FE->>API: Protected request with expired access token
API-->>AXIOS: 401 Unauthorized
AXIOS->>API: POST /api/auth/refresh (refresh token)
API-->>AXIOS: 200 new access token
AXIOS->>FE: Store new access token
AXIOS->>API: Retry original request with new token
API-->>FE: 200 OK with data
sequenceDiagram
actor Manager
actor NewStaff
participant FE as Next.js Frontend
participant API as Express API
participant DB as PostgreSQL
participant MAIL as Gmail SMTP
Manager->>FE: Enter email and role
FE->>API: POST /api/staff/invite (MANAGER+)
API->>DB: Check for existing PENDING invite
alt Already pending
API-->>FE: 409 Conflict
else No conflict
API->>DB: INSERT StaffInvite (64-char token, 72 h expiry)
API->>DB: INSERT AuditLog
API->>MAIL: Send invitation email
API-->>FE: 201 Created
end
MAIL-->>NewStaff: Invitation email with token link
NewStaff->>FE: Open /invite/token
FE->>API: GET /api/staff/invite/token
API->>DB: Validate token (not expired, not accepted)
API-->>FE: Invite details (org name, role)
NewStaff->>FE: Set password and submit
FE->>API: POST /api/auth/register (with invite token)
API->>DB: INSERT User, SET invite status ACCEPTED
API->>MAIL: Send welcome email
API-->>FE: access token + refresh token
FE-->>NewStaff: Dashboard with role-specific welcome modal
Frontend component architecture: pages, shared library, and component dependencies.
graph TB
subgraph AppDir["app/"]
ROOT["layout.tsx<br/>AuthProvider + Toaster"]
subgraph AuthGroup["(auth)/"]
LOGIN_P["login"]
REG_P["register"]
VERIFY_P["verify-email"]
FORGOT_P["forgot-password"]
RESET_P["reset-password"]
CALLBACK_P["auth-callback"]
end
CREATEORG_P["create-organization"]
INVITE_P["invite/[token]"]
subgraph StoreGroup["store/[slug]/"]
STORE_P["page (catalog + cart)"]
CHECKOUT_P["checkout"]
PROD_PAGE["products/[id]"]
end
subgraph DashGroup["dashboard/[slug]/"]
DASH_LAYOUT["layout<br/>(auth guard + sidebar)"]
OVERVIEW_P["overview"]
PRODUCTS_P["products"]
ORDERS_P["orders"]
CUSTOMERS_P["customers"]
STAFF_P["staff"]
REPORTS_P["reports"]
PAYMENTS_P["payments"]
AUDIT_P["audit-logs"]
SETTINGS_P["settings"]
end
end
subgraph LibDir["lib/"]
API_LIB["api.ts<br/>Axios instance + JWT interceptor"]
AUTH_CTX["auth-context.tsx<br/>useAuth hook"]
SB_LIB["supabase.ts"]
end
subgraph Comps["components/"]
SIDEBAR_C["sidebar.tsx"]
TOPBAR_C["topbar.tsx"]
PROD_MODAL["product-modal.tsx"]
ORDER_MODAL["create-order-modal.tsx"]
end
ROOT --> AUTH_CTX
DASH_LAYOUT --> AUTH_CTX
DASH_LAYOUT --> SIDEBAR_C
DASH_LAYOUT --> TOPBAR_C
PRODUCTS_P --> PROD_MODAL
ORDERS_P --> ORDER_MODAL
PROD_MODAL --> SB_LIB
AuthGroup --> API_LIB
DashGroup --> API_LIB
StoreGroup --> API_LIB
INVITE_P --> API_LIB
CREATEORG_P --> API_LIB
API_LIB --> AUTH_CTX
Role-based access: what each actor can do in the system.
flowchart LR
OWNER(["Owner"])
MANAGER(["Manager"])
CASHIER(["Cashier"])
AUDITOR(["Auditor"])
PUBLIC(["Customer (Public)"])
subgraph Auth["Authentication"]
UC_AUTH["Register / Login<br/>OTP Verification<br/>Password Reset<br/>Google OAuth"]
end
subgraph OwnerOnly["Owner Only"]
UC_ORG["Manage Org Settings and Logo"]
UC_PUB["Publish / Unpublish Store"]
UC_ROLE["Change Staff Roles"]
UC_REMOVE["Remove Staff Members"]
UC_DEL_ORD["Delete Orders"]
end
subgraph ManagerPlus["Manager and Above"]
UC_PRODUCTS["Create and Edit Products<br/>and Categories"]
UC_INV["Adjust Inventory"]
UC_APPROVE["Approve and Cancel Orders"]
UC_CUST["Manage Customers"]
UC_PAY["Record Payments"]
UC_STAFF_INV["Invite and Restrict Staff"]
UC_REPORTS["Download and Email Reports"]
end
subgraph CashierPlus["Cashier and Above"]
UC_POS["Create POS Orders"]
UC_ADD_CUST["Add Customers"]
UC_DASH["View Dashboard"]
end
subgraph AuditorOwner["Auditor and Owner"]
UC_AUDIT["View Audit Logs"]
UC_VIEW_PAY["View All Payments"]
end
subgraph PublicStore["Public Store"]
UC_BROWSE["Browse Product Catalog"]
UC_CHECKOUT["Paystack Checkout"]
UC_WISHLIST["Wishlist and Reminder Emails"]
UC_REVIEW["Submit Product Review"]
end
OWNER --> Auth & OwnerOnly & ManagerPlus & CashierPlus & AuditorOwner
MANAGER --> Auth & ManagerPlus & CashierPlus
CASHIER --> Auth & CashierPlus
AUDITOR --> Auth & AuditorOwner & CashierPlus
PUBLIC --> PublicStore
stateDiagram-v2
[*] --> PENDING : Order created (POS or store checkout)
PENDING --> APPROVED : Manager approves
PENDING --> CANCELLED : Manager cancels
APPROVED --> COMPLETED : Logistics marks delivered
APPROVED --> CANCELLED : Manager cancels
COMPLETED --> [*]
CANCELLED --> [*]
stateDiagram-v2
[*] --> PENDING : Payment recorded
PENDING --> COMPLETED : Payment confirmed
PENDING --> FAILED : Transaction fails
COMPLETED --> REFUNDED : Refund issued
COMPLETED --> [*]
FAILED --> [*]
REFUNDED --> [*]
stateDiagram-v2
[*] --> PENDING : Manager sends invite
PENDING --> ACCEPTED : Staff accepts within 72 h
PENDING --> EXPIRED : 72 hours elapse (lazy check on read)
PENDING --> [*] : Manager cancels (hard delete)
ACCEPTED --> [*]
EXPIRED --> [*]
stateDiagram-v2
[*] --> draft : Product created
draft --> published : Manager publishes
published --> draft : Manager unpublishes
published --> inactive : Soft deleted (isActive false)
draft --> inactive : Soft deleted (isActive false)
inactive --> [*]
graph TB
subgraph Internet["Internet"]
USER["Staff / Admin Browser"]
CUST["Store Customer Browser"]
end
subgraph Vercel["Vercel Edge Network"]
subgraph FE_DEPLOY["traqify.vercel.app"]
NEXT_SRV["Next.js 14<br/>Serverless Functions<br/>+ CDN Static Assets"]
end
subgraph BE_DEPLOY["traqify-api.vercel.app"]
EXPRESS_SRV["Express.js<br/>Serverless Function<br/>Node.js 18 runtime"]
end
end
subgraph SupabaseCloud["Supabase Cloud"]
PG["PostgreSQL 15<br/>Primary database<br/>(connection pooler)"]
SB_STORE["Object Storage<br/>products/ bucket<br/>avatars/ bucket"]
end
subgraph External["Third-Party Services"]
GMAIL["Gmail SMTP<br/>smtp.gmail.com:587"]
GOOGLE_ID["Google Identity<br/>accounts.google.com"]
PAYSTACK_API["Paystack API<br/>api.paystack.co"]
end
USER -->|HTTPS| NEXT_SRV
CUST -->|HTTPS| NEXT_SRV
NEXT_SRV -->|HTTPS REST| EXPRESS_SRV
EXPRESS_SRV -->|Prisma / PG wire protocol| PG
EXPRESS_SRV -->|Supabase SDK| SB_STORE
EXPRESS_SRV -->|SMTP TLS| GMAIL
NEXT_SRV -->|OAuth redirect| GOOGLE_ID
GOOGLE_ID -->|callback| EXPRESS_SRV
NEXT_SRV -->|Paystack.js CDN| PAYSTACK_API
PAYSTACK_API -->|verify call| EXPRESS_SRV
| 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 |
| Phone input | react-phone-number-input | International flag selector, default NG |
| PDF generation | PDFKit | Landscape A4 report PDFs |
| 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 |
- 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 (7-day default) + refresh token pair
- Axios interceptor catches 401 and silently refreshes the token
- Account lock by admin with notification email
- 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 (
productsbucket) - Downloadable file upload via dedicated
POST /api/products/upload-fileendpoint (any MIME type, max 4 MB) - Edit page at
/products/[id]/editwith 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)
- 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)
/dashboard/[slug]/logistics— OWNER/MANAGER only- Shows all APPROVED orders as cards with customer contact info and item list
- "Mark delivered" button sets order to COMPLETED
- Full customer records (name, email, phone, address)
- View-detail modal: click any customer row (or the eye icon) to see contact info, source, date added, and up to 5 recent orders
- Purchase history per customer
- Search by name or email
- Email invitation with 3-day (72-hour) secure token; cryptographically random 64-char hex token
- Role assignment on invite: MANAGER, CASHIER, or AUDITOR; OWNER is protected and cannot be assigned via invite
- Pending invites visible in the Staff table with amber badge; re-inviting a pending address is blocked at the API level (409 Conflict)
- Cancel invite action removes the pending invite and logs the cancellation in the audit trail
- Lazy expiry: when invites are fetched, newly-expired entries are auto-marked
EXPIREDand logged - RESTRICT: toggle staff access on/off; sends
accountRestrictedEmailTemplatenotification - RESET-PASSWORD: generates temporary password; sends
passwordResetByAdminEmailTemplatenotification - DELETE: removes staff from the organization; sends
staffRemovedEmailTemplatenotification; all three actions produce audit log entries - Invite accept page: branded card UI with left-aligned content and eye icons on both password fields
- Role-specific welcome modal on first login: invited members see their role name and access scope; org owners see workspace setup instructions
- Account restriction / unrestriction with email notification; OWNER account cannot be restricted
- Admin-initiated password reset; OWNER password cannot be reset via staff tools
- 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-containimages - 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 Paystackbadge - Paystack payment: inline popup (no redirect), backend verification before order creation; successful payments create orders as
APPROVED - Confirmation email to customer on order placement
- 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)
/dashboard/[slug]/newsletter— OWNER/MANAGER only- Summary stats: total subscribers, last 7 days, this month
- Searchable subscriber table with status badges
- CSV export of all subscriber records
- Refresh button for live updates
- 7 report types: Revenue, Products, Orders, Customers, Inventory, Staff, Payments (new)
- Date-range filtering with empty-report guard (400 if no data in range)
- PDF download: landscape A4 via PDFKit; branded header (Traqify logo + org name + contact); coloured strip with report title and period; footer with record count + generation timestamp
- Email report: sends PDF as attachment via Nodemailer to the requesting user's email
- All report types accessible to AUDITOR and above; download/email restricted to OWNER/MANAGER
- PDF tagline: "Enterprise Store Management System"
- 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
- Record and track payments against orders with status flow:
PENDING->COMPLETED/FAILED/REFUNDED - Payment fields: amount (NGN), method (cash, transfer, Paystack, etc.), reference, notes, order link
- Dashboard summary: total received, pending, failed, refunded — all with count badges
- Status filter; records paginated
- Inline update: click a payment to change its status with confirmation
- Fully audited: all payment creates and updates are logged
- Full customer profiles: name, email, phone, address
- Source tracking:
MANUAL(staff-created) vsPURCHASE(auto-created at checkout) - Per-customer order history
- Search by name or email; add customer via modal overlay (no page navigation)
Branded HTML templates for:
- OTP email verification
- Password reset
- Staff invitation
- Welcome email after verification
- Order confirmation (to customer, includes org contact details)
- Account restriction notice
- Admin-initiated password reset
- Wishlist reminder (4 waves: 30min, 2hr, 1 day, 3 days after wishlist creation)
- Store published/unpublished notification
- Report delivery
- OTP email verification — every new account must verify their email before gaining access; OTPs are 6-digit codes generated with
crypto.randomInt, SHA-256 hashed before storage, expire after 10 minutes, and are invalidated after 5 failed attempts - bcrypt password hashing — cost factor 12; no plain-text passwords stored anywhere
- JWT access token — 7-day default lifetime; signed with
JWT_SECRET; carriesuserId,email,organizationId,role, andtokenVersion - JWT refresh token — separate secret (
JWT_REFRESH_SECRET); used by Axios interceptor to silently re-issue access tokens on 401; both token types are validated againstUser.tokenVersionon every use - Token version invalidation —
tokenVersionis incremented in the database on logout and on password change; all outstanding tokens for a user become invalid immediately, regardless of their remaining TTL - Google OAuth 2.0 — redirect-based flow; after the callback the backend stores tokens in a short-lived
OAuthSessionrecord and redirects with only an opaque one-time code; tokens are never in the URL - Invited user registration block — users with a pending staff invitation cannot create new accounts via registration or Google OAuth; they must use their invitation link or sign in
- Startup validation — the API process throws before accepting any traffic if any of the five required environment variables (
JWT_SECRET,JWT_REFRESH_SECRET,DATABASE_URL,PAYSTACK_SECRET_KEY,FRONTEND_URL) are missing
authenticatemiddleware — verifies the JWT on every protected route; attachesreq.userrequireOrgmiddleware — enforces thatreq.user.organizationIdis 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
organizationIdin thewhereclause; no query trusts the frontend to scope data
Two tiers applied via express-rate-limit:
- Global: 200 requests per 15 minutes per IP across all routes
- Auth: 20 requests per 15 minutes per IP on all
/api/auth/*routes
helmetis applied globally: setsX-Content-Type-Options,X-Frame-Options,Strict-Transport-Security,X-XSS-Protection, and Content Security Policy headers
- Only
image/jpeg,image/png,image/webpMIME types accepted (validated server-side) - Max file size: 5 MB (enforced by Multer before the handler runs)
- Files stored in Supabase Storage (not the server filesystem); server never persists files to disk
- Requires the current password to be verified before accepting a new one
- New password cannot contain the user's email address (or email local part)
- New password cannot contain any segment of the user's display name (>2 chars)
- Invite tokens are 64-character cryptographically random hex strings (
crypto.randomBytes(32)) - Tokens expire after 3 days (72 hours)
- Re-inviting a pending email address is blocked at the API level (409 Conflict)
- OWNER role cannot be assigned via invite; only MANAGER, CASHIER, AUDITOR
- Invite cancellation hard-deletes the token from the database; old links become immediately invalid
erDiagram
Organization {
string id PK
string name
string slug UK
boolean storePublished
}
User {
string id PK
string email UK
string role
boolean isActive
int tokenVersion
datetime passwordChangedAt
string organizationId FK
string invitedById FK
}
ProductCategory {
string id PK
string name
string slug
string organizationId FK
}
Product {
string id PK
string name
string sku
float price
string productType
string status
string organizationId FK
string categoryId FK
}
ProductVariant {
string id PK
string name
string value
float price
string productId FK
}
Inventory {
string id PK
int quantity
int lowStockAlert
int reorderPoint
string productId FK
}
Customer {
string id PK
string name
string email
string source
string organizationId FK
}
Order {
string id PK
string orderNumber UK
string status
float totalAmount
string organizationId FK
string customerId FK
string createdById FK
}
OrderItem {
string id PK
int quantity
float unitPrice
float subtotal
string orderId FK
string productId FK
}
Payment {
string id PK
float amount
string currency
string status
string method
string organizationId FK
string orderId FK
}
StaffInvite {
string id PK
string email
string role
string token UK
string status
datetime expiresAt
string organizationId FK
}
AuditLog {
string id PK
string action
string entity
string entityId
boolean isRead
string userId FK
string organizationId FK
}
Review {
string id PK
int rating
string status
string productId FK
string orderId FK
string organizationId FK
}
Wishlist {
string id PK
string sessionId
string email
string organizationId FK
}
OTPVerification {
string id PK
string email
string otp_hash
datetime expiresAt
boolean used
int attempts
}
PasswordResetToken {
string id PK
string email
string token UK
datetime expiresAt
boolean used
}
OAuthSession {
string id PK
string code UK
string token
string refreshToken
string userData
datetime expiresAt
}
NewsletterSubscriber {
string id PK
string email
string organizationId FK
}
Organization ||--o{ User : "employs"
Organization ||--o{ Product : "owns"
Organization ||--o{ Order : "processes"
Organization ||--o{ Customer : "tracks"
Organization ||--o{ StaffInvite : "sends"
Organization ||--o{ AuditLog : "generates"
Organization ||--o{ Payment : "records"
Organization ||--o{ Wishlist : "receives"
Organization ||--o{ ProductCategory : "manages"
Organization ||--o{ Review : "receives"
Organization ||--o{ NewsletterSubscriber : "collects (optional)"
User ||--o{ Order : "creates"
User ||--o{ AuditLog : "authors"
User }o--o| User : "invited by"
ProductCategory ||--o{ Product : "groups"
Product ||--|| Inventory : "tracked by"
Product ||--o{ ProductVariant : "has variants"
Product ||--o{ OrderItem : "appears in"
Product ||--o{ Review : "receives"
Order ||--o{ OrderItem : "contains"
Order ||--o{ Payment : "has"
Order ||--o{ Review : "generates"
Customer ||--o{ Order : "places"
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 -- v1.9.0+
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
All protected endpoints require Authorization: Bearer <token>.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /register |
Public | Register with name, email, password |
| POST | /verify-email |
Public | Verify OTP code |
| POST | /send-otp |
Public | Resend OTP |
| POST | /login |
Public | Email + password login |
| POST | /refresh |
Public | Refresh access token |
| POST | /logout |
Auth | Increment tokenVersion; invalidates all outstanding tokens |
| POST | /forgot-password |
Public | Send reset link (same response whether account exists or not) |
| POST | /reset-password |
Public | Set new password via token |
| GET | /google-redirect |
Public | Redirect to Google consent screen |
| GET | /google-callback |
Public | Handle OAuth callback; stores tokens in OAuthSession; redirects with one-time code |
| GET | /oauth-exchange/:code |
Public | Exchange one-time code for access token, refresh token, and user |
| GET | /me |
Auth | Get current user profile |
| PATCH | /me |
Auth | Update name / avatarUrl |
| POST | /change-password |
Auth | Change password; increments tokenVersion |
| POST | /upload-avatar |
Auth | Upload avatar to Supabase (avatars bucket) |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | / |
Auth | Create organization |
| GET | /:slug |
Auth + OrgMember | Get organization details |
| PATCH | /:slug |
OWNER only | Update org settings |
| POST | /:slug/upload-logo |
OWNER only | Upload logo to Supabase (avatars bucket) |
| GET | /:slug/store |
Public | Public store data (org + products + categories) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Auth | List categories for org |
| POST | / |
MANAGER+ | Create category |
| PATCH | /:id |
MANAGER+ | Update category |
| DELETE | /:id |
OWNER only | Delete category |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /:slug |
Public | Public store product list |
| POST | /:slug/checkout |
Public | Guest checkout — creates order + sends email |
| POST | /:slug/wishlist |
Public | Sync wishlist (sessionId + productIds + optional email) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /:type/pdf |
OWNER/MANAGER | Stream PDF report |
| POST | /:type/email |
OWNER/MANAGER | Email PDF as attachment |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Auth + OrgMember | List products |
| POST | / |
MANAGER+ | Create product |
| PATCH | /:id |
MANAGER+ | Update product |
| DELETE | /:id |
MANAGER+ | Soft-delete product |
| POST | /upload-image |
MANAGER+ | Upload product image to Supabase (JPEG/PNG/WebP, max 5 MB) |
| POST | /upload-file |
MANAGER+ | Upload downloadable product file (PDF, ZIP, etc., max 4 MB) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Auth | List inventory items |
| PATCH | /:productId |
MANAGER+ | Adjust stock / alert threshold |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Auth | List orders (paginated, filterable by status) |
| POST | / |
CASHIER+ | Create order |
| GET | /:id |
Auth | Get order detail |
| PATCH | /:id/status |
MANAGER+ | Update order status (PENDING/APPROVED/COMPLETED/CANCELLED) |
| DELETE | /:id |
OWNER only | Delete order |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Auth | List customers |
| POST | / |
CASHIER+ | Create customer |
| GET | /:id |
Auth | Get customer + order history |
| PATCH | /:id |
MANAGER+ | Update customer |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
AUDITOR+ | List staff members |
| GET | /invites |
MANAGER+ | List all pending/expired invites |
| POST | /invite |
MANAGER+ | Send staff invitation (3-day token) |
| DELETE | /invites/:inviteId |
MANAGER+ | Cancel a pending invite |
| GET | /invite/:token |
Public | Fetch invite details (for accept page) |
| PATCH | /:userId/role |
OWNER only | Change a staff member's role |
| PATCH | /:userId/access |
MANAGER+ | Toggle account restriction |
| DELETE | /:userId |
OWNER only | Remove staff member from org |
| POST | /:userId/reset-password |
MANAGER+ | Admin-initiated password reset email |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
AUDITOR+ | List payments (filterable by status) |
| GET | /:id |
AUDITOR+ | Get single payment record |
| POST | / |
MANAGER+ | Record a payment |
| PATCH | /:id |
MANAGER+ | Update payment status |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | / |
Public | Submit a review (post-purchase) |
| GET | /product/:productId |
Public | List approved reviews for a product |
| GET | / |
MANAGER+ | Dashboard: list all reviews for org |
| PATCH | /:id/moderate |
MANAGER+ | Approve or reject a review |
| DELETE | /:id |
MANAGER+ | Delete a review |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /overview |
AUDITOR+ | KPI summary (revenue, orders, products, customers, low-stock) |
| GET | /revenue-chart |
AUDITOR+ | Daily revenue for last N days |
| GET | /customer-chart |
AUDITOR+ | Daily new customers for last N days |
| GET | /top-products |
AUDITOR+ | Top products by revenue |
| GET | /sales |
AUDITOR+ | Date-range sales tabular data |
| GET | /download |
MANAGER+ | Stream PDF report (type + date range) |
| POST | /email |
MANAGER+ | Email PDF report as attachment |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
AUDITOR+ | Paginated audit log with search |
| GET | /unread-count |
AUDITOR+ | Count of unread log entries |
| GET | /:id |
AUDITOR+ | Single log entry (marks it read) |
| PATCH | /mark-read |
AUDITOR+ | Bulk mark read/unread (ids[], all, isRead) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /:slug |
Public | Store info + product catalog |
| POST | /:slug/checkout |
Public | Place guest order + Paystack verification |
| POST | /:slug/wishlist |
Public | Sync wishlist (sessionId + productIds + email) |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /subscribe |
Public | Subscribe to newsletter |
| GET | /subscribers |
OWNER/MANAGER | List newsletter subscribers scoped to the requesting org |
| DELETE | /:id |
OWNER/MANAGER | Remove a subscriber (org-scoped) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /ping |
CRON_SECRET bearer | Keep-alive ping to prevent Supabase database pausing |
traqify/
+-- README.md
+-- LICENSE
+-- CHANGELOG.md
+-- WIKI.md
+-- .gitignore
|
+-- backend/
| +-- DEPLOY.md
| +-- vercel.json
| +-- .env.example
| +-- package.json
| +-- tsconfig.json
| +-- prisma/
| | +-- schema.prisma # All 11 models
| +-- src/
| +-- index.ts # Express app bootstrap, CORS, rate-limit, route mounts
| +-- config/
| | +-- database.ts # Prisma client singleton
| | +-- email.ts # Nodemailer transporter
| | +-- supabase.ts # Supabase client (storage access)
| +-- middleware/
| | +-- auth.middleware.ts # JWT verify, attach user to req
| | +-- rbac.middleware.ts # Role-based access guards
| | +-- error.middleware.ts # Global error handler
| +-- routes/
| | +-- auth.routes.ts
| | +-- org.routes.ts
| | +-- product.routes.ts
| | +-- inventory.routes.ts
| | +-- order.routes.ts
| | +-- customer.routes.ts
| | +-- staff.routes.ts
| | +-- report.routes.ts
| | +-- audit.routes.ts
| | +-- store.routes.ts
| | +-- newsletter.routes.ts
| +-- controllers/ # One controller per route module
| +-- emails/
| | +-- templates.ts # Branded HTML email templates
| +-- utils/
| +-- jwt.ts # sign / verify / refresh
| +-- otp.ts # OTP create/verify, password-reset tokens
| +-- slug.ts # Unique org slug generator
| +-- audit.ts # createAuditLog() helper
| +-- validators.ts # All Zod schemas
|
+-- frontend/
+-- DEPLOY.md
+-- .env.local.example
+-- next.config.mjs
+-- tailwind.config.ts
+-- package.json
+-- tsconfig.json
+-- public/
| +-- img-hero.jpg
| +-- google-logo.png
| +-- payments.png
+-- app/
| +-- layout.tsx # Root layout: AuthProvider, Toaster, ScrollToTop
| +-- page.tsx # Landing page (assembly of all sections)
| +-- globals.css # Tailwind base + Jost font + CSS keyframes
| +-- not-found.tsx
| +-- (auth)/
| | +-- layout.tsx
| | +-- login/page.tsx
| | +-- register/page.tsx # 3-step registration with password strength
| | +-- verify-email/page.tsx # 6-digit OTP input
| | +-- forgot-password/page.tsx
| | +-- reset-password/page.tsx
| | +-- auth-callback/page.tsx # Google OAuth landing
| +-- (public)/
| | +-- privacy/page.tsx
| | +-- terms/page.tsx
| +-- create-organization/page.tsx # Post-registration org setup
| +-- invite/[token]/page.tsx # Accept staff invitation
| +-- store/[slug]/page.tsx # Public catalog + cart + dual price range slider
| +-- store/[slug]/checkout/page.tsx # Paystack checkout + CAPTCHA + breadcrumb
| +-- store/[slug]/products/[id]/page.tsx # Individual product page with upsells
| +-- dashboard/[slug]/
| +-- layout.tsx # Auth guard + sidebar layout
| +-- overview/page.tsx # Role-based Recharts dashboard
| +-- products/page.tsx
| +-- products/new/page.tsx # Full-page product creation with Zod validation
| +-- inventory/page.tsx
| +-- orders/page.tsx
| +-- customers/page.tsx
| +-- staff/page.tsx
| +-- reports/page.tsx
| +-- audit-logs/page.tsx
| +-- settings/page.tsx # Org tab: industry/size/description/phone/address/website
| +-- newsletter/page.tsx # Subscriber stats + searchable table + CSV export
+-- components/
| +-- landing/
| | +-- hero.tsx # 2-col hero, image first on mobile
| | +-- logo-scroller.tsx # Infinite scroll on black bg
| | +-- about.tsx # 2-col with animated dashboard mockup
| | +-- features.tsx # 8-card feature grid
| | +-- stats.tsx # Animated counters on dark bg
| | +-- how-it-works.tsx # Framer Motion timeline
| | +-- testimonials.tsx # Dual scrolling rows
| | +-- everything.tsx # Role comparison cards
| | +-- faq.tsx # 2-column accordion
| | +-- cta.tsx # Red CTA with gradient glow
| +-- shared/
| | +-- navbar.tsx # Scroll spy + animated red underline
| | +-- footer.tsx # Newsletter, socials, legal links
| | +-- logo.tsx
| | +-- scroll-to-top.tsx
| | +-- error-modal.tsx
| +-- dashboard/
| | +-- sidebar.tsx
| | +-- topbar.tsx
| | +-- product-modal.tsx # Create/edit with image upload
| | +-- create-order-modal.tsx
| | +-- order-detail-modal.tsx
| +-- ui/ # Button, Input, Label, Card, Badge, Toast
+-- lib/
| +-- api.ts # Axios instance with JWT + silent token refresh
| +-- auth-context.tsx # AuthProvider, useAuth() hook
| +-- supabase.ts # Supabase browser client
| +-- utils.ts # cn(), formatCurrency(), INDUSTRY_OPTIONS
+-- hooks/
+-- use-toast.ts
- 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
git clone https://github.com/oyedokunken/traqify.git
cd traqifycd backend
npm install
cp .env.example .envOpen .env and fill in the following (see Environment Variables for the full list):
DATABASE_URLandDIRECT_URLfrom Supabase > Project Settings > Database > Connection stringSMTP_USERandSMTP_PASS(your Gmail address and 16-char App Password)GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETfrom Google Cloud ConsoleJWT_SECRETandJWT_REFRESH_SECRET(any two long random strings, keep them secret)
Push the schema to your database:
npx prisma db pushStart the backend dev server:
npm run devThe API will be available at http://localhost:5000 (local) or https://traqify-api.vercel.app/ (production). You can verify it is running:
curl https://traqify-api.vercel.app/health
# { "status": "ok", ... }Open a new terminal:
cd frontend
npm install
cp .env.local.example .env.localFill in:
NEXT_PUBLIC_API_URL=http://localhost:5000
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-keyStart the frontend:
npm run devThe app will be available at http://localhost:3000 (local) or https://traqify.vercel.app/ (production).
- Open http://localhost:3000/register
- Fill in your name, email, and password
- Enter your organization details
- Check your email for the OTP code
- Verify your email
- Sign in at http://localhost:3000/login
- You will be redirected to set up your organization at http://localhost:3000/create-organization
| URL | Description |
|---|---|
| https://traqify.vercel.app | Landing page (production) |
| https://traqify.vercel.app/register | Create an account |
| https://traqify.vercel.app/login | Sign in |
| https://traqify.vercel.app/create-organization | Org setup (post-registration) |
| https://traqify.vercel.app/dashboard/[slug]/overview | Dashboard home |
| https://traqify.vercel.app/dashboard/[slug]/products | Products |
| https://traqify.vercel.app/dashboard/[slug]/orders | Orders |
| https://traqify.vercel.app/dashboard/[slug]/staff | Staff management |
| https://traqify.vercel.app/store/[slug] | Public store page |
| http://localhost:3000 | Landing page (local) |
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Supabase pooled connection string |
DIRECT_URL |
Yes | Supabase direct connection string (for migrations) |
PORT |
No | API port (default: 5000) |
NODE_ENV |
Yes | development or production |
API_URL |
Yes | Full base URL of this API server |
FRONTEND_URL |
Yes | Full base URL of the frontend (for CORS + email links) |
JWT_SECRET |
Yes | Long random string for signing access tokens |
JWT_REFRESH_SECRET |
Yes | Different long random string for refresh tokens |
JWT_EXPIRES_IN |
No | Access token lifetime (default: 7d) |
SUPABASE_URL |
Yes | Your Supabase project URL |
SUPABASE_ANON_KEY |
Yes | Supabase anonymous/public key |
SUPABASE_SERVICE_ROLE_KEY |
Yes | Supabase service role key (for storage writes) |
SMTP_HOST |
Yes | SMTP host (e.g. smtp.gmail.com) |
SMTP_PORT |
Yes | SMTP port (e.g. 587) |
SMTP_USER |
Yes | SMTP username (your Gmail address) |
SMTP_PASS |
Yes | Gmail App Password (16 characters) |
SMTP_FROM |
Yes | From header, e.g. "Traqify <you@gmail.com>" |
GOOGLE_CLIENT_ID |
Yes | OAuth 2.0 client ID from Google Console |
GOOGLE_CLIENT_SECRET |
Yes | OAuth 2.0 client secret |
PAYSTACK_SECRET_KEY |
Yes | Paystack secret key (verify payments server-side) |
PAYSTACK_PUBLIC_KEY |
No | Paystack public key (also set in frontend env) |
CRON_SECRET |
Yes (production) | Secret Vercel injects as Authorization: Bearer on cron invocations; must not contain whitespace |
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_API_URL |
Yes | Backend API base URL |
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Supabase anon key |
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY |
Yes | Paystack public key (used by inline popup) |
This step is required to enable "Continue with Google" on the login and register pages.
- Go to Google Cloud Console and select your project
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth 2.0 Client IDs > Web application
- Under Authorized JavaScript Origins, add:
http://localhost:3000
- Under Authorized Redirect URIs, add:
https://traqify-api.vercel.app/api/auth/google-callback
- Save and copy the Client ID and Client Secret into
backend/.env - Wait up to 5 minutes for changes to propagate
For production, also add:
https://traqify.vercel.appto JavaScript Originshttps://traqify-api.vercel.app/api/auth/google-callbackto Redirect URIs
See the step-by-step guides:
- frontend/DEPLOY.md for deploying the Next.js app to Vercel
- backend/DEPLOY.md for deploying the Express API to Vercel
Traqify is purpose-built with enterprise patterns. Here is an honest assessment of where it stands and where it can grow.
| Concern | Implementation |
|---|---|
| Multi-tenancy | All queries scoped by organizationId at the ORM layer; no cross-org data leakage possible |
| RBAC | 4-tier role hierarchy enforced via dedicated Express middleware per route; not just UI-level hiding |
| Audit trail | Every state-changing action generates an AuditLog row with actor, entity, IP, user agent, and timestamp |
| Auth security | bcrypt (cost 12) + OTP verification + JWT access/refresh pair + Google OAuth; rate-limited auth endpoints |
| HTTP hardening | Helmet headers + per-route rate limiting via express-rate-limit |
| File security | MIME-type validation + size caps + Supabase Storage (not local disk) |
| Type safety | 100% TypeScript (strict) across frontend and backend; Prisma ORM for compile-time query safety |
| Email reliability | Nodemailer with branded templates; transactional emails for OTP, invites, orders, reports, restrictions |
| Structured reports | Downloadable and emailable PDF reports (7 types) with org branding |
| Public storefront | Separate customer-facing catalog with Paystack checkout, wishlist reminders, and product reviews |
| Staff lifecycle | Invite → accept → restrict → remove; full audit log at every step |
| Area | Current State | Enterprise Upgrade Path |
|---|---|---|
| Password history | Not enforced (current vs new password checked, but no history log) | Add PasswordHistory model; hash-compare last N passwords |
| MFA / 2FA | Not implemented | TOTP (Google Authenticator) or SMS via Twilio |
| Session management | tokenVersion in every JWT invalidates all tokens simultaneously on logout or password change; selective per-device revocation is not supported |
Add a per-session token store (Redis) if selective revocation is needed |
| Background jobs | Wishlist reminders use a simulated delay model; invite expiry is lazy (on-read) | Proper cron/queue (BullMQ + Redis) for scheduled tasks |
| Horizontal scaling | Single Express instance per Vercel function | Stateless architecture already; add Redis for shared state |
| Webhook events | No outbound webhooks | Add a WebhookEndpoint model + POST delivery queue |
| API versioning | No /v1/ prefix |
Add version prefix before any public API consumers |
| Unit / integration tests | Not yet written | Jest + Supertest for backend; Playwright for E2E |
| Database migrations | Uses prisma db push (schema-level diff) |
Switch to prisma migrate deploy for CI/CD controlled migrations |
| Observability | console.error logging only |
Add structured logging (Pino) + error tracking (Sentry) |
MIT. See LICENSE.
Oyedokun Kehinde Software Engineer
- Email: oyedokunken@gmail.com
- Portfolio: oyedokun.dev
- GitHub: @oyedokunken