RORK LABJP
MAX — Rork Max builds native Swift apps instead of React Native, supporting iPhone, iPad, Watch, TV, Vision Pro, and iMessageNATIVE — It unlocks native capabilities: AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core MLCORE — Standard Rork generates iOS/Android apps with React Native (Expo), taking you from plain English to the app storesFUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)GROWTH — The platform now sees 743,000 monthly visits with 85% growthPRICING — Free to start, with paid plans from $25/monthMAX — Rork Max builds native Swift apps instead of React Native, supporting iPhone, iPad, Watch, TV, Vision Pro, and iMessageNATIVE — It unlocks native capabilities: AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core MLCORE — Standard Rork generates iOS/Android apps with React Native (Expo), taking you from plain English to the app storesFUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)GROWTH — The platform now sees 743,000 monthly visits with 85% growthPRICING — Free to start, with paid plans from $25/month
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.

Rork437Rork Max180Cloudflare Workers20API versioningarchitecture8

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 $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-05-26
Aggregating App Store Connect, RevenueCat, and AdMob into One Morning Digest with Cloudflare Workers — A Six-App Indie Architecture
A practical architecture for indie developers running multiple Rork apps: aggregate App Store Connect API, RevenueCat REST, and AdMob Reporting API in a Cloudflare Workers Cron job and ship a single daily Slack digest each morning.
Dev Tools2026-06-22
Hardcoding Your OpenAI Key in a Rork (Expo) App Means It Gets Stolen — Slip a Thin Worker Proxy In Between
Embed an OpenAI or Gemini API key directly in the Expo app Rork generates and it can be extracted from the shipped binary. Here is why a key inside an app is never secret, plus a minimal Cloudflare Workers proxy that hides it (streaming passthrough included), simple abuse controls, and key rotation that needs no app review.
Dev Tools2026-06-21
Reclaiming the Pre-Submit Checks You Lose When Publishing Without Xcode
Rork Max's two-click publishing skips the Xcode Organizer, making it easy to miss usage strings, version bumps, and Bundle IDs. Here is a quick pre-submit self-audit you can run yourself, with how to read the generated files.
📚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
See all →