← BACK TO WRITING
17 MAY 2026·9 min·mridula

Weekend 2: An Organic Farm Listing App

Five FPOs, fourteen listings, Hindi by default. The platform's job isn't to close deals — it's to bridge a listing to a phone call.

The third prototype of the Mridula Initiative is live at organic-farm-listings.adityaksingh.dev: a bilingual marketplace where Farmer Producer Organisations (FPOs) in the Kanpur belt list organic produce and buyers reach them directly. Hindi by default, English on a toggle. Mobile-first, designed for basic Android browsers.

Five FPOs are seeded with fourteen current listings across wheat, basmati, pulses, millets, mustard, and vegetables. The names and details are placeholders for the prototype — the real version waits for actual FPOs to fill the rows, and a sample-data banner makes that explicit on every page.

What follows isn't a build log. It's a note on the one design decision that ended up shaping everything, the bilingual line I had to draw twice, and the question I keep coming back to.

The marketplace that isn't a marketplace

The honest version of what I built: it's not really a marketplace. There's no checkout, no escrow, no payments, no fulfilment tracking. A buyer clicks a listing, fills in their name and phone number, and that's it. A row gets written to a Postgres table. The FPO calls them back.

That sounds like a shortcut. I want to defend it as the right shortcut for this stage.

A working transactional platform would need months of work — real KYC for both sides, a relationship with an aggregator or bank, escrow logic, dispute resolution, GST handling on agricultural produce that's exempt but only sometimes. None of that is the bottleneck for the long-term NGO partner conversation this portfolio is building toward. The bottleneck is showing that FPO discovery plus a clean inquiry handoff works at all. The actual deal-closing happens by phone anyway — that's how 2026 agri trade in the Kanpur belt is.

So the inquiry form is the contract. The buyer leaves a phone number. The FPO calls. That's the whole thing.

What surprised me was how much that single framing clarified the rest of the build. Once "the platform's job is discovery, not settlement" was the design north star, dozens of smaller questions answered themselves. No carts. No buyer accounts. No notification system. Most of the complexity I was tempted to build was complexity I didn't need.

The bilingual line, drawn twice

This is the part of the project I expected to be hard, and it was hard for a reason I didn't expect.

The roadmap was explicit: Hindi-first, no parallel strings inside JSX. "Parallel strings" means writing {locale === "hi" ? "हिंदी" : "English"} inline in components — the obvious wrong path that every shortcut tutorial seems to teach. So I knew where I wasn't going. I didn't yet know where I was going.

The thing that took the most thinking was that bilingual content is actually two different problems wearing the same costume.

UI labels — buttons, headers, form errors — change when the code changes. They belong in version control, alongside the code that uses them. next-intl with two JSON files (messages/hi.json, messages/en.json) handles this cleanly. A missing key fails loudly in development. Adding a new label means one edit per locale and one t("key") call.

Data fields — FPO names, village names, free-text crop descriptions — change when the data changes. They belong in the database, not in version control. Putting an FPO's Hindi name into messages/hi.json would mean a redeploy every time a new FPO signed up. So Postgres gets _hi columns: name and name_hi, village and village_hi. A single pickLocalised(locale, en, hi) helper returns the Hindi value when present and falls back to English when the column is null.

Drawing the line between these two categories turned out to be the bulk of the design work. Every new bilingual string I wanted to add forced the question: who owns this? If a developer changes it, JSON. If an FPO changes it, database. A third category — numbers and dates — got handed off entirely to Intl.NumberFormat("hi-IN") and Intl.DateTimeFormat("hi-IN"), which is interestingly not a translation problem at all but a locale-formatting one.

One detail I hadn't thought through: hi-IN keeps Arabic numerals by default (4,250, not ४,२५०). I assumed I'd need to override this. I didn't — Arabic numerals are what Indian users actually expect. Dates change month names (15 May 202615 मई 2026), which is exactly the right behaviour. The standard library got this right; I just had to trust it.

What was actually hard

The next-intl quickstart wants URL-prefixed locales (/hi/listings vs /en/listings). For a marketplace shared on WhatsApp, that's broken: the share URL leaks the previous user's locale, link previews differ between users, the toggle changes the URL. I had to run next-intl in cookie-based mode instead — NEXT_LOCALE cookie, server action to set it, revalidatePath("/", "layout") after toggling so the App Router re-renders with the new locale on the next paint. None of this is in the default tutorial. It works, but the seams took longer to find than the code took to write.

