Where the project comes from
Before this tool, looks were composed in a shared spreadsheet. The spreadsheet served as the working interface, and every selection prepared for a customer required a sequence of manipulations that stretched the process out.
The back-office was built for that. Composing an outfit now happens on a single screen, with the product visuals in view, which has sharply cut the time it takes to put a look together. The style questionnaires filled in by customers became visible during composition, instead of being looked up separately.
The context
Wilook Backoffice is the admin tool for a fashion e-commerce business. It covers three daily jobs: keeping the product catalog current, composing outfits by pairing several items, and tracking customers along with their style preferences. It is a desktop tool, used at length and repetitively, which puts list responsiveness ahead of visual flourish.
The application is a Nuxt 4 SPA: server rendering is off, because none of these screens has any reason to be indexed and all of them sit behind authentication. Built solo over three months, from design to deployment.
A back-office is judged on unglamorous criteria, and three constraints drove the decisions.
Lists that keep growing. A fashion catalog runs to thousands of references declined across sizes and colorways. Search, sorting and pagination have to stay stable as it fills out.
Cards that are paid for in the plural. A product card fetching its own thumbnail is painless alone and ruinous by fifty. The real cost of a screen is measured in queries per render, not per component.
A back-office does not expose itself. Product and customer data must only be readable when authenticated, without breaking public image delivery along the way.
The path of a piece of data
The project's structural decision sits in this route: a component displays, it never reaches the database.
display
Vue component
It renders what it is given, it never reaches the database.
state
Domain composable
One per domain: reactive state, pagination, errors.
access
Service layer
One class per domain, the only point of contact with the database.
data
Managed database
PostgreSQL, authentication and image storage.
server
Server route
The dashboard counts server-side, with the user's session.
The service base translates database errors into typed application errors: not found, network, server. No component ever has to recognise an error code from the data layer.
That boundary was not drawn for elegance. It is what allowed unit queries to be replaced by batched loads without touching a single screen.
The technical choices
Data access
One service layer per domain
An abstract class carries query execution and error translation; one class per domain inherits from it. Components only ever see domain objects and typed errors.
Filter state
Stored in the URL
A filtered view becomes an address: shareable, bookmarkable, and compatible with the back button. Nine named parameters, read back by the query instead of being duplicated in memory.
Large lists
Server pagination with infinite scroll
Twenty items per page, offset derived from the current list, concurrent calls ignored and end of list detected. The browser never holds the whole catalog.
Security
Lock at the database level, with the session
The dashboard's server route builds its access from the request cookies, never with an admin key. The lock stays the database's own, even when the computation moves server-side.
Two choices round these out. Server state lives in Vue composables, with no caching library: each domain exposes its refs for list, loading, error, total and end of list, and the scope did not justify one more layer. And the global store is reduced to what is strictly local to the screen, the filter drawer's visibility, since everything else lives on the server or in the URL.
The architecture that follows
The look editor is the one part where the interface leads: five named slots, drag and drop from the catalog, and assigning the look to a customer. The rest of the application is deliberately plain, it is a working tool and not a showcase.
The pass that mattered most
The starting brief was "move work to the server". The assessment showed the route under consideration did not apply here, and that the real cost was elsewhere: in the number of queries a plain render triggered.
Before
- ✗50-product grid: 51 queries, each card fetching its own thumbnail
- ✗Look grid: up to six sequential queries per card, stalling the render
- ✗Lists loaded with a full select, images and descriptions included
- ✗Dashboard: three counts from the browser, neither parallel nor awaited
After
- ✓50-product grid: one query, the thumbnail already being in the row
- ✓Looks hydrated in two batched queries whatever their count
- ✓Dedicated projection for lists, reflected in a summary type
- ✓Dashboard: one server route, three parallel counts
The server-component route was dropped on evidence, not on principle: a fragment rendered server-side by the framework has no access to the privileged data client, which is only exposed to the server layer. The benefit sought was obtained through the stack's native path, a server route, where that access exists.
What the project does not have
There is no automated test suite in this repository. Quality control rests on strict typing, the linter's static analysis and manual screen verification. That is a scope call owned on three months solo, and it is also the first thing I would revisit: the service layer, isolated from the rest and with no dependency on the interface, is exactly the boundary that would make those tests cheap to write.
Where the product stands
Delivered after three months. The application handles the catalog, outfit composition and customer profiles, with filters in the URL, pagination resolved by the database and reads locked at the database level. The performance pass brought list screens down to a handful of queries where they used to trigger dozens.



