SelfShop
Reseller marketplace with web dashboards for admins, resellers, and suppliers. I built the Next.js frontend and the React Native reseller app on Google Play, integrating the team’s Laravel backend.
- Next.js 16
- React 19
- TypeScript
- Ant Design
- Redux Toolkit
- NextAuth
- React Native · Expo
- Tamagui
- Pusher · Laravel Echo
- Laravel API (consumed)

The brief
SelfShop connects suppliers, resellers, and customers. Suppliers list wholesale stock; resellers choose products to sell through their own channels. I built the web dashboards and reseller mobile app.
The web and mobile clients integrate with the team’s Laravel API.
My role
The reseller app is live on Google Play.
The problem
Reseller commerce isn't B2C with a skin on. The reseller is the vendor of record but doesn't hold stock. The supplier never sees the end customer. The platform sits in the middle of three relationships at once and has to keep them coherent in real time.
- Two roles, two mental models. Resellers care about social-friendly product pages, commission, and cash flow. Suppliers care about purchase orders, stock-on-hand, and dispatch. The same order touches both, and they need different views of it.
- Three surfaces in lockstep. Two web dashboards and one native app. When a customer places an order it has to surface on the reseller's phone within seconds and on the supplier's web dashboard at the same time, without divergence.
- Non-technical primary users. Resellers aren't ops people; suppliers manage their inventory between other things. Forms have to be forgiving, errors have to be readable, and the mobile flows have to work one-handed on bad networks.
- API integration. The team owned the Laravel API. I maintained the web and mobile clients as its endpoints and response shapes changed.
The approach
The web dashboards are Next.js 16 + React 19, with Redux Toolkit and Ant Design. Forms-and-tables work (vendor onboarding, order management, payout reconciliation, discount rules) leans on Ant's data primitives. Redux keeps state predictable across the role-scoped surfaces.
The reseller app is Expo + Tamagui + TanStack Query. Different state library on purpose: mobile is read-mostly with mutations, so a fetch-cache covers most of it without the boilerplate Redux brings to a small surface.
Real-time runs on Pusher channels with Laravel Echo on the client. New-order events fan out to whichever surfaces care: a push notification on the reseller's phone (Firebase + expo-notifications), a banner on the supplier's web dashboard, a row appearing in the operator's inbox. The same channel, different consumers, no polling.


- Web · reseller
- The Next.js client uses Redux Toolkit and Ant Design for the reseller dashboard.
- Web · supplier
- The same Next.js app provides the supplier dashboard, scoped to its role.
- Native · reseller
- The Expo and Tamagui reseller app is available on Google Play.
- Shared API
- All three surfaces consume the Laravel REST API. Pusher channels carry updates to the clients that need them.
The hard parts
Three surfaces, no shared codebase
The Next.js dashboards live in one app and the reseller app lives in native/. Both connect to the Laravel backend. They are separate codebases.
What I did share was the parts that drift fastest if they aren't shared:
- Typed clients: each client defines response schemas for the API endpoints it consumes. Backend changes require corresponding updates to those schemas and their consumers.
- Pusher channel names + event shapes: copied verbatim across the three surfaces, with a comment pointing at the canonical list. A misnamed channel is a silent bug; making it grep-able was worth the duplication.
What I duplicated on purpose: UI components, navigation, theme tokens. Web and native have different idioms and trying to abstract over them costs more than it saves.
Real-time orders without dropping events
A single new-order event has to surface in three places at once: the reseller's phone (push notification), the reseller's web dashboard (banner), and the supplier's web dashboard (queue row). The naive approach, listening on Pusher in each client, drops events when the app is backgrounded on mobile.
The fix:
- Pusher for foreground real-time (reseller and supplier dashboards on the web, reseller app while open).
- Firebase Cloud Messaging via
expo-notificationsfor backgrounded mobile state. - A reconcile-on-foreground TanStack Query refetch on the reseller app, so anything missed during the backgrounded window catches up the moment the app returns.
The dedupe key is the order id. The same order arriving via REST poll, websocket, and push notification all collapse to one row, one badge, one notification.
Plugging into a Laravel API I didn't write
The backend was a moving target. The team shipped vendor features, discount rules, and admin tooling on its own cadence. I needed those changes to break loudly on my surfaces, not silently.
I wrote a typed API client for each application, with shared conventions for response shapes, pagination, and auth tokens. Backend changes require updating the corresponding client schema and its consumers.
Generating clients from an API specification would reduce the manual work of keeping those schemas aligned.
The Reseller App

Live on Google Play. The surface a reseller spends the most time inside.
- Order management: incoming orders, status updates, status timeline. Pull-to-refresh + Pusher means the list is rarely stale.
- Product catalog: browse the supplier-listed catalog, save items to a personal storefront, share listings outbound.
- Push notifications: Firebase +
expo-notifications. New orders, low stock on saved items, payout-ready alerts. - Image upload for listings:
expo-image-picker+expo-file-systemwith a retry queue for bad networks. - Token storage:
expo-secure-storefor auth, scoped per build channel (dev / preview / production). - Telemetry: Sentry for crashes; the app is in real users' hands and silent failures get surfaced fast.


What's running today
The Next.js Client serves the admin, reseller, and supplier dashboards. The reseller native app is live on Google Play.
What I'd do differently
Shared TS contracts via OpenAPI codegen. Hand-mirroring schemas worked but it's a tax I paid every backend change. An OpenAPI spec generated off the Laravel routes, even a partial one, turns three hand-maintained client schemas into one source of truth.