partial · live surfacesMulti-Party Commerce Marketplace· 2025—present

Bikalpo

Multi-party commerce for a Bangladeshi distributor: platform admin, warehouse/wholesaler, two retailer tiers (resell-and-buy and buy-only), and a consumer storefront on one catalog with tier-aware pricing. Partial work is live on b2b.bikalpo.com. The full platform and Turborepo rewrite (web, mobile, Hono API) are in active development.

Next.js 16React 19Drizzle ORMPostgresBetter-AuthTanStack QueryLexicalCloudinary
Bikalpo — B2B storefront on b2b.bikalpo.com

The brief

A Dhaka-based distributor needed commerce across wholesale, retail, and consumer. Not a downsized B2C shop with a wholesale login bolted on. Their warehouse sells in tiers, two kinds of retail shops buy under different rules, and consumers order like a normal storefront. Off-the-shelf Shopify-style platforms collapse under all of it.

I shipped partial surfaces at b2b.bikalpo.com. The full platform lives in bikalpo-project: a Turborepo with Next.js web, Expo native, and a Hono · oRPC server (documented API) on Postgres, Better-Auth, Drizzle, and Lexical for product copy and policy pages. Part of the catalogue is live today; warehouse tiers, consumer checkout, and the mobile app are still under build.

The problem

The status quo had four pain points:

  • Per-customer pricing: every wholesale account negotiates its own tiered price, and the storefront has to respect it without leaking other customers' tiers.

  • Catalogue scale: thousands of SKUs across dozens of brands, each with its own copy, imagery, and inventory state.

  • Rich content alongside commerce: landing pages, brand stories, and policy pages live next to the catalogue and need first-class editing, not a CMS bolted on the side.

  • Bangladesh-specific payments: local payment rails, COD, and partial-payment flows that most platforms ignore.

The approach

The platform is a Turborepo monorepo: a Next.js web storefront, a React Native buyer app, and a Hono server that exposes catalogue, orders, and content through an oRPC contract with full API documentation. Both clients import the same typed client, so the boundary stays honest.

Postgres holds catalogue rows and Lexical JSON for product copy and policy pages. Auth, sessions, and role gating run through Better-Auth, including per-customer pricing keyed off the authenticated identity.

apps/webnext.js 16 · tiered storefrontapps/nativeexpo · buyer mobileapps/serverhono · oRPC · API docspostgresdrizzle · better-auth · lexical
fig. — web + native through Hono · oRPC — documented API, shared Postgres

The hard parts

Per-tier pricing without per-tier fetches

Each authenticated buyer sees their own price column on every product. The naive shape (fetch product, then fetch overrides) adds a round-trip on every catalogue page. I push tier resolution into the Drizzle query on the API with a single LEFT JOIN ... ON pricing.tier_id = $session.tier, so the listing page returns 60 products with the right number on each, in one query. The same hook covers cart totals, so the price the buyer sees is the price they're charged. No second source of truth.

Lexical alongside SKU data

Treating product copy and policy pages as the same content primitive was the call that paid off most. Both render through a single set of Lexical nodes (paragraph, heading, image, callout, table). New page types are a database row, not a new template.

Shared packages

packages/db, packages/auth, and packages/ui sit between the three apps so schema, auth rules, and design tokens stay in one place. The oRPC contract is the only front door: web and native both call the same documented API, not separate data paths.

What's running today

partiallive on b2b.bikalpo.com
devfull platform · in progress
3target apps · web / native / server

The live surfaces cover part of the catalogue and account tooling on b2b.bikalpo.com. The rest of the multi-party platform ships through the same Hono · oRPC stack described above.

What I'd do differently

Lexical was the right call for content; it would have been the right call sooner. Earlier versions used a hand-rolled markdown layer that I had to keep patching. Once the rich-text model is treated as part of the schema, every other decision gets easier.