Supabase connection pooling, again. Same gotcha as Project 2: the seed script wants the session-mode pooler on port 5432, the Vercel serverless runtime wants the transaction-mode pooler on port 6543. Mix them up and you get intermittent connection-churn errors on Vercel that don't reproduce locally. I'd already burned a session on this for the chatbot. I burned another half-hour here. Writing it down in case future me forgets again: long-running scripts → 5432, serverless → 6543.

The Supabase free tier caps at two projects. Both slots were already used — mandi explorer, SHG chatbot. So this project shares the chatbot's Supabase instance with a farm_ table prefix to keep things straight in the dashboard. Two unrelated apps sharing one database is technically fine and pragmatically fine for now, but it's the kind of thing that becomes a problem the moment either app needs different scaling settings.

What I cut

  • FPO authentication and the write side. FPOs can't currently edit their own listings — the seed script is the only way data gets in. Intentional for the prototype; the inquiry side is what proves the concept. The next iteration is Supabase Auth plus row-level security so each FPO owns their own row.
  • Rate limiting on the inquiry endpoint. Acceptable now because the form is gated by a listing page that isn't indexed publicly. Before opening to real buyers, per-IP and per-phone-number limits are the first thing to add.
  • Realistic seed FPO data. I'd written real-sounding cooperative names initially and pulled back. Putting a phone number on a public website is enough commitment that I want the actual FPO to consent before their details appear. Placeholder names plus a sample-data banner are the honest middle ground.
  • Search, filters, map view. Five FPOs and fourteen listings don't need search. At fifty FPOs and three hundred listings those features earn their complexity; right now they'd just be empty controls.
  • An admin dashboard for inquiries. Inquiries get written to Postgres but there's no UI to view them yet. For the prototype, looking at the table in the Supabase console is fine. For a real deployment, an FPO-facing "people who asked about your produce, by date" view is the obvious next add.

What I learned

"What doesn't need to exist" is the more useful question than "what should this have". Coming into Project 3 from Projects 1 and 2 — both of which had real engineering complexity worth defending — I had to keep reminding myself that the simpler thing was usually the right thing. No carts, no accounts, no notifications, no payments. The discipline of "the inquiry form is the contract" kept me from building a half-version of fifteen things instead of a complete version of one.

Bilingual is a content-ownership problem, not a translation problem. I'd thought this would be about finding good Hindi for technical terms. It turned out to be about which database table or JSON file each string belongs in. The translations themselves were the easy part once the categories were right.

Server components for read paths just work. Every page that displays data is a server component. The only client components are the language toggle and the inquiry form — the two interactive surfaces. The build outputs 102 KB of shared first-load JS with index pages adding ~1.5 KB each. On a low-end Android over patchy 3G, this should matter in a way it doesn't on a developer laptop. I want to be honest that I haven't yet measured it on a real phone in real rural connectivity — that's still on the to-do list.

The question I can't answer from Bengaluru

Do buyers of organic produce actually look for FPOs on a website?

I built this assuming yes — that a small institutional buyer (a city restaurant chain sourcing organic ingredients, a college mess running a farm-to-plate experiment, a wholesaler trying to skip a layer of intermediaries) might search for "organic basmati Uttar Pradesh" and land here. But I genuinely don't know whether that's how organic produce discovery happens in 2026, or whether it still runs almost entirely through brokers, mandi networks, and personal relationships built over decades.

The mirror question is just as live: do FPOs actually want a public-facing listing? My assumption is that visibility helps. But there are real reasons an FPO might not want their phone number publicly searchable — spam, GST queries from buyers fishing for cheaper deals, the genuine cost of fielding inquiries from people who never end up buying. A platform that gets discovery right for buyers but wastes FPO time is worse than no platform.

Neither question gets answered by building. They get answered by talking to FPOs and to small organic buyers — and ideally watching how a buyer who's never heard of an FPO finds one today, what fails in that process, and whether what fails is something a website can fix.

What's next

The Micro-Loan Portfolio Simulator. The Kanpur trip — sitting with NGOs and SHG members who do this work daily, listening rather than pitching — is in October. Iteration has run faster than I'd budgeted for, so the fourth prototype gets built before that trip, not after.

How does a ₹5–10L revolving fund actually evolve over three years? Loan size, tenure, repayment rate, default rate, reinvestment — modelled with assumptions calibrated from NABARD and Sa-Dhan reports, not arbitrary sliders. The interesting test isn't the math. It's whether the assumptions are transparent enough that someone with real SHG experience could push back on them productively.

A fifth project is on the bench after that: a skill-gap visualiser for UP districts using NSDC and NCS data. Whether it earns its place depends on whether the underlying data turns out to be usable — a question the build itself will answer.