Multi-tenant ecommerce analytics dashboard with Email authentication by Google sign-in, Prisma/MySQL to store ingested data from shopify, and a polished App Router UI for a powerful dashboard.
- Google OAuth sign-in (JWT sessions)
- Protected dashboard and APIs (NextAuth session checks)
- Key metrics: revenue, orders, customers, inventory (with low/out-of-stock)
- Trends with WoW/MoM deltas, returning customer rate
- Inventory table with quick filters and search
- Clean, responsive UI with micro sparklines
- Node.js 18+
- MySQL database (e.g., Railway) and DATABASE_URL
- Google OAuth client (Client ID and Secret)
npm installCreate .env from .env.example (or set via your hosting provider):
DATABASE_URL="mysql://user:pass@host:port/db"
TENANT_SHOP="your-shop.myshopify.com"
NEXTAUTH_SECRET="generate-a-strong-secret"
NEXTAUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secretGoogle Console (OAuth):
- Authorized redirect URI (dev):
http://localhost:3000/api/auth/callback/google - Authorized JavaScript origin (dev):
http://localhost:3000
npm run devOpen http://localhost:3000 and click “Continue with Google”.
- Set
NEXTAUTH_URLto your production origin, e.g.https://your-domain.com - Add production redirect URI:
https://your-domain.com/api/auth/callback/google - Configure all env vars in your hosting provider (Vercel/Railway/etc.)
All metrics routes are protected—require a valid NextAuth session.
GET /api/metrics?start=YYYY-MM-DD&end=YYYY-MM-DD&shop=<shop>- Returns totals (orders, revenue, products, customers), series, trends, insights, and low-stock counts.
GET /api/metrics/products?shop=<shop>- Returns
{ id, title, inventory, threshold, lowStock, outOfStock }[]for the tenant.
- Returns
GET /api/metrics/tenants- Returns tenant list for selector.
Auth endpoints (NextAuth):
GET/POST /api/auth/*viaapp/api/auth/[...nextauth]/route.js
Core models (simplified):
model Tenant {
id String @id @default(uuid())
name String
shop String @unique
createdAt DateTime @default(now())
customers Customer[]
orders Order[]
products Product[]
events Event[]
}
model Product {
id String @id @default(uuid())
shopifyId String @unique
title String
price Float
inventory Int @default(0)
lowStockThreshold Int @default(10)
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
}
model Order {
id String @id @default(uuid())
shopifyId String @unique
totalPrice Float
createdAt DateTime @default(now())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
@@map("orders")
}
model Event {
id String @id @default(uuid())
type String
createdAt DateTime @default(now())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
@@index([type, tenantId])
@@index([createdAt, tenantId])
}Note: The repo also includes standard NextAuth models (User, Account, Session, VerificationToken) which enable database sessions if we plan to switch from JWT sessions later.
- Next.js (App Router), React 19
- NextAuth (Google OAuth, JWT sessions)
- Prisma ORM + MySQL (Railway)
- Tailwind CSS 4
- Recharts
- Preview deploys: Google OAuth requires exact redirect URIs; previews may need a separate OAuth client or be disabled for login.
- Multi-tenant selection expects existing tenants in DB.
- Inventory thresholds default to 10 when missing.
- Returning rate and some trends require enough historical data.
- Cart and Checkout Abandonment caveats:
- Many stores don’t emit explicit
cart_abandonedorcheckout_abandonedevents. When missing, we use a heuristic:cart_abandoned ≈ checkout_started − ordersin the selected window. - Checkout abandonment uses
checkout_abandonedwhen present; otherwise it falls back to the computedcart_abandonedvalue. Rates are clamped to [0, 1]. - The denominator for cart-to-checkout uses distinct carts inferred from
cart_updatedevents (deduped byshopifyId). IfshopifyIdis missing/unstable or if bots cause noisycart_updatedtraffic, the rate can be biased. - These are proxies; for production-grade accuracy, instrument explicit checkout/cart lifecycle events (or webhook-based signals) with stable identifiers and consistent firing semantics.
- Many stores don’t emit explicit
Built by Dave Meshak J | Portfolio
GNU General Public License. See LICENSE for more details.

