Overview
This custom Shopify embedded app was built for a multi-location retail operation managing purchase orders, inventory distribution, supplier returns, and physical stock counts — all from both the Shopify admin and the POS terminal. The app runs as a multi-tenant SaaS platform with PostgreSQL Row-Level Security, a strict layered architecture, and real-time inventory synchronisation through the Shopify Admin GraphQL API.
The Challenge
A Shopify Plus Partner based in Ireland needed a backend operations platform for a retail client running multiple physical store locations. The brief was broad: purchase order management with multi-level discount structures, inbound goods reception with barcode scanning, automated stock redistribution across locations, physical inventory counting from POS devices, and supplier return tracking — all integrated with Shopify's inventory system as the source of truth.
What made this technically interesting was the intersection of constraints. The app needed to serve both the admin dashboard (embedded iframe) and POS hardware (iPad tile and modal extensions) using the same data layer. Multi-tenancy had to be enforced at the database level, not just application logic. The inventory operations required atomic coordination between local database writes and Shopify GraphQL mutations — a partial failure in either couldn't leave the system in an inconsistent state. And the entire codebase had to pass zero-tolerance static analysis (SonarQube quality gates, zero any in TypeScript, explicit return types on every model function) enforced through team code review on every pull request.
The initial contract was four weeks. It was extended to eight based on delivery velocity.
Technical Architecture
The app is built on React Router 7, Prisma 6, and PostgreSQL, deployed as an embedded Shopify app with POS extensions written in Preact. The database schema spans 23 migrations, 715 lines of Prisma models, and roughly 25 tables covering everything from purchase orders to automated distribution rules with cron scheduling.
Multi-tenancy is handled through PostgreSQL Row-Level Security. Every transaction sets a session variable (app.current_tenant) via a SECURITY DEFINER function that resolves the shop UUID from the Shopify domain. All tenant tables carry RLS policies filtering on this session variable. The application layer wraps every database call through withTenantByShopDomain(), which opens a Prisma transaction, sets the tenant context, and passes a scoped client to the callback. This means even a bug in application code cannot leak data across tenants — the database itself enforces isolation.
The codebase follows a strict layered architecture: graphql/ for Shopify API calls (no business logic), models/ for atomic Prisma queries (no orchestration), services/ for business logic coordinating multiple model and GraphQL calls, and routes/ kept deliberately thin — authenticate, parse input, delegate to a service, return the response. This separation was enforced through PR review by the team's tech lead.
Key Features Built
Purchase Orders with Multi-Level Discounts
The order system supports distributor-specific purchase conditions with up to four cascading discount levels per line item. Orders flow through creation, reception (partial or complete), and completion. Each reception triggers adjustInventoryQuantities mutations against the Shopify Admin API, with inventoryBulkToggleActivation called first to ensure the product is stocked at the target location — a prerequisite Shopify doesn't enforce automatically.
POS Extensions for Reception, Inventory Counts, and Supplier Returns
Three POS extensions provide tile-and-modal interfaces for store staff. These are Preact apps using Shopify's s-* web components, communicating with the main app through authenticated REST endpoints. POS authentication uses authenticate.pos() returning a session token, with CORS preflight handlers that bypass auth (the browser sends no Bearer token on OPTIONS). State-based routing inside each modal guarantees re-renders and fresh data on screen transitions — the POS navigation API doesn't provide this.
Automated Stock Distribution
A rule engine lets merchants define copy, fill, or donate distribution patterns across locations. Each rule carries date range filters, day-of-week constraints, per-location donor and receive limits, and a cron schedule with IANA timezone support. The distribution algorithm resolves current stock levels per variant per location, computes deltas, and batches the resulting inventory adjustments.
Webhook-Driven Movement Tracking
The app subscribes to inventory_levels/update webhooks and records every stock change as an inventory movement row, resolving the variant, product, SKU, and size label from the Shopify API. This creates a complete Kardex (stock ledger) per product, displayed as a filterable table with per-size columns, source badges, and net stock summaries.
Technical Challenges Solved
Row-Level Security with Prisma
Prisma doesn't natively support PostgreSQL session variables or RLS. The solution was a dual-client architecture: prismaAdmin for the shop table (outside RLS) and prismaTenant for all tenant data. Every tenant operation runs inside $transaction(), which first upserts the shop record, resolves the UUID via a SECURITY DEFINER function, sets app.current_tenant with set_config(), then passes the scoped transaction client to the caller. This keeps RLS invisible to the model layer while guaranteeing enforcement.
POS Extension CORS and Authentication
POS extensions run in a separate JavaScript context and cannot import from the main app bundle. Every POST route needed both an action handler and a separate OPTIONS handler that returns CORS headers without attempting authentication — Shopify's POS client sends preflight requests with no Bearer token. The session token from authenticate.pos() carries the shop domain in sessionToken.dest, which is used to resolve the tenant context for database operations. Getting this wrong produced silent failures: the POS modal would show a generic error toast while the server logged nothing, because the preflight rejection happened before the request reached the route handler.
Inventory Activation Before Adjustment
Shopify's adjustInventoryQuantities mutation fails silently for items not activated at a location — and products default to the primary location only. After encountering item not stocked at location errors, the service layer was updated to call inventoryBulkToggleActivation before every adjustment batch. This is now a standard pattern across reception, supplier returns, and distribution features.
Outcome
The project delivered a full-stack inventory management platform: 22,000 lines of TypeScript across 49 routes, 20 model files, 8 services, and 29 UI components, plus 3 POS extensions and 2,500 lines of SQL migrations. The codebase passes SonarQube quality gates with zero issues and maintains a strict zero-any TypeScript policy. Every model function carries an explicit return type, every component uses Readonly props, and every PR targets the develop branch with individual review comments resolved before merge.
The contract was extended from four weeks to eight based on the quality and pace of delivery. The app is in active use across multiple store locations.
Skills Demonstrated
- Languages & Frameworks: TypeScript, React 18, React Router 7, Preact, PostgreSQL, SQL (DDL, RLS policies, functions)
- Shopify Platform: Admin GraphQL API (2026-04), POS UI Extensions, App Bridge, Resource Picker, embedded app architecture, OAuth with PrismaSessionStorage, webhook subscriptions, metafield definitions, inventoryBulkToggleActivation, adjustInventoryQuantities
- Data & Infrastructure: Prisma 6 ORM, PostgreSQL Row-Level Security, multi-tenant SaaS architecture, dbmate migrations, Docker
- Tooling & Quality: SonarQube quality gates, strict TypeScript (zero any), i18next (EN/ES), Polaris design system, Polaris web components (s-*), ESLint
- Delivery: Async remote delivery across time zones, PR-based workflow with tech lead review, layered architecture enforcement, eight-week engagement extended from four on delivery quality