Inventory Pro uses two separate authentication systems that work together:
Supabase (frontend)
Manages the user session in the browser. Supabase tracks whether a user is logged in and refreshes session tokens automatically via cookies.
JWT (backend API)
Every protected API request requires a JWT signed with JWT_SECRET. The token carries userId and tenantId so the backend can scope all queries to the correct tenant.
All protected routes pass through authMiddleware, which extracts the Bearer token, verifies it with jsonwebtoken, and attaches userId and tenantId to the request object:
backend/src/middleware/auth.ts
import type { Request, Response, NextFunction } from "express"import jwt from "jsonwebtoken"declare global { namespace Express { interface Request { userId?: string tenantId?: string } }}export function authMiddleware(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing or invalid authorization header" }) } const token = authHeader.slice(7) try { const decoded = jwt.verify( token, process.env.JWT_SECRET || "your-secret-key" ) as any req.userId = decoded.userId req.tenantId = decoded.tenantId next() } catch (error) { res.status(401).json({ error: "Invalid token" }) }}
authMiddleware is applied globally to all routes registered after it. Public auth routes (/api/auth/*) are registered before the middleware:
backend/src/index.ts
// Public routes — no token requiredapp.use("/api/auth", authRouter)// All routes below this line require a valid JWTapp.use(authMiddleware)app.use("/api/products", productsRouter)app.use("/api/services", servicesRouter)app.use("/api/customers", customersRouter)app.use("/api/inventory", inventoryRouter)app.use("/api/stock-movements", movementsRouter)
POST /api/auth/register accepts a tenant name, admin name, email, and password. It creates a Tenant and a User atomically in a single Prisma transaction, then returns a signed JWT:
The frontend stores the JWT in localStorage after a successful login and sends it as an Authorization: Bearer <token> header on every API request:
// Storing the token after loginlocalStorage.setItem("token", data.token)// Sending the token with API requestsfetch(`${process.env.NEXT_PUBLIC_API_URL}/products`, { headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, },})
Storing JWTs in localStorage exposes them to XSS attacks. For production deployments, consider storing the token in an HttpOnly cookie instead, which is inaccessible to JavaScript.
Set JWT_SECRET to a cryptographically random value of at least 32 bytes. You can generate one with openssl rand -hex 32. Never commit this value to source control.