Malamal hero
01SaaS Web Application

Malamal

Inventory & Order Management for Small Businesses

A multi-tenant inventory and order management system built for small businesses across five industry verticals, with role-based access and plan-gated features.

Multi-Tenant SaaSPWARole-Based AccessTiered SubscriptionsNiche-Adaptive
01 // Overview

About the Project

What it is

Malamal is a multi-tenant web application for inventory, order, and stock management. Each business gets its own isolated workspace with a separate product catalog, customer list, team, and subscription.

Who it's for

Small business owners in retail, pharmacy, restaurants, fashion, and hardware — and the managers they invite to help run day-to-day operations. A Super Admin account governs workspace access and approvals.

Why it exists

Built to give small shop owners a single place to manage stock levels, process sales via a POS screen, monitor analytics, and receive alerts — without needing to switch between spreadsheets or separate tools.

02 // My Role

What I Built

Backend Architecture

Designed and built the entire NestJS backend: module structure, multi-tenant data isolation, auth system, BullMQ email queue, cron notification jobs, subscription and quota enforcement, and Swagger API documentation.

Frontend Development

Built the Next.js frontend including the POS page, dashboard, analytics, onboarding wizard, subscription billing UI, PWA service worker, and bilingual (English and Bengali) i18n support.

Data Modeling

Designed the Prisma schema across seven files covering workspaces, users, products, orders, stock movements, expiry alerts, restock queues, subscriptions, and activity logs with composite indexes for multi-tenant queries.

Infrastructure & Security

Configured Redis-backed rate limiting and session storage, JWT access and refresh token rotation with blacklisting, CSRF protection on mutating requests, and HTTP security headers in Next.js.

03 // Key Capabilities

What It Can Do

Niche-adaptive product forms: Pharma shows expiry date, batch number, and controlled substance flag; Fashion shows size and color variants; Hardware shows part numbers and compatibility; Restaurant shows ingredient and unit fields

Point-of-sale screen with barcode camera scanning via html5-qrcode, product search, customer lookup, cart management, tax calculation, and order submission

Subscription tier enforcement: plan quotas (max products, max orders per month, max managers) checked at write time; feature gates checked at query time; plans cached in Redis with TTL

Web Push notifications (VAPID) and email notifications for low-stock events and order status changes, controlled by per-workspace and per-user preferences

04 // Under the Hood

Engineering Deep Dive

Click any card to read the full technical detail.

05 // Engineering Decisions

Challenges & Trade-offs

The real decisions behind the architecture.

⚠ Challenge

How to isolate tenant data without separate databases per workspace

→ Decision

Every domain model carries a tenantId foreign key pointing to the Workspace. All queries filter by tenantId, enforced at the service layer. Composite indexes on (tenantId, createdAt), (tenantId, status), and similar pairs make per-tenant queries efficient.

⇄ Trade-off

Simpler to operate than per-tenant databases, but requires disciplined filtering at the service layer. A missed tenantId filter would leak cross-tenant data.

⚠ Challenge

How to handle niche-specific product fields without separate tables per business type

→ Decision

All possible niche fields (expiryDate, batchNumber, controlledSubstance, variantType, availableSizes, availableColors, isIngredient, unit, partNumber, compatibleWith) live on a single Product table. Which fields appear in the UI is determined by the workspace's featureFlags, not the database schema.

⇄ Trade-off

Keeps the schema simple and queries uniform, but the Product table has many nullable columns that most business types never use.

⚠ Challenge

How to deliver low-stock and expiry alerts without a real-time event bus

→ Decision

NotificationService uses NestJS @Cron decorators to run an hourly job checking low-stock conditions and workspace-configured reminder times, and a daily midnight cron to refresh expiry alert severities. Individual product events also call notifyLowStockIfDue synchronously on the write path.

⇄ Trade-off

Avoids the complexity of a message broker, but the hourly cron means scheduled alerts may fire up to one hour after the configured workspace time.

⚠ Challenge

Building a PWA that works reliably across both Android and iOS install flows

→ Solution

