Project snapshot
- Product: Offline-first iPad field-sales app connected to a separate Next.js B2B platform
- Client: Fabrilamp, a Spanish lighting distributor
- Role: Lead engineer across the shared product system, with hands-on web, data, pricing, sync, and mobile-performance work
- Engagement: Client contract through Wandity Ltd, June 2024 to February 2026
- Scale: 5,000+ products, 11,000+ locally stored images, a 55-table server mirror, and 79 shared-package test files
Outcome and scope
Alfonso led engineering across the Abrila product system and wrote nearly all of the Next.js storefront. He worked hands-on in the shared data, pricing, sync, and mobile-performance layers; a second engineer worked mainly on the iPad application. This case study separates Alfonso’s work from the team’s delivery.
Fabrilamp’s sales representatives visit retail stores across Spain, sometimes with limited or no mobile connectivity. They need the full product catalog, high-quality images, customer management, and B2B ordering to keep working. The iPad stores its product data in SQLite and media in the device filesystem, so reps can browse, prepare orders, and create provisional customers offline. When connectivity returns, typed synchronization sends the local work to the server for validation.
The iPad and the separate Next.js platform share Bun workspace packages for schema, queries, pricing tiers, quantity breaks, coupons, and tax rules. That keeps calculations consistent without pretending the two applications use the same runtime or database.





Scroll for more →
System architecture
Text description
The team-delivered React Native iPad app reads and writes 5,000+ products in on-device SQLite and loads 11,000+ images from the on-device filesystem, allowing field work without connectivity. Alfonso led the shared Bun packages for schema, pricing, coupons, and tax, and worked hands-on on the iPad's query shape, image resolution, and cart-state rendering; a second engineer worked mainly on the iPad application. When connectivity returns, the team-delivered tRPC boundary sends changes for validation. Alfonso led and wrote nearly all of the separate Next.js 15 B2B platform, whose catalog, dashboards, and checkout use a 55-table server-side SQLite mirror rather than the iPad database. Fabrilamp's legacy API feeds that mirror through validated batch synchronization, while external media, EAS, and Sentry services support assets, delivery, and telemetry. Gold and blue labels describe implementation responsibility within the client engagement, not legal ownership of Fabrilamp's systems or sole authorship of team-delivered components.
The mobile application uses Expo, React Native, Expo Router, NativeWind, FlashList, Zustand, and React Query. SQLite through Drizzle stores products, customers, and orders locally; the first product image resolves from the device filesystem and later carousel images load lazily, with CDN URLs used only as fallbacks.
The web platform uses Next.js 15 and a 55-table server-side SQLite mirror. Its catalog, dashboards, and checkout do not read from the iPad database or depend on live storefront calls to the legacy API. tRPC provides typed online synchronization across that boundary.
What I owned
- Engineering leadership and architecture across connected product system
- Nearly all Next.js B2B storefront and server-side catalog mirror
- Shared schema/pricing/coupon/tax/query decisions
- Hands-on iPad performance: query shape, image resolution, cart-state rendering
- Quality gates, monitoring, shared-package testing workflow
Key decisions and trade-offs
- Keep field work local: The iPad needs its own database and media because connectivity can be unavailable. The cost is explicit synchronization and freshness management when pricing and stock change.
- Protect the storefront from the legacy system: The web server mirror isolates catalog and checkout from legacy API latency or downtime. The cost is validation and reconciliation between the API and the 55-table mirror.
- Share business rules across runtimes: Shared Bun packages keep pricing, quantity breaks, coupons, and tax behavior consistent. The cost is coordinated schema compatibility across the React Native and Next.js runtimes.
Technical deep dive
Text description
A sales rep browses, prices, and prepares an order in the team-delivered iPad app. The app reads catalog records and images from on-device SQLite and the filesystem, then persists a provisional customer or order locally while offline. After reconnecting, the app sends batched changes through the team-delivered tRPC synchronization boundary. The server validates the customer, pricing, stock, and order. Valid records are confirmed as synchronized; failed records remain available for correction or retry, without claiming a proprietary conflict-resolution algorithm. The synchronization layer reports records, images, and processing progress to the status screen, which shows completed and remaining work.
The category path was profiled phase by phase. Its largest bottleneck was an N+1-shaped query: SQLite scanned the full product table rather than starting from model_product, while per-card feature icons repeatedly crossed the native filesystem bridge. Reshaping the query and resolving icon URIs in the data layer removed that work before FlashList rendered recycled cards.
Cart state had also lived in one React Context reducer around the application, so a quantity change re-rendered every visible product card. Moving it to Zustand with per-product selectors limited updates to the changed product; CartProvider now mounts only on the cart page, while a lightweight indexed hydration query initializes other screens. The cart still supports customer-specific pricing, quantity breaks, coupons, shipping addresses, payment terms, free-shipping thresholds, and provisional customers.
Production evidence
- Offline scale: The field-sales app carries 5,000+ products in SQLite and 11,000+ images in the local filesystem. Its dedicated status screen reports database records, downloaded images, and processing progress; the captured production state shows 11,091 of 11,118 images downloaded. After 24 hours, another sync is required to refresh pricing and stock.
- Measured query performance: The warm core category query fell from ~63ms to 16.4ms, while cold start fell from 165ms to 73ms. Starting from the correct SQLite driving table reduced the product query from ~48ms to ~4.5ms.
- Measured rendering work: Pre-resolving icon URIs cut a recycled batch of up to 12 cards from ~18ms to ~3.6ms. Per-product Zustand selectors reduced observed re-renders by 97%, and the indexed Zustand hydration query measured ~1.4ms warm.
- Delivery and visibility: EAS handles iPad builds and distribution, while Sentry captures production errors, performance, sync breadcrumbs, and offline state changes. Formatting, lint, and typechecking run through Husky quality gates.
- Tests with honest status: The shared packages have 79 Vitest test files covering pricing, coupons, quantity breaks, shipping, tax, sync utilities, and Drizzle models. A written Maestro plan defines
testIDconventions and login → browse → cart → checkout flows that still need automation; the existing smoke test reaches the login screen on a real build. - Web-specific detail: See Abrila Web for the server mirror, role-based dashboards, and shared-package test-generation workflow.
The measured query and rendering fixes above are the same class of work covered by the React Native audit — profiling a slow screen down to the query and re-render that caused it.
