Skip to content

← Work

My Fit Foods

My Fit Foods is a US meal-prep company, operating since 2008 with physical stores and an established Shopify business that already ran its catalog, checkout, orders, and store fulfillment. The work was a customer mobile app that had to become a new ordering channel inside that operation rather than a second business running next to it: shopping and accounts, subscriptions, scheduled last-mile delivery, meal logging, and a 21-day habit challenge.

I was the technical lead and sole backend developer. I owned the technical direction, built the backend, and set the mobile architecture, working alongside another senior engineer on the app and its deployment.

At a glance

  • Role: Technical lead and sole backend developer; mobile technical direction within a multi-developer app team.
  • Stage: Live since September 2026. Processed about $10K in orders a day in its launch week, with no launch promotion.
  • Core constraint: Add a mobile channel without replacing or duplicating the client's existing Shopify operation.
  • Strongest evidence: About $10K a day in launch-week orders, three Shopify trust boundaries, Nash delivery quoting, timezone-safe challenge data, and 153 passing backend tests.

Context

Staff were already fulfilling orders through Shopify every day, and that could not change. An app with its own catalog, its own order records, or its own admin surface would have left the team running two systems and reconciling them by hand.

That constraint decided the architecture more than any preference did:

  • Shopify had to stay the system of record for products, customers, orders, and subscriptions.
  • Mobile products and orders still needed a clean boundary inside the shared store, so app inventory and pricing could be managed without touching the web or retail business.
  • Everything Shopify does not model — habit challenges, meal logs, delivery scheduling, push notifications — needed a backend of its own that stayed in sync instead of drifting.

My role

I led the technical direction and was the sole developer of the main backend: Shopify and Nash integration, and the domain modules behind customers, products, checkout, orders, subscriptions, reviews, admin controls, and the habit challenge. I also planned the mobile architecture, bootstrapped the app, and built a large share of its shopping, challenge, subscription, and delivery flows.

Architecture

The backend sits between the app and Shopify. It resolves customer identity, reads catalog data, creates and updates carts, brokers subscription contracts, quotes delivery, and owns the product features Shopify has no concept of.

Public-safe My Fit Foods architecture showing the mobile app, Bun and Elysia API, MongoDB, Shopify API boundaries, Nash delivery, and existing fulfillment.

Shopify exposes three APIs with three different trust models, and choosing wrongly means either handing admin scope to a mobile client or losing access to data the app needs. I mapped that boundary deliberately: Storefront for catalog and cart, Customer Account for anything acting on behalf of a signed-in customer, and Admin for store-side operations that must never be reachable from a device. GraphQL or REST was chosen per operation rather than uniformly.

Mobile products and orders are isolated through a dedicated Shopify sales channel. Only products published to that channel appear in the app, and app orders arrive identifiable as mobile without disturbing web or retail fulfillment.

A single Zod schema per domain drives three things at once: request validation at the edge, the OpenAPI contract the app codes against, and — through a Zod-to-Mongoose conversion step — the persistence model. One definition to change instead of three that drift apart.

State Shopify does not own lives in MongoDB: challenge progress, meal logs, delivery tracking, and push tokens. Shopify and Nash both push into the backend by webhook, and both paths verify signatures before doing anything. The environment is validated at boot so a missing credential fails at startup rather than mid-checkout, order reads are checked against customer ownership, and delivery notifications are idempotent so a redelivered webhook cannot notify twice.

Stack

Backend — Bun · ElysiaJS · TypeScript · MongoDB · Zod · OpenAPI

Mobile — React Native · Expo · Expo Router · TanStack Query · Zustand · NativeWind

Platforms — Shopify Storefront, Customer Account, and Admin APIs · Nash · GCP Cloud Run

Engineering challenges

Challenge days that changed meaning across timezones

The problem. The team is globally distributed, which surfaced a bug fast: challenge dates read correctly in one timezone and incorrectly in another. The same user's day could shift depending on where it was viewed from.

Why it was hard. The model stored a user's calendar day as a UTC midnight timestamp. A calendar date and an instant in time are not the same kind of value — midnight UTC is a specific moment, and it lands on the previous day in Chicago. There was no offset to patch, because the stored value had already lost the information needed to recover the day the user meant. Records written under the old model were also already in the database.

The decision. Store each value as what it actually is. The moment onboarding completed stays a UTC instant, because it is a real point in time. The challenge calendar boundaries became plain calendar dates in year-month-day form, because that is what a person means by "day 3". The user's resolved IANA timezone is saved alongside, and the server derives start and end in that zone instead of trusting boundaries computed on a device.

