---
title: "GleanGrid: Building a Pre-Order Marketplace for Farmers Markets"
description: "How I designed and built GleanGrid on my own, a Laravel and React marketplace where Hyderabad's farmers publish weekly stock and customers reserve it, pay online or at the stall, and collect it: architecture, data model, payments, security and UX."
author: "Syed Ahmer Shah"
date: 2026-09-27
url: https://ahmershah.dev/blogs/gleangrid-case-study-farmers-market-preorder-platform
tags: ["gleangrid", "case-study", "laravel", "react", "mysql", "web-development", "software-architecture", "system-design"]
series: "The Engineering Logs" (part 7)
---

# GleanGrid: Building a Pre-Order Marketplace for Farmers Markets

_A case study on turning Hyderabad's weekly farmers markets into something you can reserve from your phone, without taking the market itself away_

![Building a pre-order marketplace for farmers markets: Ahmer holding a crate of fresh vegetables beside the GleanGrid dashboard on a laptop](https://ahmershah.dev/blog/gleangrid-case-study-farmers-market-preorder-platform/cover-17e70d3e.webp)


Every week, growers from Tando Jam, Kotri, Thatta and Mirpurkhas bring vegetables, mangoes, dairy, honey and bread into Hyderabad. The markets themselves are great. Everything around them is guesswork. You rarely know which farmer will turn up, what they'll bring or what it'll cost, and the Sindhri mangoes you crossed the city for are often gone by nine. Farmers have the opposite problem: they can't take an order before market day, so they either pick too much and throw some away, or pick too little and lose the sale.

GleanGrid is my answer to that. I built it for Aptech's TechWiz 7 competition, against a brief called *MarketLink: eGreen Basket*, and I built all of it myself: the research, the UI, the database, the backend, payments, security, testing and the documentation. Farmers publish their weekly stock, prices and pickup windows. Customers find a market on a map, reserve produce against real stock, pay online or in cash at the stall, and collect it on market day. There's no delivery, on purpose. The market stays the market; the guesswork goes.

This is how it's put together, what I got wrong along the way, and what I'd change next.

* * *

## The brief, in one sentence

"Let people reserve this week's harvest from a specific farmer, at a specific market, for a specific pickup window, and never promise something the farmer doesn't have."

That last clause shaped almost everything. A shop can apologise for a late parcel. A farmer who drove in at six in the morning with forty kilos of tomatoes cannot sell forty-one.

![GleanGrid's home page: a live badge shows how many markets are open today, and the search jumps straight into this week's produce.](/blog/gleangrid-case-study-farmers-market-preorder-platform/e1a0812eba.webp)

## Who uses it

Three roles, each with a workspace of its own:

- **Customers** browse markets, stalls and produce, fill a basket without an account, then check out with a pickup window per stall. They can change or cancel until the farmer's cut-off, carry a QR pickup pass, save favourites, ask to be told when something sold out comes back, share a family account, and review a farmer after pickup, with photos if they like.
- **Farmers** register a stall (an admin approves it), list weekly stock with photos, set pickup windows with a capacity and a cut-off, and move orders through accepted, ready and completed. At the stall they scan the customer's QR code.
- **Admins** approve or suspend stalls, manage markets and categories, moderate listings, reviews and review photos, tune the stall-badge rules, edit the seasonal calendar, refund payments, answer the contact inbox and read reports across every market.

![Who can do what: the roles and the modules each one reaches.](/blog/gleangrid-case-study-farmers-market-preorder-platform/6d80a6a8c3.webp)

## Choosing the stack

I wanted two things that usually pull against each other: an interface that feels like a modern app, and a backend where routing, auth and validation stay on the server, where I can reason about them.

**Laravel 12 and React 19, joined by Inertia.js,** gave me both. Every page is a React component, but there's no public API to secure. A controller validates the request, checks who's asking, and hands props to the page. Navigation feels instant because links prefetch on hover and the layout never unmounts; only the page in the middle swaps. Inertia's server-side rendering means crawlers and slow phones get real HTML before any JavaScript runs.

| Layer | Choice | Why |
|---|---|---|
| Backend | Laravel 12, PHP 8.2 | Routing, Eloquent, queues, notifications, rate limiting |
| Bridge | Inertia.js 3 with SSR | SPA feel, server-side routing and auth, real HTML first |
| Frontend | React 19, Tailwind CSS 4, Vite | Components, design tokens, fast builds |
| Motion | Motion, Lenis | Spring physics, layout animation, smooth scroll |
| Maps | Leaflet + OpenStreetMap, OSRM | Free, keyless maps and routes |
| Database | MySQL 8 / MariaDB | Foreign keys, CHECK constraints, views, triggers |
| Mail | Resend | Branded e-mail, sent only after the database commits |

## Architecture

![How a request flows: React runs through Inertia, Laravel handles middleware, controllers and services, and MySQL does its share of the work.](/blog/gleangrid-case-study-farmers-market-preorder-platform/90b564aa4c.webp)

The rule I kept returning to was **thin controllers, services for anything with rules in it**. Placing, changing and cancelling an order, moving it between states, redeeming a coupon, taking a payment, awarding a badge, checking a one-time code: all of that lives in `OrderService`, `PaymentService`, `CouponService`, `BadgeService` and `AccountSecurity`. They can be tested without HTTP and reused from the customer area, the farmer panel, the admin panel and the scheduler.

A few decisions paid for themselves many times over:

- **One source of truth per concern.** SEO copy lives in one PHP class, rendered on the server and reused by React. Policy and FAQ text lives in one place and feeds the pages, the FAQ structured data and `llms.txt`.
- **Notifications are queued and sent after commit.** If an order rolls back, nobody gets an "order placed" e-mail for something that never happened.
- **The web tier is stateless.** Sessions, cache and queue sit behind drivers, so moving them to Redis and adding servers is configuration, not a rewrite.
- **The scheduler does the boring, important work:** expiring unpaid checkouts, evening-before pickup reminders, the Monday restock, and the nightly badge recalculation.

## The data model

![The entity–relationship diagram: 26 tables, with users, farmer profiles, markets, products, pickup slots and orders at the centre.](/blog/gleangrid-case-study-farmers-market-preorder-platform/c3417a134d.webp)

The core schema is 26 tables and four reporting views, with a handful more added later for payments, price history, badges and the seasonal calendar. The shape is simple once you see it: a **farmer profile** belongs to a user and trades at several **markets**. It lists **products** and offers **pickup slots** at each market. An **order** belongs to one customer, one farmer, one market and one pickup slot, and holds **order items**.

What makes it trustworthy is what the database refuses to accept:

- Every relationship is a real foreign key with a deliberate `ON DELETE` rule.
- **22 CHECK constraints** reject impossible data even if a bug slips past PHP: ratings outside 1–5, negative prices, a closing time before an opening time, a coupon used more often than its limit.
- `order_items` keeps a **snapshot** of the product name, unit and price, so last month's order still says what it said when a farmer edits the listing.
- Triggers write every status change into `order_status_history`, and views like `v_market_revenue` pre-join the heavy report queries.
- Every price change lands in `product_price_history`, one row per product per day, which is what the price charts read.

## One order, many states

![The pre-order lifecycle: placed, accepted, ready, completed, with declined, cancelled and no-show as the exits.](/blog/gleangrid-case-study-farmers-market-preorder-platform/b7ebe2275a.webp)

An order is a small state machine, and the server is the only place that decides whether a move is legal. A farmer can't mark a cancelled order as ready. A customer can't cancel after the cut-off. Every transition re-reads the order under a row lock first, so a customer cancelling at the exact moment the farmer taps "accept" can't leave it half one thing and half the other. The allowed transitions are unit-tested on their own, separately from the HTTP flows.

## Never selling what isn't there

This is the part I'm proudest of, and it has its own article: [The last mango problem](/blogs/the-last-mango-problem-race-conditions-in-a-real-marketplace). The short version: stock, coupons and pickup-window capacity are all decided **inside one transaction, under row locks**, taken in a fixed order so two checkouts can't deadlock each other.

Unit tests can't really prove that, because they run in a single process. So I wrote a script that launches fifty separate PHP processes at checkout at the same instant, against the real MySQL database.

![Fifty buyers racing for one item, one single-use coupon and a pickup window with room for three. Every run: one sale, one coupon use, three bookings, zero crashes.](/blog/gleangrid-case-study-farmers-market-preorder-platform/63037fe2b8.webp)

The first run failed. A pickup window with room for three accepted twenty orders. The cause was a MySQL isolation detail I explain properly in the other article, and the fix was one line. I'd much rather find that with a script than with a queue of annoyed customers at a stall.

The same thinking later caught two quieter bugs. A farmer saving the stock form while customers were buying could overwrite their purchases with the number the form had loaded minutes earlier. And the Monday restock read each product, then wrote it back, which could wipe a reservation made in between. Both are fixed; the article has the details.

## Payments, added later, done carefully

The brief said "pay at pickup, no gateway", and cash at the stall is still always there. But a lot of people in Pakistan would rather tap Easypaisa or JazzCash than carry cash, so I added online payment on top of the brief, and I treated it as the riskiest feature in the project.

- Placing an online order **reserves the stock and opens a payment window** (20 minutes by default). Farmers aren't notified and can't accept until it's paid. Unpaid checkouts are released by a scheduled job, which puts the stock back and returns the coupon.
- **The amount is always recomputed on the server** from the orders. The browser never tells the server how much to charge.
- **A double tap can't charge twice.** The payment row is locked, it moves from `pending` to `processing` exactly once, and every attempt carries a one-time idempotency key under a unique index, so a replayed request returns the existing result.
- **Late confirmations are refunded automatically.** If the gateway confirms after the window has closed and the stock has gone back on sale, GleanGrid records a `late_capture` event and refunds.
- **Card numbers and CVCs are never stored, logged or flashed back to the session.** Only the brand and the last four digits are kept.
- JazzCash's hosted checkout is wired in with its `pp_SecureHash` HMAC, checked again on the signed callback in constant time. In sandbox mode a built-in simulator runs the whole flow with test cards, so the competition judges could try it without real money.

## Signing in, and not getting taken over

Google and Facebook sign-in were the other big addition. The interesting part isn't the button, it's the edge cases:

- An e-mail is only trusted if the provider says it's verified.
- **Pre-account takeover** is blocked. If someone registered your e-mail with a password but never verified it, and you then sign in with Google, the unverified password is wiped and its sessions are killed. Otherwise the squatter would still hold a way in.
- Profile photos are downloaded once, only over HTTPS and only from Google or Facebook hosts, with every redirect re-checked (no SSRF), then re-encoded to WebP.
- No access or refresh tokens are stored, and you can't disconnect your last way of signing in.

## Security, layer by layer

A marketplace holds people's names, phone numbers and addresses, so security was part of the build from the first migration, not a feature bolted on at the end:

- **Passwords** are hashed with Argon2id, and old bcrypt hashes upgrade on the next sign-in.
- **Sign-in** has per-account and per-IP lockouts, six-digit e-mail codes, alerts for new devices, and "sign out everywhere else".
- **Bots** meet four layers. A honeypot field. A **signed form ticket**: every page render carries a fresh, encrypted issue time, and a form that comes back too fast, hours later, or with a forged ticket is refused. (My first version trusted a timestamp from the browser, which a script can simply fake.) Invisible **reCAPTCHA v3** scoring. And a visible challenge, **Cloudflare Turnstile** or the reCAPTCHA v2 box, on sign-up, contact and password resets, and on sign-in after a low score.
- **CSRF**: every state-changing request carries a token, the token is **regenerated on every full page load**, and any write the browser marks as cross-site is refused before it reaches a controller.
- **XSS** is handled by React's escaping plus a nonce-based Content-Security-Policy with **a new nonce on every response**, `strict-dynamic`, `object-src 'none'` and `frame-ancestors 'self'`.
- **SQL injection** has nowhere to go: every query runs through prepared statements.
- **Uploads** are shrunk and converted to WebP in the browser, then decoded and re-encoded on the server, which strips EXIF and GPS data and anything hidden after the pixels. SVG is refused.
- **Basket Buddy**, the on-site assistant, is tap-only. It accepts whitelisted intent keys, never free text, so there's nothing to prompt-inject, and every personal answer is built from the signed-in user alone.
- **Floods** hit a site-wide rate limiter on every page, with much tighter limits on sign-in, sign-up, search, checkout, payments and every write.

## Designing for a real market morning

People open GleanGrid on a phone, often on mobile data, usually in a hurry. That set the priorities.

![The produce page: a category rail with counts, a price slider, market and day filters, and real product photos with a 3D illustration on top.](/blog/gleangrid-case-study-farmers-market-preorder-platform/e4d8869484.webp)

On phones the search bar gets its own row, categories sit in a scrollable rail with item counts, and the other filters open in a bottom sheet with **Reset** and **Show results**, so you set three filters and load once instead of three times. Every filter, search and page is a readable path rather than a query string (`/products/category/fruits/price/100-500/sort/price-low`), which is friendlier to share and to search engines.

![On a phone: the home page, the filter sheet and the basket with its sticky total.](/blog/gleangrid-case-study-farmers-market-preorder-platform/98f4926407.webp)

Every product has a real photograph paired with a 3D illustration. The photo tells you what you're actually buying; the illustration keeps the catalogue recognisable at a glance. Product pages also chart the price over one, three or six months and say when today's price is below its 30-day average, and a seasonal calendar shows what's at its peak in Sindh this month.

![A product page with a real photo, the illustration as a sticker, stock, cut-off time and the farmer who grows it.](/blog/gleangrid-case-study-farmers-market-preorder-platform/5e95fc2320.webp)

The basket groups items by stall, because each stall becomes its own pre-order with its own pickup window. It shows the next pickup for every stall, lets you undo a removal, and keeps the total and the checkout button in a sticky bar on phones.

![The basket: one pre-order per stall, the next pickup for each, and a ticket-style summary.](/blog/gleangrid-case-study-farmers-market-preorder-platform/9b566b21f1.webp)

A few more details that matter more than they look:

- **Eight languages**, including Urdu and Arabic, which flip the whole layout to right-to-left. Every locale has every key, checked by a script.
- **Search** opens anywhere with Ctrl K, groups results into produce, farmers and markets, highlights matches and suggests "tomatoes" when you type "tomatos".
- **Stall badges** (Top Rated, Reliable Pickups, Rising Star, Customer Favourite) are recalculated nightly. Top Rated uses a Bayesian average, so a stall with two five-star reviews can't outrank one with fifty 4.8s.
- **Motion with manners**: headlines reveal word by word, items fly into the basket, two galleries slide sideways as you scroll. All of it switches off for anyone who prefers reduced motion.
- **Dark mode** and an installable **PWA** with an offline page.

## The farmer and admin side

Farmers aren't power users, so their panel is built around the three things they do every week: update stock, see what's been ordered, and hand it over.

![The farmer dashboard: revenue for the last eight weeks, pending orders and best sellers.](/blog/gleangrid-case-study-farmers-market-preorder-platform/11ef8b59c3.webp)

Admins get a control room: today's orders, 30-day trends, pickups for the next week, revenue by market, catalogue health, a payments console with every payment's timeline and one-click refunds, and a sign-in security panel that flags IP addresses with repeated failures.

![The admin control room.](/blog/gleangrid-case-study-farmers-market-preorder-platform/1eae0705d3.webp)

## SEO and performance

Every page has a hand-written title and description, rendered on the server. Public pages carry linked structured data: `Product` with an offer that points at the stall selling it, `LocalBusiness` for stalls, `Place` with opening hours for markets, `FAQPage` for the help page, and a site search action. There's a live sitemap, sensible `robots` rules, and `llms.txt` / `llms-full.txt` for AI assistants that read the web.

Only English ships in the main bundle; every other language loads on first use. Maps load as they approach the viewport, images are WebP with responsive sizes, and fonts never block the first paint. Lighthouse gives the public pages **100 for accessibility, best practices and SEO**.

## Testing

- **133 PHPUnit tests with 779 assertions** run against a real MySQL database and cover the order flow, payments, coupons, security headers, the bot and CSRF defences, structured data, race conditions and every role's pages.
- **Playwright end-to-end tests** drive the real site as a visitor, a customer and an admin, on desktop and on a phone.
- The **stress script** proves the concurrency guarantees with real parallel processes.
- **GitHub Actions** runs all of it on PHP 8.2 and 8.3, plus a code-style check and a dependency audit, on every push.

## How I worked, and where AI helped

I'll be straight about this, because I think it matters. I designed and wrote GleanGrid myself, and every decision in this article is one I made and can defend. I did use Claude as a second pair of eyes. It was genuinely useful for debugging, when I was staring at a failing test and needed someone to question my assumptions. It was useful for finding improvements, like pointing at a place where a lock was taken too late or a check trusted the browser. And it helped me tighten the documentation, which is the part of a project I'm most tempted to rush.

What it didn't do was replace understanding. The REPEATABLE READ bug is a good example. A suggestion is only worth something if you can explain why it's right, and I only trusted a fix once I could reproduce the failure with fifty processes and watch it disappear.

## What I'd do next

If GleanGrid grew beyond a handful of markets, the next steps are clear. Redis for sessions, cache and rate-limit counters. A CDN in front of the images, with Cloudflare absorbing floods before they reach the server. WhatsApp alerts when an order is ready, because that's where people here actually read messages. And live Easypaisa and card acquiring, which plug into the same gateway interface JazzCash already uses once there's a merchant agreement.

## What it taught me

The interesting problems were never the ones on the feature list. They were the questions in between. What happens when two people want the last item? What does the database see while a transaction is waiting? What happens if the payment gateway says yes twenty-one minutes later? What does a farmer need at six in the morning, with wet hands and one bar of signal? Building GleanGrid made me answer those properly instead of hoping they wouldn't come up.

If you run a market, sell at one, or just have thoughts on any of this, I'd genuinely like to hear from you. The source is on [GitHub](https://github.com/ahmershahdev/GleanGrid), and my inbox is open.


---

*Originally published at https://ahmershah.dev/blogs/gleangrid-case-study-farmers-market-preorder-platform — © Syed Ahmer Shah*
