Overview
Inventory Pro is a full-stack multi-tenant SaaS application. Each layer has a distinct responsibility:Frontend
Next.js 16 (App Router) with React 19 and Tailwind CSS. Handles routing, UI, and client-side session management via Supabase Auth.
Backend
Node.js with Express. Exposes a REST API on port 5000. Validates JWTs and enforces tenant isolation on every protected route.
Database
PostgreSQL accessed through Prisma ORM. Every table carries a
tenantId column that scopes all rows to a specific tenant.Auth
Supabase manages client-side sessions. The backend independently issues and verifies JWTs that carry both
userId and tenantId.Multi-tenant data model
TheTenant model is the root of the data hierarchy. Every resource in the system — users, products, services, customers, inventory records, and stock movements — belongs to exactly one tenant via a tenantId foreign key with cascade delete.
schema.prisma
Tenant through a tenantId field:
| Model | Key fields | tenantId |
|---|---|---|
User | email, password, name, role | Required |
Product | name, sku, price | Required |
Service | name, description, price | Required |
Customer | name, email, phone, address | Required |
Inventory | productId, quantity, minStock, maxStock | Required |
StockMovement | productId, type (IN/OUT), quantity, reason | Required |
Tenant cascades to all related records across every table.
Data isolation
Tenant isolation is enforced at the API layer, not the application layer. The auth middleware extractstenantId from the verified JWT and attaches it to every incoming request:
backend/src/middleware/auth.ts
req.tenantId as a mandatory filter in every Prisma query. A user from tenant A cannot read or modify data belonging to tenant B — even if they know the resource ID.
Authentication flow
- Registration —
POST /api/auth/registercreates aTenantrecord and an associated adminUserin a single transaction. The response includes a signed JWT. - Login —
POST /api/auth/loginverifies credentials and returns a JWT containinguserIdandtenantId. - Token verification —
GET /api/auth/verifyconfirms the token is valid without requiring a database lookup for the full user record. - Protected routes — every request to
/api/products,/api/services,/api/customers,/api/inventory, and/api/stock-movementspasses throughauthMiddlewarebefore reaching the route handler.
API routes
| Route prefix | Visibility | Description |
|---|---|---|
POST /api/auth/register | Public | Create a new tenant and admin user |
POST /api/auth/login | Public | Authenticate and receive a JWT |
GET /api/auth/verify | Public | Verify a JWT |
GET /api/products | Protected | List products for the authenticated tenant |
POST /api/products | Protected | Create a product |
PUT /api/products/:id | Protected | Update a product |
DELETE /api/products/:id | Protected | Delete a product |
GET /api/services | Protected | List services |
POST /api/services | Protected | Create a service |
GET /api/customers | Protected | List customers |
POST /api/customers | Protected | Create a customer |
GET /api/inventory | Protected | List inventory records |
PUT /api/inventory/:id | Protected | Update stock levels |
GET /api/stock-movements | Protected | List stock movements |
POST /api/stock-movements | Protected | Record a stock movement |
index.ts, all protected routers are registered after the global authMiddleware:
backend/src/index.ts
Request lifecycle
Client sends request
The browser or API client sends an HTTP request to the Next.js frontend or directly to the Express API. For data requests, it includes an
Authorization: Bearer <token> header.Next.js frontend
Next.js serves the React UI. Data fetching calls are forwarded to the Express backend at
NEXT_PUBLIC_API_URL (default: http://localhost:5000/api).Auth middleware
authMiddleware intercepts the request, extracts the Bearer token, and calls jwt.verify(). On success, it attaches userId and tenantId to the req object and calls next().Route handler + Prisma query
The route handler runs a Prisma query that always includes
tenantId: req.tenantId in its where clause, ensuring results are scoped to the authenticated tenant.PostgreSQL returns data
Prisma executes the parameterised SQL against PostgreSQL and returns only rows matching the tenant.
Frontend authentication
The client-side session is managed by Supabase Auth. TheAuthProvider component wraps the entire application and tracks the current session:
lib/auth-context.tsx
middleware.ts) calls updateSession on every request to keep the Supabase session fresh, excluding static assets.
For direct API calls to the Express backend, the JWT returned at login is stored in localStorage and attached as the Authorization header on each request.