★ Featured case study

Ecobazar — a full-stack grocery store I actually shipped

A Next.js 16 organic-grocery store where the storefront and a role-based admin / moderator / customer dashboard share one auth session and one MongoDB. Built to survive real-world write races, not just to render a landing page.

eco.shanto.dev
Ecobazar — Fresh & Healthy Organic Food landing page
19
Prisma models
9
Role & state enums
20
Playwright specs
3
User roles
1
Codebase, one domain
The problem

Why this exists

A small store needs one codebase that lets customers browse, order, and pay while staff manage catalogue, inventory, orders, banners, and timed promotions from the same site. The Bangladesh context adds cash-on-delivery and BDT-first pricing (with admin-managed multi-currency), so I couldn't just fork a US-first Shopify demo and rename it.

The interesting part isn't the shopping cart. It's making concurrent orders behave, keeping an audit trail that survives a user being deleted, and running role-gated dashboards without leaking Prisma or bcrypt into the Edge runtime.

Architecture

Stack at a glance

Data model

Where the interesting shapes are

19 models, 9 enums, 567 lines of schema. The ones that pull weight:

Auth

How access is enforced

Reality check

What's live vs what's still in progress

I'd rather be honest here than embellish. Here's the split as it stands today:

Shipping

Live in the demo

  • Catalogue + slug pages
  • Cart + wishlist with localStorage persistence
  • Guest checkout with atomic order placement
  • Address book
  • Admin / moderator / customer dashboards
  • Image upload (dev & Vercel Blob paths)
  • Password reset + email verification (dev mailer)
  • Hot Deals + admin-managed multi-currency
  • Approval queue & audit log
In progress

Still being wired up

  • Email verification is issued but not enforced at login yet.
  • Cart-merge-on-login is on the TODO list — today the client cart wins.
  • Coupon table is duplicated in three places and needs a single source of truth.
  • Test coverage is Playwright e2e only; no unit runner yet.
  • Mailer is a dev logger; production SMTP still to wire.
  • next.config.mjs ships with minimumCacheTTL: 0 for dev iteration — needs a prod value.
Decisions

Three trade-offs worth explaining

Prisma-with-MongoDB, cuid over ObjectId

I kept string cuids as @map("_id") so ids stay stable across the older MySQL prototype's data shape. The trade: Prisma's Mongo adapter has no native Decimal, so every price is stored as integer cents and formatted on read. Simpler migrations were worth the small formatting cost.

Two auth configs, not one

auth.config.js holds providers only and is imported by the middleware — that keeps it edge-safe. lib/auth.js (Node runtime) adds Credentials, PrismaAdapter, and DB events on top. Prisma and bcrypt therefore never end up in the Edge bundle, and the middleware stays under the size cap.

Role in the JWT, not in the DB

Every request reads role from the token. That's a DB round-trip saved on every page, at the cost of never being able to demote a live session instantly. Write-time defence lives in the server actions — which is where destructive intent lives anyway — so a stale role can't cause a wrong write.

Engineering moments

Two hard problems I actually solved

Atomic checkout without trusting the client cart

HARD PROBLEM #1

placeOrderAction runs inside prisma.$transaction(). Prices are recomputed from the DB (anti-tampering) and each line item's stock is decremented with a guarded updateMany({ where: { id, stock: { gte: qty } } }). If the returned count === 0 for any item, a concurrent order won the race — the whole transaction rolls back and the customer sees a clear "out of stock" instead of oversell. It requires a MongoDB replica set, which the deployment docs call out up front.

An audit trail that survives user deletion

HARD PROBLEM #2

The obvious way to link privileged actions to their actor is a required foreign key. That makes user deletion painful: either you cascade the audit log away (losing history) or you refuse the delete. Ecobazar takes a third path — AuditLog.actorId is optional with onDelete: SetNull. Deleting a user preserves every privileged action they ever performed; only the actor pointer is cleared. Enforcement is by convention: every mutating server action appends an AuditLog row before it returns.

Want to see the code?

The repo has a 796-line DOCUMENTATION.md, a seed script that boots 3 users and 10 products in one command, and the full Playwright regression suite.