◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/Dev Tools
⬡ Dev Tools/2026-06-22Advanced

Serving Both Plain Rork and Rork Max From One Backend — Designing an API Response Contract That Never Breaks Old Binaries

When plain Rork (React Native / Expo) and Rork Max (native Swift) both call the same Cloudflare Workers backend, the whole design centers on not breaking old binaries you cannot force-update. Wrap responses in an envelope, evolve them additively, absorb client differences with capability flags, and retire old contracts safely — shown in code.

Rork577Rork Max235Cloudflare Workers25API versioningarchitecture13

✦ Premium Article

One morning I "tidied up" a response by renaming a field from title to displayTitle and shipped it. On my latest local build, nothing broke. A few hours later, reports started coming in from devices still running the previous version: their list screens had gone blank. The cause was mundane — the old app reached for title, and the value was no longer there.

The decisive way a mobile backend differs from a web one is this: you cannot swap the client out from under your users. On the web, the next reload hands everyone fresh JS. An app sits behind store review and each user's decision to update. People who never update keep hitting your backend today on a binary from six months ago.

That asymmetry gets heavier the moment you start using plain Rork (cross-platform generation via React Native / Expo) alongside Rork Max (native Swift generation). The same feature receives its response through JavaScript fetch on one side and Swift URLSession on the other. As an indie developer I run several Expo apps behind a single Cloudflare Workers backend, and I am currently revisiting the contract on the assumption that native clients from Max will join it later. This article shares the implementation that lets you grow such a backend without breaking it.

Why decide the "contract" first

A response shape is a contract, not a casual promise. Once it is baked into some binary out in the world, you are bound to it until that binary disappears. So the first thing to settle is not the individual fields but the rule for how they are allowed to change.

I keep only two rules. First, every response is wrapped in an envelope that separates the payload from metadata. Second, field changes are additive only — no deletions, no renames, no quiet shifts in meaning. Holding to just these two erases most of the incidents where an old binary falls over.

The minimal response envelope

The envelope is the outer structure every endpoint shares. The payload itself lives in data, and around it you place the protocol version and any operational metadata the server wants to attach. On the Cloudflare Workers side, a thin helper does the wrapping.

// envelope.ts — every response comes back in this shape
type Envelope<T> = {
  protocol: number;       // version of the envelope itself (rarely bumped)
  data: T;                // payload; grows additively only
  meta: {
    serverTime: string;   // ISO8601
    minSupported: number; // clients below this are nudged to update
    notice?: string;      // optional announcement (maintenance, etc.)
  };
};
 
export function ok<T>(data: T, minSupported = 1): Response {
  const body: Envelope<T> = {
    protocol: 1,
    data,
    meta: { serverTime: new Date().toISOString(), minSupported },
  };
  return Response.json(body);
}
 
export function fail(code: string, message: string, status = 400): Response {
  // errors are wrapped too; clients branch on error, not on the presence of data
  return Response.json(
    { protocol: 1, error: { code, message },
      meta: { serverTime: new Date().toISOString(), minSupported: 1 } },
    { status },
  );
}

The point is that both success and error come back in the same envelope. Clients branch on "is there an error," not on "is there data." Do this, and when you later add something like retryAfter to errors, existing clients simply look past it and ignore it. That they ignore it is the whole point of the envelope.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A minimal response envelope that lets one backend serve both a React Native client and a native Swift client without either one breaking the other
✦The additive-only discipline that keeps old binaries alive when you add fields, plus a decision table of the breaking changes you must never make
✦An operational procedure for reading your client-version distribution from logs and deciding the threshold at which it is safe to sunset an old contract
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-05-09
Building a Rork A/B Testing Platform with GrowthBook and PostHog from Scratch
Drop Firebase and pair GrowthBook with PostHog to ship a production-grade A/B testing stack for your Rork apps. SDK wiring, edge evaluation on Cloudflare Workers, Bayesian statistics interpretation, and the operational pitfalls I learned the hard way.
⬡ Dev Tools2026-04-30
Rork × AI Moderation Production Guide: Reporting, Auto-Review, and Tiered Sanctions for UGC Apps
A complete production-grade guide to layering AI moderation, reporting, and tiered sanctions into your Rork UGC app. Includes a working three-layer pipeline using OpenAI Moderation, Perspective API, and Cloudflare Workers Queues.
⬡ Dev Tools2026-10-07
Japanese Headings Won't Turn Bold on Android: How to Trace and Fix a fontWeight That Does Nothing
A troubleshooting note for Rork apps where headings look bold on iPhone but stay thin on Android. How to tell the symptoms apart, pick weights through fontFamily, and verify the fix.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links