Implemented a usePWAInstall hook that listens for the beforeinstallprompt event on Android and detects iOS Safari via user agent to show manual share-sheet instructions. A custom Next.js Webpack plugin stamps the BUILD_ID into sw.js on each production build so the service worker cache version updates automatically on every deployment.

✓ Result

PWA install is surfaced on both platforms with platform-appropriate UX, and the service worker cache is invalidated on each new build without manual intervention.

⚠ Challenge

Supporting admin impersonation without compromising the standard JWT auth flow

→ Solution

When a Super Admin impersonates an owner, a new token pair is issued with an impersonatingFromId claim embedded in the payload. The JwtAuthGuard reads this claim and attaches it to the session user object. A dedicated exit-impersonation endpoint restores the original Super Admin session. All impersonation events are written to the activity log.

✓ Result

Impersonation works within the existing cookie-based auth flow; the impersonating user's identity is always traceable in the audit trail.

⚠ Challenge

Keeping subscription quota checks from adding N+1 database queries on every write

→ Solution

SubscriptionService.getWorkspacePlanCached fetches and caches the workspace's plan snapshot in Redis on the first call, keyed by tenantId. Subsequent quota checks within the same TTL window skip the database entirely. The cache is invalidated when a workspace changes its plan.

✓ Result

Quota checks add at most one Redis GET per request rather than a fresh database query on every product or order creation.

06 // Architecture

System Design

Multi-tenant NestJS monolith with workspace isolation enforced at the Prisma query layer via tenantId on every domain model
Prisma schema split across seven files (auth, history, profile, subscription, activityLog, enums, base) using the multi-file schema feature, targeting PostgreSQL
Redis used for three independent concerns: session and refresh token storage, access token blacklisting, and subscription plan caching — all via a single shared ioredis client injected globally
BullMQ email processor handles outbound email jobs asynchronously; EmailService reads from file-based HTML templates with placeholder replacement via a regex loop
Next.js 16 frontend in standalone output mode with a custom Webpack plugin that stamps the Next.js BUILD_ID into the service worker on each production build to bust the PWA cache
07 // Performance

What Makes It Fast

Subscription

Subscription plan data cached in Redis with a configurable TTL to avoid a database read on every quota enforcement check

Prisma

Prisma schema has composite indexes on every high-traffic query pattern: (tenantId, createdAt), (tenantId, status, createdAt), (tenantId, barcode), (tenantId, expiryDate), (productId, createdAt)

Next.js

Next.js configured with AVIF and WebP image formats, modular lucide-react tree-shaking via modularizeImports, and 1-year immutable cache headers on _next/static assets in production

Redis

Redis client configured with exponential backoff retry strategy, reconnect-on-error for READONLY, ECONNRESET, and ETIMEDOUT errors, and a 5-second command timeout to avoid hanging requests

08 // Security

Security Model

httpOnly cookie-based JWT auth with separate access and refresh tokens; access tokens blacklisted in Redis by jti on logout

httpOnly cookie-based JWT auth with separate access and refresh tokens; access tokens blacklisted in Redis by jti on logout

CSRF protection

CSRF protection: a non-httpOnly CSRF cookie must match the X-CSRF-Token request header on all POST, PATCH, PUT, and DELETE endpoints

Redis-backed rate limiting applied globally via NestJS ThrottlerModule; separate rate limit buckets for signup, login, password reset, and OTP resend

checked independently by email and IP address

Password policy enforced on signup and password change via regex before bcrypt hashing with a cost factor of 12

Password policy enforced on signup and password change via regex before bcrypt hashing with a cost factor of 12

09 // Deployment

How It Ships

Next.js built in standalone output mode for containerized deployment; start script runs on port 9001
NestJS API versioned under /api/v1 with global validation pipe (whitelist: true, forbidNonWhitelisted: true, transform: true); Swagger UI served at /docs
HTTP security response headers configured in next.config.ts for all routes including X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and Cross-Origin policies
Bundle analyzer available via ANALYZE=true environment variable during build for inspecting client chunk sizes
10 // Gallery

Screenshots

Architecture Diagrams
11 // Resources

Links & Assets

🌐 Live Demo ↗
01 // HERO