No clean starting point
The previous generation had accumulated client-specific features and an older build setup.
Cut a v2 on Vite with a single neutral reference module that demonstrates parent → child entities, list, view, edit and add.
Every new client back office used to begin the same way: copy the last project, strip out its business logic, and rebuild login, layout, translations and the API layer. I led the design of a reusable starter that ships all of that ready-made, so a team's first day goes into the client's domain instead of plumbing.
A software house delivers many back-office apps: user management, catalogs, operations dashboards. They share ≈60–70% of their front-end plumbing, but each one started by forking the previous client's code. That carried over dead features, inconsistent folder layouts and slightly different auth flows. A developer moving between projects had to relearn where the token refresh lived, how loaders worked and how Hebrew RTL was handled this time.
The diagram is wider than the screen: swipe sideways to see all of it →
Nothing renders until appSettings.json has loaded, so the API URL, default language and theme, company name and dev flags are always defined. The same build artifact can go to dev, staging and production; only that file changes.
Settings are fetched from a static JSON file at start-up and held in one typed object. DevOps can repoint an environment without a rebuild. Trade-off: one extra request before first paint, so the app is gated behind a loader until it returns.
A thunk factory plus a reducer helper give every endpoint the same idle / pending / fulfilled / rejected state, the same error shape and the global loader. Adding an endpoint is two short declarations instead of hand-writing three reducer cases. Trade-off: more Redux than a query-cache library, chosen to match the house backend's standard response envelope.
The sidebar is 7 composable parts (Aside.Root, Nav, NavList, NavLink, NavButton, MobileHeader, Footer) and the header 3. Projects compose their own menu instead of forking the component. Trade-off: more files to learn on day one.
Hebrew is the default, direction comes from the language record and is set as a data-direction attribute on body. One small LTR stylesheet flips icons and offsets. Trade-off: the override uses a few forceful rules that need care when components change.
All routes live in one typed map with layouts, guards and redirects. A new entity is a few entries, not a new router setup. Dev flags (SKIP_AUTHENTICATION, IS_RESPONSIVE) let designers work on screens without a backend.
The previous generation had accumulated client-specific features and an older build setup.
Cut a v2 on Vite with a single neutral reference module that demonstrates parent → child entities, list, view, edit and add.
On /entity/:id/edit the tab bar and its mobile dropdown disagreed about which item was active.
Each nav item carries a route pattern; the same matching logic now drives both the tab and the dropdown variant.
Add and edit screens needed the same fields, but edit has to wait for the record.
One shared form component; in edit mode it loads the existing record and resets the form once data arrives.
When the access token expired, every in-flight request tried to refresh it. The refreshes raced and could invalidate each other, logging the user out.
A single in-flight refresh promise is shared by all failing requests. Each original request is retried once, then the user is logged out cleanly.
Every menu change meant editing the component itself, so projects drifted apart.
Refactored both into compound components and updated all consumers in the same change.
Per-project colour and spacing tweaks were mixed into shared styles, which made upgrades painful.
Refactored the styles over two review rounds and kept one override.scss as the only place a project rebrands.
Logout only fired when another tab cleared the token. If the refresh token expired in the current tab, the user stayed on a broken screen.
One logout helper clears tokens and raises an app-level event; the route guard listens to both that event and cross-tab storage changes.
Engineering estimates in working days for one developer, based on what the starter already contains. ESTIMATE
| Area | Before | With the starter |
|---|---|---|
| Project kick-off | Fork the last client app and strip it (≈1 week) | Edit title, favicon and one settings file (<1 hour) |
| Auth | Login, OTP and refresh rebuilt each time | 5 screens wired; connect the API thunks |
| Environments | One build per environment | One build; 7 settings read at runtime |
| Token expiry | N parallel refresh calls, random logouts | 1 shared refresh, one retry per request |
| Hebrew / English | RTL handled differently per project | Direction from the language record, one LTR override file |
| Code style | Per-team conventions | Lint on every commit, format on staged files |
Next steps I'd push for: unit tests for the API layer and guards, lazy-loaded routes, and a scaffold command that generates a new entity module from the reference.
Free scoping call · 30 minutes · No commitment
Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.
Opens a pre-filled email · reply within one business day
The model reads. The code counts.
All projects