A full-stack inventory management and enterprise resource planning system built with Next.js 16, Prisma 7, and PostgreSQL. The application provides warehouse operations, stock tracking with double-entry ledger accounting, batch management, role-based access control, and low-stock alerting.
- Tech Stack
- Architecture Overview
- Getting Started
- Environment Variables
- Database Setup
- Default Credentials
- Project Structure
- Features
- Database Schema
- Authentication & Authorization
- RBAC Permission Matrix
- API Reference
- Navigation Structure
- Build Process & Design Decisions
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.2.1 |
| Language | TypeScript | 5.x |
| UI Library | React | 19.2.4 |
| Styling | Tailwind CSS | 4.x |
| Component Library | shadcn/ui + Radix UI | latest |
| Icons | Lucide React | 1.6.x |
| ORM | Prisma | 7.5.0 |
| Database | PostgreSQL | 14+ |
| DB Driver | @prisma/adapter-pg (pg) | 7.5.0 |
| Authentication | NextAuth.js v5 (beta) | 5.0.0-beta.30 |
| Validation | Zod | 4.x |
| Password Hashing | bcryptjs | 3.x |
┌─────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ React 19 · Tailwind v4 · shadcn/ui │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Next.js 16 App Router │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Pages (SSR) │ │ API Routes │ │ Middleware │ │
│ │ & Client │ │ /api/* │ │ (auth guard) │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────────┘ │
│ │ │ │
│ ┌──────▼─────────────────▼──────┐ │
│ │ Service Layer │ │
│ │ auth · rbac · inventory │ │
│ │ audit · validators │ │
│ └──────────────┬────────────────┘ │
│ │ │
│ ┌──────────────▼────────────────┐ │
│ │ Prisma 7 ORM (PrismaPg) │ │
│ └──────────────┬────────────────┘ │
└─────────────────┼───────────────────────────────────────┘
│
┌─────────────────▼───────────────────────────────────────┐
│ PostgreSQL Database │
│ Users · Products · Batches · Balances · Ledger · Alerts │
└─────────────────────────────────────────────────────────┘
- Node.js 20+ (LTS recommended)
- PostgreSQL 14+ running locally or remotely
- npm 10+
git clone <repository-url>
cd inventory-erp
npm installCreate a .env file in the project root:
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"
AUTH_SECRET="your-random-secret-string-at-least-32-chars"npx prisma migrate deploynpx prisma db seedThis creates default users, a warehouse, locations, units, a sample product, and a team. See Default Credentials.
npm run devThe app runs at http://localhost:3000.
npm run build
npm startBoth
devandbuildscripts automatically runnpx prisma generateas a pre-step to ensure the Prisma client is up to date.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string. Used by Prisma for migrations, the seed script, and the app runtime. |
AUTH_SECRET |
Yes | A random string (32+ characters) used by NextAuth v5 to sign/encrypt JWTs and cookies. Generate with openssl rand -base64 32. |
AUTH_URL |
Production | The canonical URL of your deployed app (e.g. https://erp.example.com). Required in production for NextAuth callback URLs. |
The Prisma schema lives at prisma/schema.prisma. The generated client is output to src/generated/prisma/ (custom output path configured in the generator block).
Configuration is defined in prisma.config.ts:
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts",
},
datasource: {
url: env("DATABASE_URL"),
},
});The Prisma client uses the @prisma/adapter-pg driver adapter with a singleton pattern that caches the client on globalThis during development to prevent connection exhaustion from hot reloads.
The seed script (prisma/seed.ts) creates three users with the shared password below.
| Password | Role | Department | |
|---|---|---|---|
admin@erp.local |
Password123! |
ADMIN | Operations |
manager@erp.local |
Password123! |
WAREHOUSE_MANAGER | Warehouse |
user@erp.local |
Password123! |
WAREHOUSE_USER | Warehouse |
All passwords are hashed with bcryptjs (10 salt rounds) before storage. The plaintext password is never stored in the database.
| Entity | Details |
|---|---|
| Departments | Operations, Warehouse |
| Team | "Core Warehouse Team" (Warehouse dept, user@erp.local as member) |
| Warehouse | WH-MAIN — Main Warehouse |
| Locations | MAIN (Main Store), DMG (Damaged Goods), TRANSIT (On Transit) |
| Units | PCS (Pieces), CRT (Crate) |
| Category | Beverages |
| Product | SKU-COLA-001 "Cola Drink" — reorder point 24, CRT→PCS conversion multiplier 24 |
inventory-erp/
├── prisma/
│ ├── schema.prisma # Database schema (models, enums, relations)
│ ├── seed.ts # Seed script (users, warehouse, products)
│ └── migrations/ # SQL migration files
├── src/
│ ├── app/
│ │ ├── layout.tsx # Root layout (auth-aware shell with sidebar)
│ │ ├── page.tsx # Landing page (guest) / Dashboard (signed-in)
│ │ ├── login/ # Login form
│ │ ├── unauthorized/ # Access denied page
│ │ ├── movements/ # Stock In / Out / Transfer / Adjustment
│ │ ├── stock-counting/ # Quick Stock Out (sale flow)
│ │ ├── low-stock/ # Low stock alerts management
│ │ ├── inventory-search/ # Inventory search & filtering
│ │ ├── products/ # Product CRUD
│ │ ├── categories/ # Category CRUD (soft delete)
│ │ ├── warehouses/ # Warehouse & location management
│ │ ├── teams/ # Department & team management
│ │ ├── audit-log/ # Movement audit trail with ledger entries
│ │ └── api/
│ │ ├── auth/[...nextauth]/ # NextAuth route handlers
│ │ ├── categories/ # Category CRUD endpoints
│ │ ├── products/ # Product CRUD endpoints
│ │ ├── warehouses/ # Warehouse CRUD endpoints
│ │ ├── locations/ # Location CRUD endpoints
│ │ ├── departments/ # Department CRUD endpoints
│ │ ├── teams/ # Team CRUD endpoints
│ │ ├── team-members/ # Team membership endpoints
│ │ ├── inventory/
│ │ │ ├── movements/ # Stock movement recording & history
│ │ │ ├── search/ # Inventory balance search
│ │ │ └── valuation/ # Total inventory valuation
│ │ └── low-stock-alerts/ # Alert listing & status updates
│ ├── components/
│ │ ├── app-shell/
│ │ │ └── nav-links.tsx # Desktop sidebar & mobile bottom nav
│ │ ├── auth/
│ │ │ └── sign-out-button.tsx
│ │ └── ui/ # shadcn/ui primitives (button, card, dialog, etc.)
│ ├── generated/
│ │ └── prisma/ # Generated Prisma client (custom output)
│ ├── lib/
│ │ ├── auth.ts # NextAuth configuration (Credentials provider)
│ │ ├── prisma.ts # Prisma client singleton
│ │ ├── rbac.ts # Role-based access control (permissions)
│ │ ├── api-auth.ts # API auth helpers (requireUser, requirePermission)
│ │ ├── api-response.ts # Standardized API response helpers
│ │ ├── audit.ts # Audit logging utility
│ │ ├── validators.ts # Zod schemas for request validation
│ │ ├── utils.ts # cn() utility (clsx + tailwind-merge)
│ │ └── inventory/
│ │ ├── service.ts # Core inventory movement service
│ │ └── conversion.ts # Unit conversion logic
│ └── types/
│ └── next-auth.d.ts # Session/JWT type augmentation
├── prisma.config.ts # Prisma 7 config (schema, migrations, seed)
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript config (strict, @/* path alias)
├── components.json # shadcn/ui configuration
├── postcss.config.mjs # Tailwind v4 PostCSS plugin
├── eslint.config.mjs # ESLint (Next.js + TypeScript)
└── package.json
When signed in, the root page displays a dashboard with:
- Total inventory valuation (sum of all on-hand quantities multiplied by unit costs)
- Open low-stock alert count with link to the alerts page
- Active lot (batch) count
- Quick action cards linking to key workflows
When not signed in, a marketing landing page is shown.
The movements page provides four tabbed workflows:
- Stock In — Record incoming inventory (purchases, returns). Creates a batch, updates balances, and writes immutable ledger entries.
- Stock Out — Record outgoing inventory (sales, internal use). Decrements from specific batches.
- Transfer — Move stock between locations within or across warehouses. Creates paired debit/credit ledger entries.
- Adjustment — Correct inventory discrepancies with reason codes (cycle count correction, damage, theft/loss, expiry, QA rejection, system/manual correction).
All movements:
- Look up products by SKU
- Record the performing user
- Support an optional idempotency key to prevent duplicates
- Generate
StockTransaction+StockLedgerEntryrecords (double-entry style) - Automatically trigger low-stock alerts when quantities drop below reorder points
A streamlined flow for point-of-sale or rapid stock-out operations. Posts a STOCK_OUT movement with SALE source type.
Displays all OPEN alerts when inventory drops below a product's reorder point. Supports:
- Acknowledge — Mark an alert as seen (records who acknowledged and when)
- Resolve — Close the alert after restocking
Filter and search inventory balances by product, warehouse, location, and batch. Queries the /api/inventory/search endpoint.
Full product management with:
- SKU (unique identifier)
- Category assignment
- Base unit, preferred purchase unit, preferred sales unit
- Unit conversions with configurable multipliers
- Reorder point threshold
- Soft delete support
Product category management with soft delete. Categories with deletedAt set are hidden from active listings but retained for historical reference. A unique constraint on (name, deletedAt) allows reusing category names after deletion.
Hierarchical warehouse management:
- Warehouses have a unique code (e.g.
WH-MAIN) and contain locations - Locations are typed:
MAIN_STORE,DAMAGED_GOODS,ON_TRANSIT,RECEIVING,PICKING,QUARANTINE
Organizational structure management:
- Departments group users and teams
- Teams belong to a department and have members
- Team Members link users to teams with a join date
- Access is restricted by RBAC — only users with
team:managepermission can modify teams
A comprehensive movement history page showing:
- All stock transactions with filters (movement type, warehouse, product, date range)
- Expandable rows revealing individual ledger entries (opening qty, change, closing qty, unit cost)
The AuditLog model captures entity-level changes (create, update, delete) with before/after JSON snapshots, the performing user, IP address, and user agent.
User ─────────────── Department
│ │
├── TeamMember ──── Team
├── StockTransaction ──── StockLedgerEntry
├── AuditLog │
└── LowStockAlert ├── Product ──── ProductCategory
│ │
│ ├── Batch
│ ├── ProductUnitConversion ──── Unit
│ └── InventoryBalance
│
├── Warehouse
└── InventoryLocation
| Model | Purpose |
|---|---|
| User | Authenticated users with email, password hash, role, and department |
| Department / Team / TeamMember | Organizational hierarchy |
| ProductCategory | Product groupings with soft delete |
| Product | SKU-identified items with units, conversions, and reorder thresholds |
| Unit / ProductUnitConversion | Measurement units and conversion multipliers (e.g. 1 Crate = 24 Pieces) |
| Warehouse / InventoryLocation | Physical storage hierarchy with typed locations |
| Batch | Lot tracking per product with cost, expiry, and supplier reference |
| InventoryBalance | Current on-hand and reserved quantities per batch+location, with optimistic concurrency (version) |
| StockTransaction | Header for each inventory movement (type, source, performer) |
| StockLedgerEntry | Immutable line items recording qty changes, opening/closing balances, and cost snapshots |
| LowStockAlert | Triggered alerts with acknowledge/resolve workflow |
| AuditLog | Generic entity change tracking with JSON diffs |
| Enum | Values |
|---|---|
| Role | ADMIN, PROCUREMENT_MANAGER, WAREHOUSE_MANAGER, WAREHOUSE_USER, AUDITOR |
| LocationType | MAIN_STORE, DAMAGED_GOODS, ON_TRANSIT, RECEIVING, PICKING, QUARANTINE |
| MovementType | STOCK_IN, STOCK_OUT, TRANSFER, ADJUSTMENT |
| MovementSourceType | PURCHASE, SALE, INTERNAL_USE, TRANSFER, ADJUSTMENT, STOCK_COUNT, RETURN |
| AdjustmentReason | CYCLE_COUNT_CORRECTION, DAMAGE, THEFT_OR_LOSS, EXPIRED, QA_REJECTION, SYSTEM_CORRECTION, MANUAL_CORRECTION |
| AlertStatus | OPEN, ACKNOWLEDGED, RESOLVED |
- Login — User submits email and password at
/login - Credential Validation — NextAuth's Credentials provider validates against the database:
- Email is lowercased and looked up via Prisma
- Account must be active (
isActive: true) - Password is verified with
bcrypt.compare
- JWT Session — On success, a JWT is issued containing
id,role, anddepartmentId - Middleware Protection —
src/middleware.tsintercepts all protected routes:- API routes (
/api/*except/api/auth/*) return401 JSONif unauthenticated - App pages redirect to
/loginif unauthenticated
- API routes (
interface Session {
user: {
id: string;
email: string;
name: string;
role: string; // Role enum value
departmentId: string | null;
};
}Permissions are defined in src/lib/rbac.ts and enforced via requirePermission() in API routes.
| Permission | ADMIN | PROCUREMENT_MANAGER | WAREHOUSE_MANAGER | WAREHOUSE_USER | AUDITOR |
|---|---|---|---|---|---|
inventory:write |
Yes | - | Yes | Yes | - |
category:delete |
Yes | - | - | - | - |
procurement:view |
Yes | Yes | - | - | - |
team:manage |
Yes | - | - | - | - |
warehouse:manage |
Yes | - | Yes | - | - |
Department scoping: Non-admin users can only access data within their own department. Admins bypass this restriction.
All API routes are under /api/. Authentication is required for all endpoints (enforced by middleware). Responses follow a consistent JSON structure.
| Method | Endpoint | Description |
|---|---|---|
| GET/POST | /api/auth/* |
NextAuth handler (login, session, CSRF) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/categories |
List all active categories |
| POST | /api/categories |
Create a category |
| GET | /api/categories/:id |
Get a single category |
| PUT | /api/categories/:id |
Update a category |
| DELETE | /api/categories/:id |
Soft-delete a category |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/products |
List products (optional ?sku= lookup) |
| POST | /api/products |
Create a product |
| GET | /api/products/:id |
Get a single product with relations |
| PUT | /api/products/:id |
Update a product |
| DELETE | /api/products/:id |
Soft-delete a product |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/warehouses |
List all warehouses |
| POST | /api/warehouses |
Create a warehouse |
| GET | /api/warehouses/:id |
Get warehouse with locations |
| PUT | /api/warehouses/:id |
Update a warehouse |
| DELETE | /api/warehouses/:id |
Delete a warehouse |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/locations |
List locations (filterable by warehouse) |
| POST | /api/locations |
Create a location |
| GET | /api/locations/:id |
Get a single location |
| PUT | /api/locations/:id |
Update a location |
| DELETE | /api/locations/:id |
Delete a location |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/departments |
List all departments |
| POST | /api/departments |
Create a department |
| GET | /api/departments/:id |
Get a department |
| PUT | /api/departments/:id |
Update a department |
| DELETE | /api/departments/:id |
Delete a department |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/teams |
List teams |
| POST | /api/teams |
Create a team |
| GET | /api/teams/:id |
Get a team with members |
| PUT | /api/teams/:id |
Update a team |
| DELETE | /api/teams/:id |
Delete a team |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/team-members |
List team members |
| POST | /api/team-members |
Add a member to a team |
| PUT | /api/team-members/:id |
Update a membership |
| DELETE | /api/team-members/:id |
Remove a member |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/inventory/movements |
List movements (query: take, skip, type, warehouseId, productId) |
| POST | /api/inventory/movements |
Record a stock movement (Stock In/Out/Transfer/Adjustment) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/inventory/search |
Search inventory balances with filters |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/inventory/valuation |
Get total inventory valuation |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/low-stock-alerts |
List alerts (filterable by status) |
| GET | /api/low-stock-alerts/:id |
Get a single alert |
| POST | /api/low-stock-alerts/:id |
Update alert status (action: ACKNOWLEDGE or RESOLVE) |
| Section | Links |
|---|---|
| Operations | Dashboard, Movements, Quick Stock Out, Low Stock |
| Master Data | Products, Categories, Warehouses, Teams |
| Reports | Search, Audit Log |
Home, Move, Products, Search, Audit
This section documents the architectural choices and the step-by-step process of building the Inventory ERP system.
- Initialized with
create-next-appusing Next.js 16, TypeScript, Tailwind CSS v4, and the App Router - Configured shadcn/ui as the component library with Radix UI primitives for accessible, composable UI elements
- Set up Prisma 7 with the PostgreSQL provider and the
@prisma/adapter-pgdriver adapter, outputting the generated client tosrc/generated/prisma/to keep it within the source tree - Configured path aliases (
@/*→./src/*) intsconfig.jsonfor clean imports
The schema was designed around these core principles:
- Double-entry ledger accounting — Every stock movement creates immutable
StockLedgerEntryrecords that capture opening quantity, change, and closing quantity. This provides a complete, auditable trail and enables balance reconstruction from the ledger alone. - Batch/lot tracking — Every unit of inventory belongs to a
Batchwith cost, expiry, and supplier reference. This supports FIFO costing, expiry management, and traceability. - Optimistic concurrency —
InventoryBalanceincludes aversionfield to prevent lost updates during concurrent stock operations. - Soft deletes — Products and categories use
deletedAttimestamps instead of hard deletes, preserving historical data integrity while hiding inactive records from operational views. - Flexible units — The
Unit/ProductUnitConversionsystem allows products to be received in one unit (e.g. crates) and sold in another (e.g. pieces) with configurable multipliers.
- NextAuth v5 was chosen for its App Router compatibility and JWT session strategy (no database session table needed)
- Credentials provider authenticates against the local
Usertable with bcrypt password verification - JWT callbacks inject
roleanddepartmentIdinto the token, making them available in every session without additional database queries - Middleware provides a single enforcement point for all route protection — API routes get JSON 401s, pages get redirects to
/login - RBAC is implemented as a simple permission map in
src/lib/rbac.ts— roles map to permission strings, andrequirePermission()throws before any handler logic runs
The inventory service (src/lib/inventory/service.ts) handles all movement types:
- Stock In: Creates or finds a batch → upserts inventory balance → writes ledger entry → checks reorder point → creates low-stock alert if needed
- Stock Out: Validates sufficient quantity → decrements balance → writes ledger entry with negative change → triggers alert check
- Transfer: Paired operation — decrements source location and increments destination — both within a single transaction for atomicity
- Adjustment: Similar to Stock In/Out but with explicit reason codes for audit compliance
Unit conversions (src/lib/inventory/conversion.ts) handle the math when movement quantities are in a different unit than the product's base unit.
Standard CRUD API routes were built for all master data entities (categories, products, warehouses, locations, departments, teams, team members). Each follows the pattern:
GETfor listing with optional filtersPOSTfor creation with Zod validationGET /:idfor single record retrievalPUT /:idfor updatesDELETE /:idfor deletion (soft or hard depending on the entity)
- App shell — Authenticated users see a responsive layout with a desktop sidebar and mobile bottom navigation bar
- Landing page — Unauthenticated visitors see a marketing page; authenticated users see the dashboard
- Form patterns — All forms use controlled React state with client-side validation before API submission
- Feedback — API errors are displayed inline; loading states prevent double submissions
- Responsive design — Tailwind responsive utilities ensure usability across mobile, tablet, and desktop
- Audit log — The
AuditLogmodel and utility capture who changed what and when, with JSON diffs of before/after states - Low-stock alerts — Automatically generated during stock movements when on-hand quantity drops below a product's
reorderPoint; managed through an acknowledge → resolve workflow - Movement history — The audit log page provides filterable, paginated access to all stock transactions with expandable ledger detail
| Decision | Rationale |
|---|---|
| JWT sessions over database sessions | Eliminates a database round-trip on every request; role/department are embedded in the token |
| Prisma driver adapter (pg) | Direct PostgreSQL wire protocol for better performance than Prisma's default query engine |
| Custom Prisma output path | Generated client lives in src/generated/prisma/ so it's versioned and the @/* alias works seamlessly |
| Immutable ledger entries | Stock ledger entries are never updated or deleted — corrections are new adjustment entries, ensuring a tamper-proof audit trail |
| Soft deletes for products/categories | Historical transactions reference these records; hard deletes would break referential integrity |
| Singleton Prisma client | Cached on globalThis during development to prevent connection pool exhaustion from Next.js hot module replacement |
| Zod v4 for validation | Schema-first validation with TypeScript inference for both API request bodies and form inputs |
| Script | Command | Description |
|---|---|---|
| Dev | npm run dev |
Start development server (auto-generates Prisma client) |
| Build | npm run build |
Production build (auto-generates Prisma client) |
| Start | npm start |
Start production server |
| Lint | npm run lint |
Run ESLint |
| Seed | npx prisma db seed |
Seed the database with default data |
| Migrate | npx prisma migrate deploy |
Apply pending migrations |
| Generate | npx prisma generate --schema prisma/schema.prisma |
Regenerate Prisma client |
Private — not licensed for redistribution.