Bikalpo

Wholesale marketplace with customer-specific pricing and role-based ordering. I’m building the storefront, mobile app, and API. The B2B storefront is live; the wider platform remains in development.

Technology
  • Next.js 16
  • React 19
  • Drizzle ORM
  • Postgres
  • Better-Auth
  • TanStack Query
  • Lexical
  • Cloudinary
Bikalpo — B2B storefront on b2b.bikalpo.com

The brief

A Dhaka-based distributor needed a commerce platform for warehouses, retailers, and consumers. Each audience has different pricing and ordering rules, while sharing the same product catalog.

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 storefront, a React Native buyer app, and a Hono server. Both apps use a shared oRPC client for catalogue, order, and content APIs.

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.

Web · tiered storefront
Next.js 16 provides the web storefront for the platform’s participant tiers.
Native · buyer app
The Expo buyer app is part of the platform under development.
Shared API
Both clients use the documented Hono and oRPC API through a shared typed client.
Shared data
Postgres stores catalog and content data, with Drizzle, Better-Auth, and 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.