Implementation. Reads normalize legacy values so existing records keep working, with an admin migration path to convert them. Tests cover a timezone matrix across Chicago, Los Angeles, and New York, plus invalid-zone fallback, legacy normalization, and month transitions, which is where date arithmetic usually breaks.

Outcome. The fix shipped in two parts on 17 June 2026, and the updated representation passed all 28 follow-up checks. The tested ambiguity was removed at the data-model boundary instead of patched in display logic.

Delivery quotes that had to cross five boundaries

The problem. During QA, checkout could reach the shipping step showing no delivery rates at all.

Why it was hard. Nothing was obviously broken. The customer's chosen delivery date and time window had to survive the mobile cart, Shopify's carrier-service callback, the backend's store and timezone mapping, Nash's quoting API, and Shopify's rate response contract. If any link dropped or misread the schedule, the checkout screen looked identical: no rates and no explanation.

The decision. Treat the whole path as one contract, make each hop verifiable, and never let a delivery quote take down checkout.

Implementation. The app stamps the delivery date and time window onto cart line attributes. When Shopify calls back for carrier rates, the backend maps the order's origin province to the correct Nash store and its IANA timezone, parses the selected local window, converts it to UTC instants for Nash, then maps valid quotes into Shopify rate objects labeled in the customer's local time. Failures before Nash — missing scheduling attributes, a blocked holiday, an unsupported province, an unparseable window — are handled separately from failures after Nash, such as an empty or malformed quote. Every invalid path returns an empty rate set with a diagnostic log instead of throwing.

Outcome. Rates displayed reliably in QA whenever the scheduling contract was complete, and the cases where they did not became readable from logs rather than guesswork. A bad quote now degrades the shipping step instead of breaking checkout.

Subscription status that disagreed with what the customer paid for

The problem. A FitClub member could still be inside a prepaid term while Shopify reported their subscription contract as canceled. Reading Shopify alone, the app would treat a paying customer as inactive and withhold access they had already bought.

Why it was hard. Billing state and business entitlement look like the same thing right up until they diverge. Shopify's contract status is authoritative for billing, and copying it into the backend would create exactly the drift the rest of the architecture was built to avoid.

The decision. Leave billing where it belongs and model entitlement separately on the server, as an overlay rather than a copy.

Implementation. Selling plans run through catalog and checkout: subscription products and plans come from Storefront, the selected plan is attached at checkout, and pause, activation, and cancellation go through Customer Account APIs. On top of that, a server-side customer metafield records the prepaid term and exposes a single entitlement flag, so the app reads entitlement directly instead of inferring it from contract status.

Outcome. Access follows what the customer paid for across pause, activation, and cancellation, and the app never interprets Shopify billing state on its own.

Two races in the mobile client

The problem. Two bugs turned out to be the same shape. Concurrent requests hitting an expired Shopify Customer Account token each tried to refresh it. Separately, after a cold start the challenge screen could briefly unlock the wrong day before client state finished hydrating.

Why it was hard. Both are timing bugs, so they reproduce inconsistently and look like backend faults from the outside. Competing token refreshes can invalidate each other and sign the customer out. The hydration race is worse than a visual glitch, because a tappable wrong day lets someone log against a day that has not started.

The decision. Give each piece of state exactly one owner.

Implementation. Token refresh became single-flight behind a retry guard, so concurrent unauthorized responses wait on one refresh rather than starting several. For challenge state, I consolidated hydration onto a single owner and added normalization, clamping on the selected day, and guards on the sheets that read it. That came out of a broader refactor of the challenge and shared app layers into controller hooks, domain utilities, and shared types, which is what made a single owner possible.

Outcome. The fixes prevent the two observed failure paths by giving each piece of state one owner instead of retrying around inconsistent behavior.

Outcome

The app launched in September 2026 and processed about $10K in orders a day in its launch week, without a launch campaign behind it. Orders flow through the dedicated mobile sales channel into the same Shopify operation the stores already run, so the company's existing commerce and fulfillment carried on unchanged while a new channel opened.

The client's strongest feedback was about that integration. The mobile channel did not ask staff to replace their commerce workflow, learn a parallel system, or change how they already work in order to support the app.

At the time of writing, the backend passed 153 tests across 24 files, concentrated where mistakes stay quiet: timezone boundaries, webhook verification, and order ownership. Mobile test coverage is the next gap I'd close.