
E-Commerce

Comprehensive e-commerce platform developed for Bergaz Food. A modern solution combining corporate presentation, online sales, and customer management under one roof.
This storefront is not a one-off site; it is a deployment of the e-commerce platform I built. The same platform runs for two customers β Bergaz Food and Ezine Gurme. The shared core is everything that matters: the data model, the payment flow, the stock ledger, the role system and all four panels. What is customer-specific is the brand layer, the content and the integration keys. Without that split from the start, the second customer would have meant a forked repository β and in forked repositories a bug fixed on one side stays broken on the other.
Not every page runs on the same strategy; the decision follows how fresh the
page needs to be. The home page revalidates on a 60-second ISR window and
streams through six Suspense boundaries. The catalogue is dynamic: filters and
pagination live in the URL, so a filter combination becomes a shareable address.
Corporate copy (about, privacy, returns, terms) stays static behind a daily ISR
window. Admin and sales panels are fully dynamic β protected and real-time.
Caches are invalidated by tag: updating one product refreshes the tags bound to
it, not the whole site. Cart and comparison list live in client stores persisted
to localStorage rather than on the server, so a guest visitor does not lose
their cart β but price and stock are re-validated on the server at checkout; no
number coming from the client is trusted.
The application installs from the browser, and because the last-seen catalogue stays in the cache, a dropped connection does not produce a blank storefront. Order, shipping and marketing emails are sent from a background job queue, so checkout is never blocked by a slow email provider. Visitor analytics are kept inside the application rather than handed to a third party.
Sales ran over the phone and messaging; stock, orders, and payments were not tracked in one place. The hard part of an e-commerce platform is not the storefront but the consistency between payment and stock: if the payment provider resends the same callback, if two customers reach the last unit at the same moment, or if a payment is abandoned midway, stock and orders drift apart. That drift is silent β nobody sees an error, the numbers simply stop matching.
Two sides. The buyer: an end customer looking for cheese with protected geographical origin; picks a weight and packaging variant, wants to see how far they are from the free-shipping threshold, and looks for reassurance at the 3D Secure step. The business: the ADMIN role manages products, stock, orders, and returns; the SALES role sees only the sales side and cannot reach the rest of the system.
Sole developer. Data model, payment and shipping integrations, the stock movement ledger, the admin and sales panels, the background job queue, security hardening, and Vercel deployment.
Next.js 16 App Router with Neon PostgreSQL and Prisma. The write path is Server Action β Zod validation β Prisma transaction. The payment flow has five steps: cart verification (stock and price re-checked on the server), iyzico 3D Secure initialisation, the bank verification redirect, callback processing, and order finalisation. The order record, the stock decrement, and the stock movement entry are written inside a single transaction. Shipping and email work goes onto a background job queue. Images are stored in Vercel Blob. The code side is launch-ready; the main branch sits on Vercel as a staging-like production β because the live iyzico and shipping credentials are an operator cutover step, the payment flow currently runs against a mock provider.
The payment provider can resend the same callback. Reading "has this order already been paid" and then writing left a race between two concurrent callbacks. The callback token is now written first into a log table with a unique constraint; the second callback fails on that write and is never processed.
Trade-offEvery callback β including the failed ones β writes a row; the table grows continuously and needs a separate cleanup job. It also means carrying a dedicated code path that reads a constraint violation as "already handled" rather than as an error.
Between the stock check and the stock decrement, another order could take the last unit. The read, the verification, the order creation, the stock decrement, and the stock movement entry were brought into a single transaction; when a payment fails, restoring stock and cancelling the order happen the same way, in one transaction.
Trade-offThe transaction holds the relevant variant rows for the duration of payment finalisation; simultaneous orders for the same product queue up behind it. At a busy moment that is a direct delay for the waiting customer.
The upload folder name came from administrator input; left open, path traversal and arbitrary folder clutter were both possible. The six accepted folders are defined as a fixed set in code, and any value outside it raises an error.
Trade-offOpening a new folder now requires a code change and a deployment; it cannot be done from the panel. Flexibility on the content side was deliberately traded away for a smaller attack surface.
Bot traffic is not only login attempts; registration, product reviews, the contact form, newsletter signup, and order lookup are targets too. Verification is checked server-side on all of these endpoints.
Trade-offAll of these forms now depend on a third-party script; if it fails to load, the form cannot be submitted. Each submission also carries the cost of an extra verification request.
Prisma keeps relations and migration history traceable across a 31-model schema, and the multi-statement transaction support needed on the payment and stock side comes built in. Neon PostgreSQL stays in the same serverless model as the deployment. Vercel Blob keeps images out of both the repo and the database, with image optimisation working through a remote pattern. The rate limiter is kept in the database rather than in memory β under serverless, every instance carrying its own counter made the limit meaningless.
Six HTTP security headers are defined: CSP, nosniff, framing denial, referrer policy, HSTS, and a restricted Permissions-Policy. User input (reviews, return reasons, order notes, profile text, contact form) is sanitised. Rate limiting runs through a central database-backed limiter. There are three roles β ADMIN, SALES, USER β and every protected Server Action verifies the session as well. Mock payment is controlled by a server-only variable and is rejected in production without an explicit override. Return approvals and stock movements are written to the audit log.
React Compiler automatic memoization is enabled; chart components are split out of the main bundle via dynamic import, and the homepage streams behind Suspense boundaries. The Server Action cache is invalidated by tag, so a product update refreshes the relevant tag rather than the whole page. Images are served as AVIF and WebP, and bundle analysis ships as a ready command in the repo. Ten Playwright smoke tests cover the product lifecycle, coupon, and return flows. Lighthouse mobile (my own measurement, median of 4 warm runs): 59/100 β September 2026. The score is low, and the measurement shows why: the cost is time to first byte, not blocking script β a median FCP of 6.1 seconds against a total blocking time of 36 ms. The storefront page is rendered on the server on every request; the real gain lies in moving the product listing off per-request rendering and onto revalidated static output.
I would build the payment and stock side the same way today, but I would change the order: I would write the idempotency constraint first. Here I started with an "already paid?" check and only closed the race afterwards β exactly what happened with the double-approval problem on GΓΆrev KahramanΔ±, where I also added the guarantee after the fact. Second, to avoid repeating the maintenance cost I am paying on Bergaz Analiz for leaving the period lock in the application layer, I would push the stock and payment rules down into the database earlier. Third, a performance measurement taken while payment is mocked does not represent real load; there is no point publishing a score before the live credentials are in place and a date-stamped measurement has been taken.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.