SelfShop
Multi-role reseller marketplace. I built the admin, reseller, and supplier dashboards in the Next.js client app, plus the React Native + Tamagui reseller app on Google Play. The Laravel backend was the team's. This case study covers the front of house.

The brief
SelfShop is a multi-role reseller marketplace. Suppliers list wholesale stock; resellers pick products to sell on their own social channels and through the platform; end customers buy through whichever surface fits. I owned the front of house: the surfaces admin, resellers, and suppliers actually live in, across web and native.
Three roles. Three surfaces. One Laravel API I didn't write.
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.
- A backend I didn't write. The Laravel API was the team's. I had to plug into it cleanly, catch breakage at type-check rather than runtime, and keep the three surfaces consistent without a shared codebase to enforce it.
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.


The hard parts
Three surfaces, no shared codebase
The honest answer is: I didn't share much code between the surfaces. The Next.js dashboards live in one app; the reseller native app lives in native/; the Laravel backend is upstream of all of it. There's no monorepo joining them.
What I did share was the parts that drift fastest if they aren't shared:
- Typed contracts: every API endpoint has a hand-mirrored TypeScript schema that lives in each client. When the backend changes a field, type-check fails at compile time on the surface that consumes it, not on the customer's phone.
- 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 thin client per surface with strict response schemas. Every request has a typed return shape; every list endpoint has a typed pagination envelope; auth tokens flow through one place. When the backend renamed is_active to status mid-flight, type-check failed in seventeen call sites within a minute, and the fix was a one-line schema update once the rename was understood.
It's not as clean as OpenAPI codegen would be. That's the lesson at the bottom of the page. But it's the level of discipline that's possible without changing how the backend ships.
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. Real users on the live surfaces; numbers from the team will replace these placeholders when I have them.
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.