Multi-Tenant Dashboard
A multi-tenant analytics dashboard prototype built with Next.js, React, TypeScript, and Tailwind CSS.

A shop signs in with just its name — no password — and gets sales and pricing analytics for its own products and variants, pulled from a real SQLite database rather than mock data. Built as a take-home assignment for MokshaAI: Next.js (Pages Router), Redux Toolkit, and Recharts over a small relational schema of shops, products, variants, and orders.
Shop-only auth, cookie-backed
There's no password field. /api/login validates the posted shop name against the shops table (isShopNameValid) and, on success, sets an httpOnly, signed session cookie with a one-hour expiry — not a token the client stores and re-attaches itself. Redux's authSlice tracks the logged-in user through async thunks that hit /api/login and /api/logout, and only reads the persisted user back from localStorage in an explicit rehydrateUser action fired after mount — not inside the reducer's own initial state, which would read localStorage during server rendering and risk a hydration mismatch.
Every metric ships with its own trend
getSalesSummary doesn't just total units and revenue for the selected date range — it shifts that same window back by its own length, runs the identical aggregate query again, and returns both totals side by side with the raw and percentage change already computed (guarded against a previous period with zero sales, where a percentage change is undefined rather than a divide-by-zero). Every number the dashboard shows can carry '↑12% vs. previous period' without a second manual query anywhere in the UI layer.
SELECT day,
SUM(total_units) AS total_units,
SUM(total_revenue) AS total_revenue
FROM sales_summary
WHERE day BETWEEN ? AND ? -- + optional product_id / variant_id filters
GROUP BY day
ORDER BY day ASCRedux as the boundary, not the destination
Data flows one way: REST endpoint -> Redux slice (auth, dashboard, price, sales) -> selector -> Recharts. Analytics components never query the database directly, which is the whole reason a component can be tested or reused without a live SQLite file behind it. Product and variant lookups — needed constantly for the filter dropdowns — go through a small in-memory cache in dashboard.js with a 5-minute TTL, so switching between products doesn't re-hit SQLite on every selection.

