The challenge
Fabrilamp is a Spanish lighting fixtures distributor. Its sales team, retail customers, and internal staff each need a different view of a catalog containing thousands of products. The previous system ran on a legacy PrestaShop/PHP stack that couldn’t support offline field sales, multiple languages, or the B2B workflows the business relies on, including custom pricing tiers, quantity breaks, coupons, shipping terms, and invoice management. The storefront also couldn’t safely rely on that stack to stay fast or available.
I was the lead engineer from June 2024 to February 2026 and wrote nearly all of the web storefront. A second engineer worked mainly on the companion iPad app. By the end of the engagement, the codebase had grown to roughly 950 commits in a Bun-based monorepo and had replaced the old PrestaShop setup entirely.
What I built
The result is a B2B e-commerce platform built on Next.js 15. Instead of querying the legacy PrestaShop backend on every request, the storefront keeps its own server-side mirror in a 55-table SQLite schema managed with Drizzle ORM. A dedicated /api/sync endpoint pulls products, prices, stock, customers, coupons, and other entities from Fabrilamp’s legacy API and reconciles them locally.
Catalog browsing, search, and checkout all use this mirror, so the legacy backend cannot take down the storefront when it slows down or becomes unavailable. The companion Abrila Salesman iPad app handles true offline use for field sales reps when there is no connectivity. Server-side rendering supports SEO for the public catalog, while role-based access control adapts the experience to each user type.


Key features
- Server-side product mirror: A dedicated endpoint syncs Fabrilamp’s legacy system into a 55-table SQLite schema. Catalog, search, and checkout use fast local reads instead of live calls to the old backend.
- Role-based dashboards: Admins, salesmen, customers, and employees each see the KPIs, order history, and management tools relevant to their work.
- Multiple languages: The i18n setup covers product descriptions, UI labels, and transactional emails.
- Product catalog: Customers can filter thousands of products by category, finish, and specifications. Dynamic slug-based routing provides the product URLs.
- B2B ordering: The workflow supports draft carts, approvals, custom pricing tiers, and integration with the distributor’s invoicing system.
- No N+1 product queries: Product pages, category grids, and search results fetch products with their images, translations, and category data in one Drizzle relational query instead of making a round trip for each row.
Architecture
Frontend: Next.js 15 with React 19, styled with Tailwind CSS and accessible, composable Radix UI primitives. TanStack Query manages server state, caching, and background updates.
Data layer: SQLite and Drizzle ORM provide the 55-table server-side mirror. The /api/sync endpoint pulls 43 entities, including products, prices, stock, customers, coupons, and carriers, from Fabrilamp’s legacy API in paginated batches. Concurrency limits let entities sync in parallel without overwhelming either system. Zod validates every payload before it is reconciled with the local schema. Storefront reads never touch the legacy backend directly.
Rendering: The public catalog uses server-side rendering for SEO, then client-side navigation for quick transitions once loaded.
Monorepo: The project lives in a Bun monorepo. packages/db contains the schema and migrations, packages/data contains the query layer, and packages/utils contains pricing, coupon, and tax-ID validation logic. The web platform and the Abrila Salesman iPad app share these packages, so the same code calculates a customer’s web price and a sales rep’s iPad price.
Test generation: An autonomous agent built with the Claude Agent SDK works through the shared packages one file at a time. It writes a Vitest test, runs it, makes up to 5 attempts to fix failures, and commits before moving on. One file, one commit. On the packages/ layer, it produced 79 test files and roughly 8,500 lines of test code across about 80 commits. It became part of the day-to-day workflow rather than staying a one-off experiment.
