FLOW LGX · PART 4 · JANUARY 2026

Flow LGX, part 4: project structure, roles and protected pages

In January 2026 the screens existed and the next risk was structure. A logistics platform grows sideways — new roles, new modules, new rules — and a codebase that is not organised for that growth slows down every month after.

About this project

Month by month, how a logistics platform for a Toronto client went from a simple brief to a live, multi-company SaaS product.

In short

We organised the code so each type of user has their own area behind one shared, protected layout. That made later growth — from a few roles to nine — far easier.

Two ways to split a codebase — we used both

Route-level pages are grouped by role: everything an admin sees lives under the admin folder, everything warehouse staff see under staff, and so on. That makes it obvious where a screen lives and who it is for.

The logic inside those pages is grouped by feature. Each feature folder holds its own components, types, validation schema and constants. Pages stay thin: they arrange features and pass in data. When two roles need the same feature with small differences, they share the feature and differ only at the page level.

One layout for everyone who signs in

Every protected page renders inside a single application layout: a top bar, a navigation menu and the page content. The menu is built from the signed-in user’s role, so a dispatcher and a warehouse worker see different menus inside the same frame.

Using one layout instead of one per role kept the product consistent and meant design fixes landed everywhere at once.

Routes that check the role, not just the login

Each role’s routes sit behind a protected route that checks two things: is the user signed in, and is their role allowed here? A warehouse user who types an admin address is sent to an “unauthorised” page instead of seeing a broken screen. Public pages — sign in, register, forgotten password, one-time code, reset password — sit in a separate public layout.

We wrote the role list once, in one file, and every route, menu and check reads from it. That decision paid for itself many times over when the number of roles grew later in the project.

State: as little global state as possible

We chose Zustand for the small amount of truly global state — mainly who is signed in and their role — and TanStack Query for everything that comes from the server. There is no Redux. Server data is cached, refreshed and shared by query keys rather than copied into a global store and kept in sync by hand.

What we shipped

  • Codebase organised into pages by role and features by domain
  • One protected application layout with role-based menus
  • Protected routes that check both sign-in and role
  • A single source of truth for role names and paths
  • Zustand for session state, TanStack Query for server data

Lesson learned

Keep the list of roles in exactly one place. Roles always multiply, and scattered role checks are where access bugs hide.