Hobber
Jan 2026 — Present
MARKETPLACE PLATFORM · FRONTEND ARCHITECTURE
Vendors run their whole business — services, schedules, pricing, payouts, and team access — from one dashboard instead of a scattered set of tools and manual back-and-forth.
Vendor platform for entertainment, recreation, dining & tourism vendors · UAE
Stack
UI · reference build
Signup done. 3 steps left before customers can book you.
Vendor readiness
62%You're not visible in search until you hit 100%
Profile views / wk
0
lockedServices live
0 / 4
Est. time to go live
8 min
A reference build of the flow, drawn in code to show the shape of the work. The production app is under NDA.
The pattern, in code
The boundary rule the whole codebase is held to — a slice may reach down into shared UI, never sideways into another slice. Enforced by lint, not by good intentions.
// features/<domain>/ owns its routes, components, state and api together
// shared/ui/ generic primitives only — no domain knowledge
// eslint.config.mjs — the architecture is a lint rule, not a document.
{
rules: {
"no-restricted-imports": ["error", {
patterns: [{
// Both patterns are needed. A single-segment glob misses the bare
// barrel — "@/features/payouts" — which is exactly how anyone would
// actually reach across a slice.
group: ["@/features/*", "@/features/*/**"],
message:
"Cross-slice import. Compose at the route level, or promote the " +
"shared piece into shared/ui.",
}],
}],
},
}
// Shared UI stays domain-agnostic: if a prop names a domain, it is not shared.
type ButtonProps = { variant: "primary" | "ghost"; loading?: boolean } // ok
type RowProps = { variant: "primary"; isPayoutRow: boolean } // not sharedIllustrative — written to show the shape of the decision. Client code is under NDA and none of it appears here.
01 · Context
Hobber is a marketplace for entertainment, recreation, dining, and tourism vendors, and the vendor platform is the side those businesses actually operate on. When I joined there was no dashboard — just a list of everything it would eventually need: authentication, vendor accounts, dashboards, scheduling, payouts, integrations, and team access management. Each of those is a real domain with its own state, its own permissions, and its own backend surface. The default path is to start with one screen and keep adding to it; three months later every change touches everything.
02 · The decision
I architected the frontend from scratch on a modular, feature-based structure: each domain owns its own slice — routes, components, state, and API layer together — over two shared layers underneath, generic UI primitives that know no domain and a domain layer for what every slice needs but none owns: the HTTP client, auth and token refresh, permission checks, error normalisation. A lint rule refuses a cross-slice import, and TypeScript checks the shape where slices compose at the route — two different guarantees, and neither is a convention. That let me build activity management, booking flows, stock scheduling, discount logic, and vendor content management as separate workflows that compose instead of colliding.
03 · The trade-off I made
Upfront architecture vs. time to first screen. Feature slicing and a shared component layer cost real days before anything is demo-able, and at a startup that is a genuinely uncomfortable trade. I took it because the platform's scope was known up front — seven modules, not one — and the alternative is paying it back with interest during the first big feature. What I protected was the boundary: shared UI stays generic, and anything domain-specific lives in its slice, even when copying it would have been faster that afternoon.
04 · What I measured
The honest read here is delivery and change cost, not a performance metric — this is an in-flight platform. I watch whether a new module lands inside its own slice without editing the others, whether the shared components get reused rather than forked, and whether TypeScript catches integration breaks at build time instead of in review. Working with backend and DevOps on APIs, authentication, and real-time features, the question I keep asking is whether a contract change ripples or stays contained.
05 · Outcome
Authentication, vendor accounts, dashboards, scheduling, payouts, integrations, and team access management are all live as separate feature modules, with activity management, booking flows, stock scheduling, discount logic, and vendor content management built on top of the same reusable UI layer. The structure is the deliverable as much as the screens are: adding a domain is adding a folder.
06 · What I'd do differently
What I'd do differently: I built the shared component layer slightly ahead of demand, and a couple of those early primitives were generalized for use cases that never arrived. The better rule — the one I now hold to — is to let a pattern appear twice before it becomes a shared component, and to keep the third one honest. The lesson I carry: on a greenfield platform the expensive decision is not which framework you pick, it's where you draw the boundaries, because every future feature either respects them or erodes them.
What it moved
Count
Authentication, vendor accounts, dashboards, scheduling, payouts, integrations and team access — shipped as independent feature slices over one shared component layer.
Engineering practices in play
Architecture before the first screen
Chose a modular, feature-based structure over a single dashboard surface, so each of the seven platform domains owns its routes, state, and API layer instead of accreting into a monolith.
Reusable beats bespoke
Built one shared component layer under every workflow — activity management, booking, stock scheduling, discounts, and content — so vendor features compose rather than each shipping its own UI.
The boundary is a check, not a convention
A lint rule refuses a cross-slice import outright; TypeScript catches a shape that has drifted where the slices compose. Two different guarantees — neither of them a code review remembering.
Each one maps to a specific decision above — the reasoning is on record, not asserted as a trait.